From 727446eca007556fe258e39439a6619875639906 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Tue, 14 Jul 2026 13:48:22 +0200 Subject: [PATCH 1/2] Add prepare_release lane + Prepare App Store Version workflow Adds a 4th release step that prepares an App Store version for review without submitting, so the only manual action left is clicking Submit in App Store Connect. The prepare_release lane uses deliver to: create/update the App Store version, attach the latest processed TestFlight build, push the release notes from fastlane/metadata/en-US/release_notes.txt (a fixed string, overridable in ASC), and mark the version for automatic release once approved. Screenshots and all other metadata are left untouched (managed manually in ASC). Takes an optional version input, defaulting to the branch MARKETING_VERSION. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/prepare-release.yml | 29 ++++++++++++++++ RELEASING.md | 5 ++- fastlane/Fastfile | 41 +++++++++++++++++++++++ fastlane/metadata/en-US/release_notes.txt | 1 + 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 fastlane/metadata/en-US/release_notes.txt diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..9a49f14 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,29 @@ +name: Prepare App Store Version + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to prepare (defaults to the branch MARKETING_VERSION)' + required: false + +jobs: + prepare: + name: Prepare App Store Version + runs-on: macos-15 + environment: release + if: github.actor == 'adborbas' + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare App Store version + env: + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }} + run: bundle exec fastlane prepare_release version:"${{ inputs.version }}" diff --git a/RELEASING.md b/RELEASING.md index 88c32fa..62863de 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,14 +2,17 @@ Hemera uses release branches to allow development to continue on `main` while a release stabilizes (e.g., during Apple review). -The release is automated with [fastlane](fastlane/Fastfile) and GitHub Actions. Each of the three steps below has a matching lane you can run locally and a `workflow_dispatch` workflow you can trigger from the GitHub **Actions** tab: +The release is automated with [fastlane](fastlane/Fastfile) and GitHub Actions. Each step below has a matching lane you can run locally and a `workflow_dispatch` workflow you can trigger from the GitHub **Actions** tab: | Step | Lane | Workflow | |---|---|---| | Cut a release | `bundle exec fastlane cut_release version:1.3.0` | **Cut Release** | | Upload to TestFlight | `bundle exec fastlane beta` | **Upload to TestFlight** | +| Prepare App Store version | `bundle exec fastlane prepare_release version:1.3.0` | **Prepare App Store Version** | | Publish | `bundle exec fastlane publish version:1.3.0` | **Publish Release** | +**Prepare App Store version** creates/updates the App Store version, attaches the latest processed TestFlight build, sets the release notes (from `fastlane/metadata/en-US/release_notes.txt`), and marks it for automatic release once approved. It stops short of submitting — you click **Submit for Review** in App Store Connect. Release notes are a fixed string; edit that file (or override in ASC) to change them. Screenshots and all other metadata are managed manually in App Store Connect (the lane leaves them untouched). + The manual git/`gh` steps below are what each lane does under the hood, kept for reference and for one-off manual releases. ## Cutting a release diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 2b2fae2..0ac6384 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -136,6 +136,47 @@ platform :ios do UI.success("Published #{tag}. Opened a back-merge PR for #{branch} → main.") end + + # --------------------------------------------------------------------------- + # 4. Prepare an App Store version for review + # Creates/updates the App Store version, attaches the latest processed + # build, sets the release notes (from fastlane/metadata), and marks it for + # automatic release — everything except the final Submit, which stays a + # manual click in App Store Connect. + # Usage: bundle exec fastlane prepare_release [version:1.4.0] + # --------------------------------------------------------------------------- + desc "Prepare an App Store version for review (does not submit)" + lane :prepare_release do |options| + version = options[:version] + version = nil if version.to_s.strip.empty? + version ||= get_xcconfig_value(key: "MARKETING_VERSION") + + api_key = app_store_connect_api_key( + key_id: ENV.fetch("ASC_KEY_ID"), + issuer_id: ENV.fetch("ASC_ISSUER_ID"), + key_content: ENV.fetch("ASC_KEY_P8"), + is_key_content_base64: true, + in_house: false + ) + + # Attach the latest build already uploaded to TestFlight for this version. + build_number = latest_testflight_build_number(api_key: api_key, version: version) + + deliver( + api_key: api_key, + app_version: version, + build_number: build_number.to_s, + skip_binary_upload: true, # the build is uploaded by the beta lane + skip_screenshots: true, # screenshots are managed manually in ASC + skip_metadata: false, # push release_notes from fastlane/metadata + automatic_release: true, # go live automatically once approved + submit_for_review: false, # prepare only — submit stays a manual step + run_precheck_before_submit: false, + force: true # no interactive HTML preview on CI + ) + + UI.success("Prepared App Store version #{version} (build #{build_number}) for review. Submit it in App Store Connect when ready.") + end end # Set a KEY = VALUE line in Config/Shared.xcconfig in place. diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt new file mode 100644 index 0000000..f51a6dc --- /dev/null +++ b/fastlane/metadata/en-US/release_notes.txt @@ -0,0 +1 @@ +Bug fixes and small improvements to make Hemera a little more useful every day. From 96a42d4c0a5cdb572fc14923a0dc106677e3fdcf Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Tue, 14 Jul 2026 13:53:21 +0200 Subject: [PATCH 2/2] Add optional build input to prepare_release Lets you pin a specific TestFlight build for the App Store version instead of always using the latest. Defaults to the latest processed build when omitted. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/prepare-release.yml | 5 ++++- RELEASING.md | 2 +- fastlane/Fastfile | 10 +++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 9a49f14..8845458 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -6,6 +6,9 @@ on: version: description: 'Version to prepare (defaults to the branch MARKETING_VERSION)' required: false + build: + description: 'TestFlight build number to attach (defaults to the latest for the version)' + required: false jobs: prepare: @@ -26,4 +29,4 @@ jobs: ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }} - run: bundle exec fastlane prepare_release version:"${{ inputs.version }}" + run: bundle exec fastlane prepare_release version:"${{ inputs.version }}" build:"${{ inputs.build }}" diff --git a/RELEASING.md b/RELEASING.md index 62863de..d754695 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -11,7 +11,7 @@ The release is automated with [fastlane](fastlane/Fastfile) and GitHub Actions. | Prepare App Store version | `bundle exec fastlane prepare_release version:1.3.0` | **Prepare App Store Version** | | Publish | `bundle exec fastlane publish version:1.3.0` | **Publish Release** | -**Prepare App Store version** creates/updates the App Store version, attaches the latest processed TestFlight build, sets the release notes (from `fastlane/metadata/en-US/release_notes.txt`), and marks it for automatic release once approved. It stops short of submitting — you click **Submit for Review** in App Store Connect. Release notes are a fixed string; edit that file (or override in ASC) to change them. Screenshots and all other metadata are managed manually in App Store Connect (the lane leaves them untouched). +**Prepare App Store version** creates/updates the App Store version, attaches a TestFlight build (the latest processed one by default, or a specific build via the `build` input), sets the release notes (from `fastlane/metadata/en-US/release_notes.txt`), and marks it for automatic release once approved. It stops short of submitting — you click **Submit for Review** in App Store Connect. Release notes are a fixed string; edit that file (or override in ASC) to change them. Screenshots and all other metadata are managed manually in App Store Connect (the lane leaves them untouched). The manual git/`gh` steps below are what each lane does under the hood, kept for reference and for one-off manual releases. diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0ac6384..b4a1ffa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -143,7 +143,8 @@ platform :ios do # build, sets the release notes (from fastlane/metadata), and marks it for # automatic release — everything except the final Submit, which stays a # manual click in App Store Connect. - # Usage: bundle exec fastlane prepare_release [version:1.4.0] + # Usage: bundle exec fastlane prepare_release [version:1.4.0] [build:12] + # version defaults to the branch MARKETING_VERSION; build to the latest. # --------------------------------------------------------------------------- desc "Prepare an App Store version for review (does not submit)" lane :prepare_release do |options| @@ -159,8 +160,11 @@ platform :ios do in_house: false ) - # Attach the latest build already uploaded to TestFlight for this version. - build_number = latest_testflight_build_number(api_key: api_key, version: version) + # Attach a specific build if given, otherwise the latest build already + # uploaded to TestFlight for this version. + build_number = options[:build] + build_number = nil if build_number.to_s.strip.empty? + build_number ||= latest_testflight_build_number(api_key: api_key, version: version) deliver( api_key: api_key,