Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ on:
required: false
default: false
type: boolean
release_channel:
description: 'TestFlight release channel'
required: false
default: 'internal'
type: choice
options:
- internal
- public_beta

env:
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
Expand Down Expand Up @@ -85,7 +93,7 @@ jobs:
echo "✅ Successfully created tag: $TAG_NAME"

build-and-release:
name: Build and Release to TestFlight (Public Beta)
name: Build and Release to TestFlight (Internal Testing)
needs: version-check
if: needs.version-check.outputs.should_release == 'true'
runs-on: macos-latest
Expand Down Expand Up @@ -283,6 +291,7 @@ jobs:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
TEAM_ID: ${{ secrets.TEAM_ID }}
RELEASE_CHANNEL: ${{ github.event.inputs.release_channel || 'internal' }}
run: |
# Enable verbose output for debugging
export FASTLANE_VERBOSE=true
Expand All @@ -291,21 +300,26 @@ jobs:
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8

# Run the beta lane (includes waiting for processing and public beta distribution)
fastlane beta
# Run the beta lane with release channel parameter
# Default to 'internal' for automatic releases (push to main)
# Can be set to 'public_beta' for manual workflow_dispatch
fastlane beta channel:$RELEASE_CHANNEL

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
env:
RELEASE_CHANNEL: ${{ github.event.inputs.release_channel || 'internal' }}
with:
tag_name: ${{ needs.version-check.outputs.new_tag }}
name: Release ${{ needs.version-check.outputs.version }}
body: |
## 🚀 Version ${{ needs.version-check.outputs.version }}
Build: ${{ needs.version-check.outputs.build }}

### TestFlight Public Beta
This version has been automatically submitted to TestFlight for public beta testing.
External testers will receive email notifications when the build is available.
### TestFlight Distribution
**Channel**: ${{ github.event.inputs.release_channel || 'internal' }}

${{ github.event.inputs.release_channel == 'public_beta' && '📧 **Public Beta**: This build has been submitted to TestFlight for public beta testing. External testers will receive email notifications when the build is available after beta review approval.' || '👥 **Internal Testing**: This build is available to internal testers immediately after processing. No beta review required.' }}

