Skip to content
Draft
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
195 changes: 190 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ permissions:
contents: write

jobs:
release:
name: Release to App Store
# ============================================================================
# Phase 1: Build IPA and screenshot test bundle in parallel
# ============================================================================
release-build:
name: Build Release
runs-on: macos-26
timeout-minutes: 120
timeout-minutes: 60
steps:
- name: Generate GitHub App Token
id: github_app_token
Expand Down Expand Up @@ -51,8 +54,190 @@ jobs:
--arg key "$APP_STORE_CONNECT_API_PRIVATE_KEY" \
'{key_id: $key_id, issuer_id: $issuer_id, key: $key}' > fastlane/api-key.json

- name: Release to App Store
run: bundle exec fastlane release_ci
- name: Build, Validate, and Setup Sentry
run: bundle exec fastlane release_ci_build
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_PRIVATE_KEY: ${{ secrets.MATCH_GIT_PRIVATE_KEY }}
LICENSE_PLIST_GITHUB_TOKEN: ${{ steps.github_app_token.outputs.token }}
RELEASE_BOT_TOKEN: ${{ steps.github_app_token.outputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}

- name: Upload Release Artifacts
uses: actions/upload-artifact@v7
with:
name: release-build
path: |
Flinky.ipa
Flinky.app.dSYM.zip
Flinky.xcarchive
version.txt
build_number.txt
Flinky.xcodeproj/project.pbxproj
Targets/App/Sources/Resources/Settings.bundle/Root.plist
retention-days: 1

- name: Run CI Diagnostics
if: failure()
run: ./Scripts/ci-diagnostics.sh

build-screenshots:
name: Build Screenshots
runs-on: macos-26
timeout-minutes: 30
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
ref: main
submodules: true

- name: Install Dependencies
run: brew bundle --file Brewfile-ci

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true

- name: Build Screenshot Test Bundle
run: bundle exec fastlane build_screenshots derived_data_path:/tmp/screenshot_build
env:
LICENSE_PLIST_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload Test Bundle
uses: actions/upload-artifact@v7
with:
name: screenshot-test-bundle
path: /tmp/screenshot_build/Build/Products/
retention-days: 1

- name: Run CI Diagnostics
if: failure()
run: ./Scripts/ci-diagnostics.sh

# ============================================================================
# Phase 2: Run screenshots on each device in parallel
# ============================================================================

screenshot:
name: Screenshot ${{ matrix.device }}
runs-on: macos-26
timeout-minutes: 30
needs: build-screenshots
strategy:
fail-fast: false
matrix:
device:
- "iPhone 17 Pro Max"
- "iPhone 17 Pro"
- "iPad Pro 13-inch (M5)"
- "iPad Pro 11-inch (M5)"
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
ref: main
submodules: true

- name: Install Dependencies
run: brew bundle --file Brewfile-ci

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true

- name: Download Test Bundle
uses: actions/download-artifact@v7
with:
name: screenshot-test-bundle
path: /tmp/screenshot_build/Build/Products/

- name: Run Screenshots
run: bundle exec fastlane run_screenshot_on_device "device:${{ matrix.device }}" derived_data_path:/tmp/screenshot_build

- name: Upload Device Screenshots
uses: actions/upload-artifact@v7
if: always()
with:
name: screenshots-${{ matrix.device }}
path: ~/Library/Caches/tools.fastlane/screenshots/*.png
retention-days: 1

- name: Run CI Diagnostics
if: failure()
run: ./Scripts/ci-diagnostics.sh

# ============================================================================
# Phase 3: Collect screenshots and upload to App Store
# ============================================================================

release-upload:
name: Upload to App Store
runs-on: macos-26
timeout-minutes: 60
needs: [release-build, screenshot]
steps:
- name: Generate GitHub App Token
id: github_app_token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.TECHPRIMATE_RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.TECHPRIMATE_RELEASE_BOT_PRIVATE_KEY }}

- name: Checkout Code
uses: actions/checkout@v6
with:
ref: main
submodules: true
token: ${{ steps.github_app_token.outputs.token }}

- name: Install Dependencies
run: brew bundle --file Brewfile-ci

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true

- name: Create App Store Connect API Key
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_API_ISSUER_ID }}
APP_STORE_CONNECT_API_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }}
run: |
jq -n \
--arg key_id "$APP_STORE_CONNECT_API_KEY_ID" \
--arg issuer_id "$APP_STORE_CONNECT_API_ISSUER_ID" \
--arg key "$APP_STORE_CONNECT_API_PRIVATE_KEY" \
'{key_id: $key_id, issuer_id: $issuer_id, key: $key}' > fastlane/api-key.json

- name: Download Release Build
uses: actions/download-artifact@v7
with:
name: release-build
path: .

- name: Download All Screenshots
uses: actions/download-artifact@v7
with:
pattern: screenshots-*
path: ~/Library/Caches/tools.fastlane/screenshots/
merge-multiple: true

- name: Collect Screenshots
run: bundle exec fastlane collect_screenshots

- name: Read Version Info
id: version
run: |
echo "version=$(cat version.txt)" >> "$GITHUB_OUTPUT"
echo "build=$(cat build_number.txt)" >> "$GITHUB_OUTPUT"

- name: Upload to App Store and Submit for Review
run: bundle exec fastlane release_ci_upload version:${{ steps.version.outputs.version }} build:${{ steps.version.outputs.build }}
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
Expand Down
4 changes: 3 additions & 1 deletion fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
# ├── Fastfile # This file - imports and platform definition
# ├── lanes/
# │ ├── build.rb # build_ci lane
# │ ├── release.rb # beta, release_beta_ci, publish
# │ ├── release.rb # beta, release_beta_ci, publish, release_ci_build, release_ci_upload
# │ ├── utilities.rb # generate_*, upload_metadata, setup_code_signing, bump_*
# │ ├── screenshots.rb # Parallel screenshot lanes (build_screenshots, run_screenshot_on_device, etc.)
# │ ├── helpers.rb # Private helper lanes (_bump_version, _build_app_for_store, etc.)
# │ ├── sentry.rb # Sentry integration lanes
# │ ├── version.rb # Version management lanes
Expand All @@ -38,4 +39,5 @@ platform :ios do
import "lanes/build.rb"
import "lanes/release.rb"
import "lanes/utilities.rb"
import "lanes/screenshots.rb"
end
81 changes: 81 additions & 0 deletions fastlane/lanes/release.rb
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,84 @@
# Commit and tag on main via GitHub API (creates a signed, verified commit)
_commit_and_tag_version_signed(version: version_number, build: build_number)
end

desc <<~DESC
Release CI: Build phase
Prepares version, builds IPA, validates, and sets up Sentry release.
Outputs version and build number for downstream jobs.
Used by the parallel release workflow (release.yml).
DESC
lane :release_ci_build do
setup_ci if is_ci

# Prepare: check App Store Connect, bump patch if needed, get next build from TestFlight
version_check_result = _check_and_bump_version_if_needed
version_number = version_check_result[:version]
build_number = _get_next_build_number(version: version_number)
_make(target: "generate")

# Build and validate
_setup_code_signing
_build_app_for_store
_validate_app
_setup_sentry_release(version: version_number, build: build_number)

# Write version info for downstream jobs
Dir.chdir("..") do
File.write("version.txt", version_number)
File.write("build_number.txt", build_number)
end

UI.success "✅ Release build complete: #{version_number} (#{build_number})"
end

desc <<~DESC
Release CI: Upload phase
Uploads IPA and screenshots to App Store Connect, submits for review,
finalizes Sentry release, and commits/tags the version.
Expects IPA at project root and screenshots in fastlane/screenshots/.
Options:
version: version number (required)
build: build number (required)
DESC
lane :release_ci_upload do |options|
setup_ci if is_ci

version_number = options[:version]
build_number = options[:build]

UI.user_error!("version is required") unless version_number
UI.user_error!("build is required") unless build_number

# Upload to App Store Connect with metadata, screenshots, and submit for review
upload_to_app_store(
api_key_path: File.expand_path("./api-key.json"),
ipa: File.expand_path("../Flinky.ipa"), # Explicit path to avoid relying on SharedValues

app_version: version_number,
build_number: build_number,

skip_binary_upload: false,
overwrite_screenshots: true,
submit_for_review: true,

run_precheck_before_submit: false,
precheck_include_in_app_purchases: false,

languages: ["en-US"],
metadata_path: File.expand_path("./metadata"),
screenshots_path: File.expand_path("./screenshots"),

force: true, # Skip the preview HTML

app_review_information: {
email_address: ENV["APP_REVIEW_EMAIL_ADDRESS"],
phone_number: ENV["APP_REVIEW_PHONE_NUMBER"]
}
)

_finalize_sentry_release(version: version_number, build: build_number)

# Commit and tag on main via GitHub API (creates a signed, verified commit)
_commit_and_tag_version_signed(version: version_number, build: build_number)
end
Loading
Loading