### What's New
- See [commit history](https://github.com/${{ github.repository }}/commits/${{ needs.version-check.outputs.new_tag }}) for changes
Expand All @@ -317,8 +331,16 @@ jobs:

- name: Post release notification
if: success()
env:
RELEASE_CHANNEL: ${{ github.event.inputs.release_channel || 'internal' }}
run: |
echo "✅ Successfully released version ${{ needs.version-check.outputs.version }} to TestFlight Public Beta!"
CHANNEL_DISPLAY=$([ "$RELEASE_CHANNEL" = "public_beta" ] && echo "Public Beta" || echo "Internal Testing")
echo "✅ Successfully released version ${{ needs.version-check.outputs.version }} to TestFlight $CHANNEL_DISPLAY!"
echo "🏷️ Tag: ${{ needs.version-check.outputs.new_tag }}"
echo "🔢 Build: ${{ needs.version-check.outputs.build }}"
echo "📧 External testers will be notified via email"
echo "📦 Channel: $RELEASE_CHANNEL"
if [ "$RELEASE_CHANNEL" = "public_beta" ]; then
echo "📧 External testers will be notified after beta review approval"
else
echo "👥 Internal testers can access the build immediately after processing"
fi
6 changes: 6 additions & 0 deletions V2er/View/Widget/Updatable/HeadIndicatorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ struct HeadIndicatorView: View {
.foregroundColor(.secondaryText)
}
}
.onAppear {
// Initialize animatedOnlineCount when view appears with valid stats
if animatedOnlineCount == 0 && stats.onlineCount > 0 {
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition animatedOnlineCount == 0 could prevent legitimate updates if the actual online count is 0. If the view appears when stats.onlineCount is 0 and later increases, the initialization won't trigger on subsequent appearances. Consider using a more explicit flag to track initialization state, or simplify the condition to just check if stats.onlineCount > 0.

Suggested change
if animatedOnlineCount == 0 && stats.onlineCount > 0 {
if animatedOnlineCount != stats.onlineCount {

Copilot uses AI. Check for mistakes.
animatedOnlineCount = stats.onlineCount
}
}
.onChange(of: stats.onlineCount) { newValue in
withAnimation(.easeInOut(duration: 0.3)) {
animatedOnlineCount = newValue
Expand Down
92 changes: 53 additions & 39 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ platform :ios do
)
end

desc "Distribute existing build to beta testers"
desc "Distribute existing build to internal testers"
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The description should clarify that this lane distributes the most recent build, not necessarily a specific build number, since no build_number parameter validation is shown in the implementation.

Suggested change
desc "Distribute existing build to internal testers"
desc "Distribute the most recent build to internal testers"

Copilot uses AI. Check for mistakes.
lane :distribute_beta do |options|
# Get App Store Connect API key
api_key = get_api_key
Expand All @@ -76,38 +76,38 @@ platform :ios do
build_number = options[:build_number]

begin
# Distribute to beta testers
# Distribute to internal testers only
testflight(
api_key: api_key,
app_identifier: "v2er.app",
skip_submission: false,
distribute_external: true, # Distribute to external testers (public beta)
groups: ["Public Beta", "External Testers", "Beta Testers"], # Public beta groups
notify_external_testers: true, # Send email notifications
uses_non_exempt_encryption: false,
submit_beta_review: true, # Automatically submit for Beta review
skip_submission: true, # Skip beta review for internal testing
distribute_external: false, # Internal testing only (not public beta)
wait_for_uploaded_build: true,
beta_app_description: "V2er is an elegant third-party client for V2EX forum",
beta_app_feedback_email: "support@v2er.app",
demo_account_required: false,
beta_app_review_info: {
contact_email: "support@v2er.app",
contact_first_name: "V2er",
contact_last_name: "Support",
contact_phone: "+86 13800138000",
notes: "This is a third-party client app for V2EX forum. No special account needed for testing."
}
uses_non_exempt_encryption: false
)

UI.success("✅ Successfully distributed build to beta testers!")
UI.success("✅ Successfully distributed build to internal testers!")
rescue => e
UI.error("Failed to distribute: #{e.message}")
UI.message("You may need to manually distribute the build in App Store Connect")
end
end

desc "Build and upload to TestFlight"
lane :beta do
desc "Parameters:"
desc " channel: 'internal' (default) or 'public_beta'"
lane :beta do |options|
# Get release channel from options or environment variable
# Default to 'internal' if not specified
channel = options[:channel] || ENV['RELEASE_CHANNEL'] || 'internal'

UI.message("📦 Release channel: #{channel}")

# Validate channel parameter
unless ['internal', 'public_beta'].include?(channel)
UI.user_error!("Invalid channel: #{channel}. Must be 'internal' or 'public_beta'")
end

# Validate that changelog exists for current version
unless ChangelogHelper.validate_changelog_exists
UI.user_error!("Please update CHANGELOG.md with an entry for the current version before releasing!")
Expand Down Expand Up @@ -166,31 +166,45 @@ platform :ios do
# Extract changelog for the current version
changelog_content = ChangelogHelper.extract_changelog(current_version)

# Upload to TestFlight
upload_to_testflight(
# Configure TestFlight upload based on release channel
is_public_beta = channel == 'public_beta'

upload_params = {
api_key: api_key,
skip_submission: false,
skip_submission: !is_public_beta, # Skip beta review for internal, submit for public beta
skip_waiting_for_build_processing: false, # Wait for processing before distribution
wait_processing_interval: 30, # Check every 30 seconds
wait_processing_timeout_duration: 900, # Wait up to 15 minutes for processing
distribute_external: true, # Distribute to external testers (public beta)
distribute_external: is_public_beta, # Internal testing by default, external for public beta
distribute_only: false, # Upload and distribute in one action
groups: ["Public Beta", "External Testers", "Beta Testers"], # Public beta groups
changelog: changelog_content, # Use changelog from CHANGELOG.md
notify_external_testers: true, # Send email notifications to external testers
uses_non_exempt_encryption: false, # Required for automatic distribution
submit_beta_review: true, # Automatically submit for Beta review
beta_app_description: "V2er is an elegant third-party client for V2EX forum",
beta_app_feedback_email: "support@v2er.app",
demo_account_required: false, # No demo account required
beta_app_review_info: {
contact_email: "support@v2er.app",
contact_first_name: "V2er",
contact_last_name: "Support",
contact_phone: "+86 13800138000",
notes: "This is a third-party client app for V2EX forum. No special account needed for testing."
}
)
uses_non_exempt_encryption: false # Required export compliance
}

# Add public beta specific parameters
if is_public_beta
upload_params.merge!({
groups: ["Public Beta", "External Testers", "Beta Testers"], # Public beta groups
notify_external_testers: true, # Send email notifications to external testers
submit_beta_review: true, # Automatically submit for Beta review
beta_app_description: "V2er is an elegant third-party client for V2EX forum",
beta_app_feedback_email: "support@v2er.app",
demo_account_required: false, # No demo account required
beta_app_review_info: {
contact_email: "support@v2er.app",
contact_first_name: "V2er",
contact_last_name: "Support",
contact_phone: "+86 13800138000",
notes: "This is a third-party client app for V2EX forum. No special account needed for testing."
}
})
UI.message("📧 Public beta mode: Will notify external testers and submit for beta review")
else
UI.message("👥 Internal testing mode: No beta review submission required")
end

# Upload to TestFlight
upload_to_testflight(upload_params)

# Notify success
notification(
Expand Down
Loading