From 6f280f4ca36a8303395fb1e46df362077b464b80 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 19:15:05 +0000 Subject: [PATCH 01/10] Add Fastlane App Store harness and tag-split CI workflows - Install Bundler-managed Fastlane (gym, pilot, deliver) under ios/ with lanes: preflight, build, beta, metadata_only, release. - Add canonical en-US deliver metadata plus review_information notes. - Split tags: ios-v*-build* triggers TestFlight; strict ios-v semver triggers Fastlane release workflow with optional IOS_SUBMIT_FOR_REVIEW var. - Extend dry-run script for both workflows and update runbooks plus evidence log. Closes #242 Co-authored-by: Bretton Auerbach --- .github/workflows/ios-app-store-release.yml | 225 ++++++++++++ .github/workflows/ios-testflight.yml | 4 +- .gitignore | 3 + README.md | 6 +- docs/operations/automation-evidence.md | 53 +++ docs/operations/ios-app-store-submission.md | 6 +- ios/Gemfile | 2 + ios/Gemfile.lock | 336 ++++++++++++++++++ ios/RELEASING.md | 45 ++- ios/docs/app-store-metadata.md | 2 +- ios/fastlane/Appfile | 7 +- ios/fastlane/Deliverfile | 11 + ios/fastlane/Fastfile | 69 +++- ios/fastlane/metadata/en-US/description.txt | 3 + ios/fastlane/metadata/en-US/keywords.txt | 1 + ios/fastlane/metadata/en-US/marketing_url.txt | 1 + ios/fastlane/metadata/en-US/name.txt | 1 + ios/fastlane/metadata/en-US/privacy_url.txt | 1 + ios/fastlane/metadata/en-US/release_notes.txt | 5 + ios/fastlane/metadata/en-US/subtitle.txt | 1 + ios/fastlane/metadata/en-US/support_url.txt | 1 + .../metadata/review_information/notes.txt | 10 + scripts/ios-app-store-dry-run.mjs | 68 ++-- 23 files changed, 822 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/ios-app-store-release.yml create mode 100644 docs/operations/automation-evidence.md create mode 100644 ios/Gemfile.lock create mode 100644 ios/fastlane/Deliverfile create mode 100644 ios/fastlane/metadata/en-US/description.txt create mode 100644 ios/fastlane/metadata/en-US/keywords.txt create mode 100644 ios/fastlane/metadata/en-US/marketing_url.txt create mode 100644 ios/fastlane/metadata/en-US/name.txt create mode 100644 ios/fastlane/metadata/en-US/privacy_url.txt create mode 100644 ios/fastlane/metadata/en-US/release_notes.txt create mode 100644 ios/fastlane/metadata/en-US/subtitle.txt create mode 100644 ios/fastlane/metadata/en-US/support_url.txt create mode 100644 ios/fastlane/metadata/review_information/notes.txt diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml new file mode 100644 index 00000000..5bd913da --- /dev/null +++ b/.github/workflows/ios-app-store-release.yml @@ -0,0 +1,225 @@ +name: iOS App Store release (Fastlane) + +on: + push: + tags: + # Strict semver: ios-v1.2.3 (no -build suffix — those stay on TestFlight workflow). + - 'ios-v[0-9]+.[0-9]+.[0-9]+' + +permissions: + contents: read + +concurrency: + group: ios-app-store-release + cancel-in-progress: false + +jobs: + pre-flight-tests: + name: Release-config UI smoke gate + runs-on: macos-26 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install XcodeGen + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + shell: bash + run: | + for attempt in 1 2 3; do + if brew install xcodegen; then + exit 0 + fi + if [[ "$attempt" -lt 3 ]]; then + sleep $((attempt * 10)) + fi + done + echo "Failed to install xcodegen after 3 attempts." + exit 1 + + - name: Generate Xcode project + working-directory: ios + run: xcodegen generate + + - name: Run iOS smoke lane in Release configuration + env: + E2E_ENV: ci + E2E_SECRETS_REQUIRED: "false" + IOS_TEST_CONFIGURATION: "Release" + run: npm run e2e:ios:smoke + + - name: Upload simulator diagnostic reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-app-store-release-preflight-diagreports + path: ~/Library/Logs/DiagnosticReports/** + if-no-files-found: ignore + retention-days: 14 + + - name: Upload xcresult bundle + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-app-store-release-preflight-xcresult + path: artifacts/e2e/ios/** + if-no-files-found: warn + retention-days: 14 + + fastlane-release: + needs: pre-flight-tests + runs-on: macos-26 + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate tag matches ios project marketing version + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF#refs/tags/}" + if [[ ! "$TAG" =~ ^ios-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Tag must be ios-vMAJOR.MINOR.PATCH (got $TAG)" + exit 1 + fi + VER="${TAG#ios-v}" + MARKETING="$(grep -E '^\s+MARKETING_VERSION:' ios/project.yml | head -1 | sed -E 's/.*MARKETING_VERSION:\s*"?([^"#]+)"?.*/\1/')" + if [[ "$VER" != "$MARKETING" ]]; then + echo "::error::Tag version $VER does not match ios/project.yml MARKETING_VERSION=$MARKETING" + exit 1 + fi + + - name: Validate release-readiness artifacts + shell: bash + run: | + set -euo pipefail + if ! test -f ios/PARITY_CHECKLIST.md; then + echo "::error::Missing ios/PARITY_CHECKLIST.md" + exit 1 + fi + if ! test -f ios/QA_CHECKLIST.md; then + echo "::error::Missing ios/QA_CHECKLIST.md" + exit 1 + fi + if ! grep -qE '^- \[[xX]\] Feature parity checklist between iOS and web is completed\.$' ios/PARITY_CHECKLIST.md; then + echo "::error::Missing PARITY_CHECKLIST entry: Feature parity checklist between iOS and web is completed" + exit 1 + fi + if ! grep -qE '^- \[[xX]\] All critical parity gaps are fixed or explicitly deferred with owner/date\.$' ios/PARITY_CHECKLIST.md; then + echo "::error::Missing PARITY_CHECKLIST entry: All critical parity gaps are fixed or explicitly deferred with owner/date" + exit 1 + fi + if ! grep -qE '^- \[[xX]\] Regression/QA pass for release candidate is completed\.$' ios/QA_CHECKLIST.md; then + echo "::error::Missing QA_CHECKLIST entry: Regression/QA pass for release candidate is completed" + exit 1 + fi + if ! grep -qE '^- \[[xX]\] App Store metadata/versioning/release notes are finalized\.$' ios/QA_CHECKLIST.md; then + echo "::error::Missing QA_CHECKLIST entry: App Store metadata/versioning/release notes are finalized" + exit 1 + fi + + - name: Generate App Store submission dry-run evidence + run: npm run ios:app-store:dry-run -- --release-owner "GitHub Actions App Store release" + + - name: Upload App Store submission dry-run evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-app-store-dry-run-release + path: artifacts/ios-app-store-dry-run/** + if-no-files-found: error + retention-days: 30 + + - name: Verify Xcode and iOS SDK versions + shell: bash + run: | + xcodebuild -version + SDK_VERSION="$(xcrun --sdk iphoneos --show-sdk-version)" + echo "iPhoneOS SDK: ${SDK_VERSION}" + [[ "${SDK_VERSION%%.*}" -ge 26 ]] + + - name: Install XcodeGen (retry on transient Homebrew failures) + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + shell: bash + run: | + for attempt in 1 2 3; do + if brew install xcodegen; then + exit 0 + fi + if [[ "$attempt" -lt 3 ]]; then + sleep $((attempt * 10)) + fi + done + echo "Failed to install xcodegen after 3 attempts." + exit 1 + + - name: Generate Xcode project + working-directory: ios + run: xcodegen generate + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: ios + + - name: Install Apple certificate and provisioning profile + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} + run: | + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -hex 16) + + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH + + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH $(security list-keychains -d user | tr -d '"') + + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + PP_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" /dev/stdin <<< "$(/usr/bin/security cms -D -i $PP_PATH)") + cp "$PP_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/${PP_UUID}.mobileprovision + + - name: Fastlane release (preflight, gym, deliver) + working-directory: ios + env: + APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }} + APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} + APPSTORE_API_PRIVATE_KEY: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} + RELEASE_OWNER: ${{ github.actor }} + # Default dry-run for App Store: upload binary + metadata, do not submit for review. + # Set repository variable IOS_SUBMIT_FOR_REVIEW to the literal "1" to enable submit. + SUBMIT_FOR_REVIEW: ${{ vars.IOS_SUBMIT_FOR_REVIEW }} + AUTOMATIC_RELEASE: ${{ vars.IOS_AUTOMATIC_RELEASE }} + run: bundle exec fastlane release + + - name: Clean up keychain and provisioning profile + if: always() + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true + rm -f $RUNNER_TEMP/build_certificate.p12 2>/dev/null || true + rm -f $RUNNER_TEMP/build_pp.mobileprovision 2>/dev/null || true diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml index 6ba52522..d0738ba0 100644 --- a/.github/workflows/ios-testflight.yml +++ b/.github/workflows/ios-testflight.yml @@ -3,7 +3,9 @@ name: Build & Upload to TestFlight on: push: tags: - - 'ios-v*' + # TestFlight binary uploads use extended tags, e.g. ios-v1.0.3-build10. + # Semver-only tags (ios-v1.0.0) trigger App Store release — see ios-app-store-release.yml. + - 'ios-v*-build*' permissions: contents: read diff --git a/.gitignore b/.gitignore index ecb37e7b..78dfb7d8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ /artifacts/e2e/ /artifacts/ios-app-store-dry-run/ +# Bundler (ios/Gemfile — Fastlane) +/ios/vendor/bundle/ + # next.js /.next/ /out/ diff --git a/README.md b/README.md index 4ec5a9f5..d3f74f46 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ All iOS release and App Store submission work must reference: - [iOS release / TestFlight runbook](ios/RELEASING.md) - [iOS App Store submission and delegation runbook](docs/operations/ios-app-store-submission.md) -Use these files as the source of truth when creating GitHub issues, delegating release tasks, or asking AI coding agents to prepare release work. The TestFlight upload path is automated through GitHub Actions; the App Store submission runbook identifies which App Store Connect steps are automatable through Apple's API and which still require an Account Holder/Admin or human release decision. +Use these files as the source of truth when creating GitHub issues, delegating release tasks, or asking AI coding agents to prepare release work. **TestFlight** uploads run on `ios-v*-build*` tags via [`.github/workflows/ios-testflight.yml`](.github/workflows/ios-testflight.yml). **App Store** metadata + binary automation runs on strict `ios-vMAJOR.MINOR.PATCH` tags (matching `MARKETING_VERSION` in `ios/project.yml`) via [`.github/workflows/ios-app-store-release.yml`](.github/workflows/ios-app-store-release.yml) and Fastlane under `ios/fastlane/`. The App Store submission runbook identifies which App Store Connect steps are automatable through Apple's API and which still require an Account Holder/Admin or human release decision. For automated vs manual steps, see [docs/operations/automation-evidence.md](docs/operations/automation-evidence.md). Before the next live App Store submission, run the AI-assisted dry run and save its generated evidence: @@ -224,7 +224,7 @@ Before the next live App Store submission, run the AI-assisted dry run and save npm run ios:app-store:dry-run ``` -The dry run reads the canonical runbooks, validates the current iOS version/build/tag plan, checks the TestFlight workflow's required checklist and secret references, maps every issue #242 checklist item, and writes `artifacts/ios-app-store-dry-run/summary.json`, `summary.md`, and `automation.log`. Set `APPSTORE_APP_ID`, `APPSTORE_API_KEY_ID`, `APPSTORE_API_ISSUER_ID`, and `APPSTORE_API_PRIVATE_KEY` to include live App Store Connect read checks; add `-- --require-live` when a release owner wants missing live ASC access to fail the dry run. +The dry run reads the canonical runbooks, validates the current iOS version/build/tag plan, checks both iOS workflows' required checklist and secret references, maps every issue #242 checklist item, and writes `artifacts/ios-app-store-dry-run/summary.json`, `summary.md`, and `automation.log`. Set `APPSTORE_APP_ID`, `APPSTORE_API_KEY_ID`, `APPSTORE_API_ISSUER_ID`, and `APPSTORE_API_PRIVATE_KEY` to include live App Store Connect read checks; add `-- --require-live` when a release owner wants missing live ASC access to fail the dry run. ### Neon (database) @@ -274,7 +274,7 @@ What should be green before merge: Notes: -- The GitHub Actions workflow `Build & Upload to TestFlight` is **release-only** (tag trigger `ios-v*`) and is not a pull-request merge gate. +- The GitHub Actions workflows **Build & Upload to TestFlight** (`ios-v*-build*` tags) and **iOS App Store release (Fastlane)** (`ios-vMAJOR.MINOR.PATCH` tags) are **release-only** and are not pull-request merge gates. - For strict enforcement, add these check names under GitHub branch protection required checks for `main`. ## Project structure diff --git a/docs/operations/automation-evidence.md b/docs/operations/automation-evidence.md new file mode 100644 index 00000000..5bb10587 --- /dev/null +++ b/docs/operations/automation-evidence.md @@ -0,0 +1,53 @@ +# iOS App Store automation — automated vs manual (issue #242) + +This log complements the machine-readable output from `npm run ios:app-store:dry-run` (`artifacts/ios-app-store-dry-run/summary.json`). It records what the repository automates today versus steps that remain human-owned. + +## Preconditions (human, one-time or rare) + +| Step | Owner | Notes | +|------|--------|------| +| Create App Store Connect API key (`.p8`), **never commit the key** | Account Holder / Admin | Store in 1Password; add `APPSTORE_API_KEY_ID`, `APPSTORE_API_ISSUER_ID`, `APPSTORE_API_PRIVATE_KEY` as GitHub Actions **secrets** only. | +| Confirm `gh secret list` shows required names (values are never visible) | Release owner | Cannot be automated inside CI without elevated token scope. | +| Distribution certificate + provisioning profile secrets | Engineering | `BUILD_CERTIFICATE_BASE64`, `P12_PASSWORD`, `BUILD_PROVISION_PROFILE_BASE64`. | +| Apple Agreements, Tax, Banking | Account Holder / Admin | Not reliably exposed via public API; uploads can fail until clear. | + +## Automated in CI (after secrets exist) + +| Capability | Mechanism | Workflow | +|------------|-----------|----------| +| Release-config UI smoke | `npm run e2e:ios:smoke` | TestFlight + App Store release | +| Release-readiness checklist gate | `grep` on `PARITY_CHECKLIST.md` / `QA_CHECKLIST.md` | Both | +| Machine-readable preflight | `npm run ios:app-store:dry-run` | Both | +| Archive + upload binary to App Store Connect / TestFlight | `xcodebuild -exportArchive` with API key | TestFlight (`ios-testflight.yml`) | +| Preflight + archive + metadata + binary via Fastlane | `bundle exec fastlane release` (`gym`, `deliver`) | App Store release (`ios-app-store-release.yml`) | + +## Automated locally or on a Mac runner (Fastlane) + +| Lane (`ios/` directory) | Purpose | +|-------------------------|---------| +| `bundle exec fastlane preflight` | Runs the Node dry-run (same as `npm run ios:app-store:dry-run` from repo root). | +| `bundle exec fastlane build` | `gym` → IPA (no upload). | +| `bundle exec fastlane beta` | `gym` + `pilot` (TestFlight upload). | +| `bundle exec fastlane metadata_only` | `deliver` metadata only (no binary). | +| `bundle exec fastlane release` | Preflight (unless `SKIP_PREFLIGHT=1`) + `gym` + `deliver` (binary + metadata). | + +**Submit for review:** CI defaults to **not** submitting (`SUBMIT_FOR_REVIEW` unset). Set the repository variable `IOS_SUBMIT_FOR_REVIEW` to `1` when a release owner explicitly enables programmatic submission. Optional `IOS_AUTOMATIC_RELEASE=1` maps to Deliver’s automatic release after approval. + +## Explicit human gates (not fully automatable) + +| Gate | Reason | +|------|--------| +| Release timing (manual vs automatic vs phased) after approval | Business / product decision. | +| Age rating questionnaire changes | Compliance accountability. | +| Screenshot / app preview assets | Require approved creative; automation hooks in #325. | +| Demo credentials in private review fields | Human-provided secrets. | +| Physical-device account-deletion screen recording for review | Asset capture + verification. | +| Rejection triage and engineering issue breakdown | Human judgment before filing work. | +| Closing the ops tracking issue | After final outcome is known. | + +## Evidence for a given run + +- **Dry run / preflight:** `artifacts/ios-app-store-dry-run/` (from `npm run ios:app-store:dry-run` or the `preflight` lane). +- **CI:** GitHub Actions run logs and uploaded artifacts for `ios-testflight.yml` and `ios-app-store-release.yml`. + +Last updated: 2026-05-01 (issue #242 harness). diff --git a/docs/operations/ios-app-store-submission.md b/docs/operations/ios-app-store-submission.md index af28b8bf..3a0afde2 100644 --- a/docs/operations/ios-app-store-submission.md +++ b/docs/operations/ios-app-store-submission.md @@ -8,7 +8,7 @@ Use this runbook after an iOS build has been uploaded to TestFlight and before c - **Product / App Store** owns screenshots, copy, age rating, review notes, the account-deletion recording, demo credentials, and release timing. - **Account Holder / Admin** owns Apple Developer and App Store Connect agreements, tax, and banking. -Record submission status, reviewer notes, build strings, and final outcome in ops tracker #103. Use `ios/RELEASING.md` for tag format, build bumps, and CI upload details. +Record submission status, reviewer notes, build strings, and final outcome in ops tracker #103. Use `ios/RELEASING.md` for tag format, build bumps, and CI upload details. Binary uploads use [`.github/workflows/ios-testflight.yml`](../../.github/workflows/ios-testflight.yml) (`ios-v*-build*` tags). App Store metadata + binary automation uses [`.github/workflows/ios-app-store-release.yml`](../../.github/workflows/ios-app-store-release.yml) (strict `ios-vMAJOR.MINOR.PATCH` tags) and Fastlane under `ios/fastlane/`. ## AI-assisted dry run @@ -18,7 +18,7 @@ Before a live submission, generate the issue #242 evidence package: npm run ios:app-store:dry-run ``` -The command reads `README.md`, `ios/RELEASING.md`, this runbook, `.github/workflows/ios-testflight.yml`, `ios/PARITY_CHECKLIST.md`, `ios/QA_CHECKLIST.md`, and `ios/project.yml`. It writes `artifacts/ios-app-store-dry-run/summary.json`, `summary.md`, and `automation.log` with: +The command reads `README.md`, `ios/RELEASING.md`, this runbook, `.github/workflows/ios-testflight.yml`, `.github/workflows/ios-app-store-release.yml`, `ios/PARITY_CHECKLIST.md`, `ios/QA_CHECKLIST.md`, and `ios/project.yml`. It writes `artifacts/ios-app-store-dry-run/summary.json`, `summary.md`, and `automation.log` with: - the intended `ios-v*` tag, marketing version, build number, bundle ID, workflow trigger, and release artifact placeholders; - a 38-item issue #242 checklist map across phases A-F, App Store Connect API readiness, submission execution, review follow-up, proof, and definition-of-done items; @@ -119,6 +119,6 @@ Account deletion path for Guideline 5.1.1(v): - Ops / submission log: #103 - Submit / archive checklist (historical): #37 -- CI workflow: `.github/workflows/ios-testflight.yml` +- CI workflows: `.github/workflows/ios-testflight.yml`, `.github/workflows/ios-app-store-release.yml` - Release / tag / build docs: `ios/RELEASING.md` - App Store Connect submission URL pattern: `https://appstoreconnect.apple.com/apps//distribution/reviewsubmissions/details/` diff --git a/ios/Gemfile b/ios/Gemfile index 81a4d85c..701a0377 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" gem "fastlane", "~> 2.227" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock new file mode 100644 index 00000000..cd7bb191 --- /dev/null +++ b/ios/Gemfile.lock @@ -0,0 +1,336 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1243.0) + aws-sdk-core (3.246.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.124.0) + aws-sdk-core (~> 3, >= 3.244.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.220.0) + aws-sdk-core (~> 3, >= 3.244.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.5) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.5) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.4) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.1) + fastlane (2.233.1) + CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2.0) + benchmark (>= 0.1.0) + bundler (>= 1.17.3, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, <= 2.1.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) + naturally (~> 2.2) + nkf (~> 0.2.0) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.99.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) + mini_mime (~> 1.0) + mutex_m + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + google-apis-iamcredentials_v1 (0.27.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.62.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.1.1) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.6.0) + google-cloud-storage (1.59.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + googleauth (1.11.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + jmespath (1.6.2) + json (2.19.4) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.20.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + public_suffix (7.0.5) + rake (13.4.2) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.4.1) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + x86_64-linux-gnu + +DEPENDENCIES + fastlane (~> 2.227) + +CHECKSUMS + CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 + abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 + atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f + aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b + aws-partitions (1.1243.0) sha256=2751071202381b7bb115a1f592d699c73bb5ac9ad4e5aece6a14b1dabc265c43 + aws-sdk-core (3.246.0) sha256=393864ec8948560e69fcccc2e4d256b40c7028eb98930608dd295279e3c4ddcc + aws-sdk-kms (1.124.0) sha256=40d00ab706d7e49fd620270bd0dcb546f266295abdd49b54fec2611e2a41f37c + aws-sdk-s3 (1.220.0) sha256=237fda5e6ac7ecdd9c848e27187bfdc370edad5c5a141aeec389fb450fa28c7c + aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 + babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 + base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507 + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 + claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e + colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c + colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a + commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9 + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 + digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 + domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 + dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 + emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b + excon (0.112.0) sha256=daf9ac3a4c2fc9aa48383a33da77ecb44fa395111e973084d5c52f6f214ae0f0 + faraday (1.10.5) sha256=b144f1d2b045652fa820b5f532723e1643cc28b93dae911d784e5c5f88e8f6ed + faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb + faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689 + faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750 + faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940 + faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 + faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682 + faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335 + faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7 + faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0 + faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa + faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9 + fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 + fastlane (2.233.1) sha256=38b19c43f3d9649eacbd870ae8a59118fbdfcabcbb3b4571ae26ae291d92f694 + fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 + gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 + google-apis-androidpublisher_v3 (0.99.0) sha256=a0452fdd99cb7672cc95cac07429305f8aaee54c22e4875224cf675b1ab59729 + google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee + google-apis-iamcredentials_v1 (0.27.0) sha256=9289f29968610754ef11d98b9ec627f0153f3e2616fef839aef096de529f6d1e + google-apis-playcustomapp_v1 (0.17.0) sha256=d5bc90b705f3f862bab4998086449b0abe704ee1685a84821daa90ca7fa95a78 + google-apis-storage_v1 (0.62.0) sha256=f62467c36df53287fb0252ebb4da85f9e25d7b4c5809d045c2aab1fc307760c1 + google-cloud-core (1.8.0) sha256=e572edcbf189cfcab16590628a516cec3f4f63454b730e59f0b36575120281cf + google-cloud-env (2.1.1) sha256=cf4bb8c7d517ee1ea692baedf06e0b56ce68007549d8d5a66481aa9f97f46999 + google-cloud-errors (1.6.0) sha256=1da8476dd706ad04b9d32e3c4b90d07d3463b37d6407cb56d41342ea7647d0a1 + google-cloud-storage (1.59.0) sha256=b8c9a5661d775d65ccb279bb1d6be07fd8152576eb0146c2026bd023c4b186b9 + googleauth (1.11.2) sha256=7e6bacaeed7aea3dd66dcea985266839816af6633e9f5983c3c2e0e40a44731e + highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 + http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6 + httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8 + jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 + json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac + jwt (2.10.2) sha256=31e1ee46f7359883d5e622446969fe9c118c3da87a0b1dca765ce269c3a0c4f4 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + multi_json (1.20.1) sha256=2f3934e805cc45ef91b551a1f89d0e9191abd06a5e04a2ef09a6a036c452ca6d + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 + nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 + naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01 + nkf (0.2.0) sha256=fbc151bda025451f627fafdfcb3f4f13d0b22ae11f58c6d3a2939c76c5f5f126 + optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace + retriable (3.4.1) sha256=fb3f114b7d492121c158c01f3d5152b5a615c5b70d5877d0bc08c7ec3725c3bc + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 + ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef + rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 + security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + signet (0.21.0) sha256=d617e9fbf24928280d39dcfefba9a0372d1c38187ffffd0a9283957a10a8cd5b + simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b + terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea + terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91 + trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48 + tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 + tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 + uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc + unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a + word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 + xcodeproj (1.27.0) sha256=8cc7a73b4505c227deab044dce118ede787041c702bc47636856a2e566f854d3 + xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892 + xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93 + +BUNDLED WITH + 4.0.11 diff --git a/ios/RELEASING.md b/ios/RELEASING.md index bbbea276..f737503c 100644 --- a/ios/RELEASING.md +++ b/ios/RELEASING.md @@ -43,12 +43,12 @@ Before your first TestFlight release, configure a tester group and handle encryp git add ios/project.yml ios/PARITY_CHECKLIST.md ios/QA_CHECKLIST.md ios/RELEASING.md git commit -m "Finalize iOS 1.0.3 (build 10) release readiness" ``` -4. Tag and push: +4. Tag and push using the **TestFlight** tag pattern (`ios-v{version}-build{number}`): ```bash git tag ios-v1.0.3-build10 git push origin ios-v1.0.3-build10 ``` -5. The GitHub Actions workflow builds and uploads to TestFlight automatically. +5. The [Build & Upload to TestFlight](../.github/workflows/ios-testflight.yml) workflow (`ios-v*-build*` tags) builds and uploads to TestFlight automatically. 6. After Apple processes the build (~15 minutes), it appears in the TestFlight app. 7. Record the processed build number and processing timestamp in `ios/QA_CHECKLIST.md`. 8. Before App Store submission, run the automation dry run and save its artifact: @@ -62,17 +62,48 @@ Before your first TestFlight release, configure a tester group and handle encryp `APPSTORE_APP_ID` plus the App Store Connect API key environment variables and rerun with `-- --require-live`. +## App Store release (tag → Fastlane) + +When you are ready to push **metadata + binary** to App Store Connect for the version in `ios/project.yml` (typically after TestFlight validation): + +1. Ensure `MARKETING_VERSION` in `ios/project.yml` matches the storefront version you intend to ship. +2. Use a **strict semver tag** (no `-build` suffix): `ios-vMAJOR.MINOR.PATCH` must equal `MARKETING_VERSION`, e.g. `ios-v1.0.3`. +3. Push the tag: + ```bash + git tag ios-v1.0.3 + git push origin ios-v1.0.3 + ``` +4. [iOS App Store release (Fastlane)](../.github/workflows/ios-app-store-release.yml) runs: smoke tests, checklist gate, `npm run ios:app-store:dry-run`, then `bundle exec fastlane release` (preflight → `gym` → `deliver`). + +**Submit for review** is off by default in CI (binary and metadata still upload). To enable programmatic submission for a run, set the GitHub repository variable `IOS_SUBMIT_FOR_REVIEW` to `1`. Optional: `IOS_AUTOMATIC_RELEASE=1` for automatic release after approval. + +### Fastlane (local or debugging) + +Bundler + Fastlane live under `ios/`: + +```bash +cd ios +bundle install +bundle exec fastlane preflight # same as npm dry-run from repo root +bundle exec fastlane build # gym only +bundle exec fastlane metadata_only +bundle exec fastlane release # preflight + gym + deliver +``` + +Lanes are defined in [`fastlane/Fastfile`](./fastlane/Fastfile). App identifier and team are in [`fastlane/Appfile`](./fastlane/Appfile). Default `deliver` options are in [`fastlane/Deliverfile`](./fastlane/Deliverfile). **Localized copy** for `en-US` is under [`fastlane/metadata/en-US/`](./fastlane/metadata/en-US/) (canonical for `deliver`). + ## App Store submission handoff -After the intended TestFlight build is processed and valid, use the delegate-ready [iOS App Store submission runbook](../docs/operations/ios-app-store-submission.md) to complete App Store Connect setup, reviewer notes, submission, rejection handling, approval, and release tracking. +After the intended TestFlight build is processed and valid, use the delegate-ready [iOS App Store submission runbook](../docs/operations/ios-app-store-submission.md) to complete App Store Connect setup, reviewer notes, submission, rejection handling, approval, and release tracking. For what is automated vs manual in tooling, see [Automation evidence log](../docs/operations/automation-evidence.md). ## Version numbering - `MARKETING_VERSION` - user-facing version (e.g., `1.0.0`, `1.0.3`). - `CURRENT_PROJECT_VERSION` - build number; increment every upload (`1`, `2`, `3`, ...). -- **Git tag** must match `ios-v*` to trigger [`.github/workflows/ios-testflight.yml`](../.github/workflows/ios-testflight.yml). -- Tag value does not have to equal `MARKETING_VERSION`; uploaded app version is controlled by `ios/project.yml`. -- Re-uploads for same marketing version require a new tag and incremented build number, e.g. `ios-v-build`. +- **TestFlight:** push a tag matching `ios-v*-build*` (e.g. `ios-v1.0.3-build10`) to trigger [`.github/workflows/ios-testflight.yml`](../.github/workflows/ios-testflight.yml). +- **App Store automation:** push a strict semver tag `ios-vMAJOR.MINOR.PATCH` that **equals** `MARKETING_VERSION` to trigger [`.github/workflows/ios-app-store-release.yml`](../.github/workflows/ios-app-store-release.yml). +- Uploaded app version is controlled by `ios/project.yml`, not the tag string (except the App Store workflow enforces tag ↔ `MARKETING_VERSION` match). +- Re-uploads for the same marketing version require a new TestFlight tag and incremented build number, e.g. `ios-v-build`. ## Regenerate candidate App Store screenshots (Issue #325) @@ -126,7 +157,7 @@ Workflow [`.github/workflows/ios-screenshots.yml`](../.github/workflows/ios-scre ## App Store metadata and release notes checklist -Canonical App Store Connect copy (subtitle, description alignment, privacy URL) lives in [`ios/docs/app-store-metadata.md`](./docs/app-store-metadata.md). Reconcile App Store Connect with that file when preparing a submission. +Canonical **deliverable** App Store Connect strings for English (U.S.) live in [`fastlane/metadata/en-US/`](./fastlane/metadata/en-US/). Positioning context and parity notes remain in [`ios/docs/app-store-metadata.md`](./docs/app-store-metadata.md). Reconcile App Store Connect with the `fastlane/metadata` files when preparing a submission. Before "Add for Review," verify the following in App Store Connect: diff --git a/ios/docs/app-store-metadata.md b/ios/docs/app-store-metadata.md index 85532e75..45e1e017 100644 --- a/ios/docs/app-store-metadata.md +++ b/ios/docs/app-store-metadata.md @@ -1,6 +1,6 @@ # App Store Connect — canonical marketing copy -This file is the **repository source of truth** for App Store Connect text that should match product positioning. Paste or reconcile these strings in App Store Connect when creating or editing a version. If the live storefront differs, update either ASC or this doc and note the change in release notes or the PR that touched copy. +This file documents **positioning and parity** for App Store Connect. The **machine-deliverable** strings (name, subtitle, description, keywords, release notes, URLs, review notes) live under [`../fastlane/metadata/`](../fastlane/metadata/) as `deliver` locale files (`en-US` is canonical). Reconcile App Store Connect with those files when preparing a submission. If the live storefront differs, update ASC or the `fastlane/metadata` files (and this doc if narrative context changes) and note the change in release notes or the PR that touched copy. ## Manual fix (issue #195) diff --git a/ios/fastlane/Appfile b/ios/fastlane/Appfile index fc91755d..60e833b3 100644 --- a/ios/fastlane/Appfile +++ b/ios/fastlane/Appfile @@ -1,2 +1,7 @@ +# frozen_string_literal: true + +# App Store Connect app identifier (matches ios/project.yml). app_identifier("com.brettonauerbach.stillpoint") -# Optional: FASTLANE_USER for Apple ID flows not used by API-key deliver. + +# Apple Developer Team ID (matches ios/project.yml DEVELOPMENT_TEAM). +team_id("T5UU4BP6AV") diff --git a/ios/fastlane/Deliverfile b/ios/fastlane/Deliverfile new file mode 100644 index 00000000..38a75ca2 --- /dev/null +++ b/ios/fastlane/Deliverfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Localized metadata lives under fastlane/metadata// (en-US canonical). +# Screenshots: issue #325 / separate workflow; not required for metadata-only lanes. + +# Default: do not auto-submit; CI passes submit_for_review: true when releasing. +submit_for_review(false) +automatic_release(false) +skip_screenshots(true) +skip_metadata(false) +precheck_include_in_app_purchases(false) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index a13bd776..39de4401 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -1,7 +1,18 @@ # frozen_string_literal: true +require "shellwords" + default_platform(:ios) +def stillpoint_api_key + app_store_connect_api_key( + key_id: ENV.fetch("APPSTORE_API_KEY_ID"), + issuer_id: ENV.fetch("APPSTORE_API_ISSUER_ID"), + key_content: ENV.fetch("APPSTORE_API_PRIVATE_KEY").gsub("\\n", "\n"), + in_house: false + ) +end + platform :ios do desc "Regenerate App Store screenshot candidates (15–20 per device via SnapshotTests)" lane :screenshots do @@ -91,8 +102,62 @@ platform :ios do ) end - desc "Alias for upload_app_store_screenshots" + desc "Machine-readable preflight (npm dry-run). Invoke from ios/ (see RELEASING.md)." + lane :preflight do + release_owner = ENV["RELEASE_OWNER"] || "fastlane" + repo_root = File.expand_path("../..", __dir__) + sh("cd #{repo_root.shellescape} && npm run ios:app-store:dry-run -- --release-owner #{release_owner.shellescape}") + end + + desc "Build Release IPA (no upload). Requires Xcode project generated (xcodegen)." + lane :build do + gym( + project: "StillPoint.xcodeproj", + scheme: "StillPoint", + configuration: "Release", + export_options: "ExportOptions.plist", + output_directory: ENV["GYM_OUTPUT_DIRECTORY"] || "build", + output_name: "StillPoint.ipa", + clean: !ENV["GYM_NO_CLEAN"] + ) + end + + desc "Build and upload to TestFlight (pilot)." + lane :beta do + stillpoint_api_key + build + pilot( + api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], + ipa: lane_context[SharedValues::IPA_OUTPUT_PATH], + skip_waiting_for_build_processing: true + ) + end + + desc "Upload local metadata to App Store Connect (no IPA)." + lane :metadata_only do + stillpoint_api_key + deliver( + api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], + force: true, + skip_binary_upload: true, + submit_for_review: false, + run_precheck_before_submit: false + ) + end + + desc "Preflight → build → deliver metadata + IPA. Set SUBMIT_FOR_REVIEW=1 to submit for review after upload." lane :release do - upload_app_store_screenshots + preflight unless ENV["SKIP_PREFLIGHT"] == "1" + stillpoint_api_key + build + deliver( + api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], + ipa: lane_context[SharedValues::IPA_OUTPUT_PATH], + force: true, + skip_binary_upload: false, + submit_for_review: ENV["SUBMIT_FOR_REVIEW"] == "1", + automatic_release: ENV["AUTOMATIC_RELEASE"] == "1", + run_precheck_before_submit: false + ) end end diff --git a/ios/fastlane/metadata/en-US/description.txt b/ios/fastlane/metadata/en-US/description.txt new file mode 100644 index 00000000..eaafc255 --- /dev/null +++ b/ios/fastlane/metadata/en-US/description.txt @@ -0,0 +1,3 @@ +Still Point is a meditation timer for people struggling to get started and stay consistent: it begins at one minute, adds ten seconds per day, and shows time as boxes that fill up as you go to make sitting still easier. + +Build steadier focus with a guided one-minute meditation practice designed for real, distractible days. diff --git a/ios/fastlane/metadata/en-US/keywords.txt b/ios/fastlane/metadata/en-US/keywords.txt new file mode 100644 index 00000000..f083900f --- /dev/null +++ b/ios/fastlane/metadata/en-US/keywords.txt @@ -0,0 +1 @@ +meditation,focus,timer,mindfulness,breathing,habit,daily,stillness diff --git a/ios/fastlane/metadata/en-US/marketing_url.txt b/ios/fastlane/metadata/en-US/marketing_url.txt new file mode 100644 index 00000000..a5d1994b --- /dev/null +++ b/ios/fastlane/metadata/en-US/marketing_url.txt @@ -0,0 +1 @@ +https://still-point.me diff --git a/ios/fastlane/metadata/en-US/name.txt b/ios/fastlane/metadata/en-US/name.txt new file mode 100644 index 00000000..22b3bc3b --- /dev/null +++ b/ios/fastlane/metadata/en-US/name.txt @@ -0,0 +1 @@ +Still Point diff --git a/ios/fastlane/metadata/en-US/privacy_url.txt b/ios/fastlane/metadata/en-US/privacy_url.txt new file mode 100644 index 00000000..d2d2c7b8 --- /dev/null +++ b/ios/fastlane/metadata/en-US/privacy_url.txt @@ -0,0 +1 @@ +https://still-point.me/privacy diff --git a/ios/fastlane/metadata/en-US/release_notes.txt b/ios/fastlane/metadata/en-US/release_notes.txt new file mode 100644 index 00000000..a71a25b5 --- /dev/null +++ b/ios/fastlane/metadata/en-US/release_notes.txt @@ -0,0 +1,5 @@ +Still Point improves session reliability and social sitting stability. + +- Refines buddy session join/start/leave flows for more reliable partner synchronization. +- Improves solo session save behavior and completion-note consistency. +- Includes account and visibility settings polish ahead of App Store review. diff --git a/ios/fastlane/metadata/en-US/subtitle.txt b/ios/fastlane/metadata/en-US/subtitle.txt new file mode 100644 index 00000000..b88ff176 --- /dev/null +++ b/ios/fastlane/metadata/en-US/subtitle.txt @@ -0,0 +1 @@ +Meditate one minute at a time diff --git a/ios/fastlane/metadata/en-US/support_url.txt b/ios/fastlane/metadata/en-US/support_url.txt new file mode 100644 index 00000000..a5d1994b --- /dev/null +++ b/ios/fastlane/metadata/en-US/support_url.txt @@ -0,0 +1 @@ +https://still-point.me diff --git a/ios/fastlane/metadata/review_information/notes.txt b/ios/fastlane/metadata/review_information/notes.txt new file mode 100644 index 00000000..98bd2a1a --- /dev/null +++ b/ios/fastlane/metadata/review_information/notes.txt @@ -0,0 +1,10 @@ +Thank you for reviewing Still Point. + +Sign in using the demo credentials in the private review fields, or create a new account from the sign-in screen. + +Account deletion path for Guideline 5.1.1(v): +1. Open Settings. +2. Tap Delete Account. +3. Tap Continue on the first prompt. +4. Tap Delete Account on the final confirmation alert. +5. Confirm the app returns to the signed-out screen. diff --git a/scripts/ios-app-store-dry-run.mjs b/scripts/ios-app-store-dry-run.mjs index 7d820a5a..4ab6ba34 100644 --- a/scripts/ios-app-store-dry-run.mjs +++ b/scripts/ios-app-store-dry-run.mjs @@ -18,7 +18,10 @@ const requiredWorkflowSecrets = [ "APPSTORE_API_PRIVATE_KEY", ]; -const workflowUrl = "https://github.com/auerbachb/still-point/actions/workflows/ios-testflight.yml"; +const testflightWorkflowUrl = + "https://github.com/auerbachb/still-point/actions/workflows/ios-testflight.yml"; +const appStoreReleaseWorkflowUrl = + "https://github.com/auerbachb/still-point/actions/workflows/ios-app-store-release.yml"; const appStoreConnectBaseUrl = "https://appstoreconnect.apple.com/apps"; const appStoreApiMinimumRole = "App Manager or Admin for app/version/build/metadata/screenshot/submission operations; Account Holder/Admin for agreements, tax, banking, and some compliance decisions."; @@ -27,11 +30,11 @@ const issueChecklist = [ ["A1", "Read README Release operations and confirm both canonical iOS runbooks are referenced."], ["A2", "Read ios/RELEASING.md and confirm MARKETING_VERSION, CURRENT_PROJECT_VERSION, and ios-v* tag plan."], ["A3", "Read docs/operations/ios-app-store-submission.md and map every Phase A-F item to automation, AI-assisted verification, or human decision."], - ["A4", "Read .github/workflows/ios-testflight.yml and confirm the intended tag triggers TestFlight upload."], + ["A4", "Read .github/workflows/ios-testflight.yml and ios-app-store-release.yml and confirm tag triggers (TestFlight vs App Store release)."], ["A5", "Confirm ios/PARITY_CHECKLIST.md and ios/QA_CHECKLIST.md satisfy workflow-required checked items before tagging."], ["B1", "Confirm an App Store Connect API key is available with minimum roles for version/build/metadata/screenshot/submission operations."], ["B2", "Confirm GitHub secrets used by the TestFlight workflow are present."], - ["B3", "Create or identify a script/tooling approach for App Store Connect API calls instead of manual website entry where possible."], + ["B3", "Create or identify a script/tooling approach for App Store Connect API calls instead of manual website entry where possible (Fastlane under ios/fastlane/)."], ["B4", "API-check Apple agreements/tax/banking if possible; otherwise record Account Holder/Admin confirmation."], ["B5", "API-query the latest processed TestFlight build for the intended version/build and fail fast on missing, processing, invalid, or non-incremented build."], ["C1", "Create or open the target App Store version programmatically."], @@ -274,7 +277,7 @@ function classifyIssueChecklist({ project, asc, trackingIssue, requireLive }) { reportSecretStatus.status, reportSecretStatus.message, ], - B3: ["pass", "This script is the dry-run tooling approach and records ASC endpoints for build/version validation."], + B3: ["pass", "Fastlane under ios/fastlane/ (gym, pilot, deliver) plus this dry-run script cover ASC API automation paths."], B4: ["warning", "Apple agreements/tax/banking are not exposed by the public ASC API; Account Holder/Admin confirmation remains required."], B5: !asc.live ? [offlineStatus, asc.reason] @@ -326,7 +329,8 @@ Generated: ${report.generatedAt} - Bundle ID: ${report.project.bundleId} - Release owner: ${report.releaseOwner} - Submission URL: ${report.submissionUrl} -- Workflow URL: ${report.preflightSummary.workflowUrl} +- TestFlight workflow: ${report.preflightSummary.testflightWorkflowUrl} +- App Store release workflow: ${report.preflightSummary.appStoreReleaseWorkflowUrl} - TestFlight build status: ${report.preflightSummary.testFlightBuildStatus} - App Store version URL: ${report.preflightSummary.appStoreVersionUrl} - Attached build: ${report.preflightSummary.attachedBuild} @@ -370,6 +374,7 @@ async function main() { const releasing = read("ios/RELEASING.md"); const submissionRunbook = read("docs/operations/ios-app-store-submission.md"); const workflow = read(".github/workflows/ios-testflight.yml"); + const releaseWorkflow = read(".github/workflows/ios-app-store-release.yml"); const parity = read("ios/PARITY_CHECKLIST.md"); const qa = read("ios/QA_CHECKLIST.md"); const projectYml = read("ios/project.yml"); @@ -389,9 +394,20 @@ async function main() { ? pass("A3", "Submission runbook covers phases A-F.", { phases: phaseMatches }) : fail("A3", "Submission runbook does not cover every phase A-F.", { phases: phaseMatches }); - workflow.includes("tags:") && workflow.includes("'ios-v*'") - ? pass("A4", "TestFlight workflow is triggered by ios-v* tags.", { intendedTag }) - : fail("A4", "TestFlight workflow is missing ios-v* tag trigger."); + const testflightTagOk = + workflow.includes("tags:") && workflow.includes("'ios-v*-build*'"); + const releaseTagOk = + releaseWorkflow.includes("tags:") && releaseWorkflow.includes("'ios-v[0-9]+.[0-9]+.[0-9]+'"); + testflightTagOk && releaseTagOk + ? pass("A4", "TestFlight uses ios-v*-build* tags; App Store release uses strict ios-vMAJOR.MINOR.PATCH tags.", { + intendedTag, + testflightTagPattern: "ios-v*-build*", + appStoreReleaseTagPattern: "ios-vMAJOR.MINOR.PATCH", + }) + : fail("A4", "Workflow tag triggers are misconfigured for TestFlight vs App Store release.", { + testflightTagOk, + releaseTagOk, + }); const parityOk = requiredChecked(parity, "Feature parity checklist between iOS and web is completed.") && requiredChecked(parity, "All critical parity gaps are fixed or explicitly deferred with owner/date."); @@ -401,32 +417,34 @@ async function main() { ? pass("A5", "Workflow-required release-readiness checklist items are checked.") : fail("A5", "One or more workflow-required release-readiness checklist items are missing."); - const workflowSecretRefs = requiredWorkflowSecrets.filter((secret) => workflow.includes(`secrets.${secret}`)); + const workflowSecretRefs = requiredWorkflowSecrets.filter( + (secret) => workflow.includes(`secrets.${secret}`) && releaseWorkflow.includes(`secrets.${secret}`), + ); const githubSecrets = listGithubSecrets(); const missingWorkflowRefs = requiredWorkflowSecrets.filter((secret) => !workflowSecretRefs.includes(secret)); const missingGithubSecrets = githubSecrets.available ? requiredWorkflowSecrets.filter((secret) => !githubSecrets.names.includes(secret)) : []; if (missingWorkflowRefs.length > 0) { - fail("B2", "TestFlight workflow is missing required secret references.", { + fail("B2", "One or both iOS workflows are missing required secret references.", { requiredWorkflowSecrets, workflowSecretRefs, missingWorkflowRefs, }); reportSecretStatus = { status: "fail", - message: `Workflow is missing required secret references: ${missingWorkflowRefs.join(", ")}.`, + message: `Workflow(s) missing required secret references: ${missingWorkflowRefs.join(", ")}.`, }; } else if (githubSecrets.available && missingGithubSecrets.length === 0) { - pass("B2", "TestFlight workflow references every required secret and GitHub reports all required secret names.", { + pass("B2", "Both iOS workflows reference every required secret and GitHub reports all required secret names.", { requiredWorkflowSecrets, }); reportSecretStatus = { status: "pass", - message: "TestFlight workflow references every required secret and GitHub reports all required secret names.", + message: "Both iOS workflows reference every required secret and GitHub reports all required secret names.", }; } else if (githubSecrets.available) { - warn("B2", "TestFlight workflow references every required secret, but GitHub is missing one or more required secret names.", { + warn("B2", "Both iOS workflows reference every required secret, but GitHub is missing one or more required secret names.", { requiredWorkflowSecrets, missingGithubSecrets, }); @@ -435,13 +453,13 @@ async function main() { message: `GitHub secret names missing: ${missingGithubSecrets.join(", ")}.`, }; } else { - warn("B2", "TestFlight workflow references every required secret, but repository secret visibility could not be confirmed.", { + warn("B2", "Both iOS workflows reference every required secret, but repository secret visibility could not be confirmed.", { requiredWorkflowSecrets, reason: githubSecrets.reason, }); reportSecretStatus = { status: "warning", - message: `Workflow references required secrets; GitHub secret list unavailable: ${githubSecrets.reason}.`, + message: `Workflows reference required secrets; GitHub secret list unavailable: ${githubSecrets.reason}.`, }; } @@ -514,10 +532,17 @@ async function main() { intendedTag, releaseOwner: args.releaseOwner ?? "UNSET_RELEASE_OWNER", submissionUrl: args.submissionUrl ?? "UNSET_SUBMISSION_URL", - workflow: { - path: ".github/workflows/ios-testflight.yml", - tagTrigger: "ios-v*", - url: workflowUrl, + workflows: { + testflight: { + path: ".github/workflows/ios-testflight.yml", + tagTrigger: "ios-v*-build*", + url: testflightWorkflowUrl, + }, + appStoreRelease: { + path: ".github/workflows/ios-app-store-release.yml", + tagTrigger: "ios-vMAJOR.MINOR.PATCH", + url: appStoreReleaseWorkflowUrl, + }, requiredSecrets: requiredWorkflowSecrets, }, asc, @@ -530,7 +555,8 @@ async function main() { intendedTag, version: project.marketingVersion, build: project.buildNumber, - workflowUrl, + testflightWorkflowUrl, + appStoreReleaseWorkflowUrl, appStoreApiMinimumRole, testFlightBuildStatus: asc.live ? asc.intendedBuild?.processingState ?? "intended build not found" From 58c3dadf7b8eccb60c1fcda0061b063780f76d30 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 19:34:43 +0000 Subject: [PATCH 02/10] Wire Fastlane to APP_STORE_CONNECT_* env and skip ASC submission in CI - Read API key from APP_STORE_CONNECT_API_KEY_* (mapped from existing APPSTORE_* secrets) with fallback to APPSTORE_* for local runs. - Set DELIVER_SKIP_SUBMISSION=1 in ios-app-store-release workflow so deliver uploads only until #296 enables submission. - Document secrets as already configured; align automation-evidence. Co-authored-by: Bretton Auerbach --- .github/workflows/ios-app-store-release.yml | 11 +++++---- docs/operations/automation-evidence.md | 6 ++--- ios/RELEASING.md | 24 +++++++++++-------- ios/fastlane/Fastfile | 26 +++++++++++++++++---- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml index 5bd913da..2a88d9a6 100644 --- a/.github/workflows/ios-app-store-release.yml +++ b/.github/workflows/ios-app-store-release.yml @@ -207,14 +207,17 @@ jobs: - name: Fastlane release (preflight, gym, deliver) working-directory: ios env: + # Map existing GitHub Actions secrets → Fastlane standard env (spaceship / deliver). + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }} + APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} + APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} + # Same secrets (explicit refs keep ios:app-store:dry-run B2 checks satisfied). APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }} APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} APPSTORE_API_PRIVATE_KEY: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} RELEASE_OWNER: ${{ github.actor }} - # Default dry-run for App Store: upload binary + metadata, do not submit for review. - # Set repository variable IOS_SUBMIT_FOR_REVIEW to the literal "1" to enable submit. - SUBMIT_FOR_REVIEW: ${{ vars.IOS_SUBMIT_FOR_REVIEW }} - AUTOMATIC_RELEASE: ${{ vars.IOS_AUTOMATIC_RELEASE }} + # Harness default: upload binary + metadata only — no Submit for Review (#296 enables submission). + DELIVER_SKIP_SUBMISSION: "1" run: bundle exec fastlane release - name: Clean up keychain and provisioning profile diff --git a/docs/operations/automation-evidence.md b/docs/operations/automation-evidence.md index 5bb10587..6b663778 100644 --- a/docs/operations/automation-evidence.md +++ b/docs/operations/automation-evidence.md @@ -6,7 +6,7 @@ This log complements the machine-readable output from `npm run ios:app-store:dry | Step | Owner | Notes | |------|--------|------| -| Create App Store Connect API key (`.p8`), **never commit the key** | Account Holder / Admin | Store in 1Password; add `APPSTORE_API_KEY_ID`, `APPSTORE_API_ISSUER_ID`, `APPSTORE_API_PRIVATE_KEY` as GitHub Actions **secrets** only. | +| Create App Store Connect API key (`.p8`), **never commit the key** | Account Holder / Admin | One-time setup; this repo already stores `APPSTORE_API_*` in GitHub Actions secrets (rotate in ASC + GitHub if the key is revoked). | | Confirm `gh secret list` shows required names (values are never visible) | Release owner | Cannot be automated inside CI without elevated token scope. | | Distribution certificate + provisioning profile secrets | Engineering | `BUILD_CERTIFICATE_BASE64`, `P12_PASSWORD`, `BUILD_PROVISION_PROFILE_BASE64`. | | Apple Agreements, Tax, Banking | Account Holder / Admin | Not reliably exposed via public API; uploads can fail until clear. | @@ -19,7 +19,7 @@ This log complements the machine-readable output from `npm run ios:app-store:dry | Release-readiness checklist gate | `grep` on `PARITY_CHECKLIST.md` / `QA_CHECKLIST.md` | Both | | Machine-readable preflight | `npm run ios:app-store:dry-run` | Both | | Archive + upload binary to App Store Connect / TestFlight | `xcodebuild -exportArchive` with API key | TestFlight (`ios-testflight.yml`) | -| Preflight + archive + metadata + binary via Fastlane | `bundle exec fastlane release` (`gym`, `deliver`) | App Store release (`ios-app-store-release.yml`) | +| Preflight + archive + metadata + binary via Fastlane | `bundle exec fastlane release` (`gym`, `deliver`) | App Store release (`ios-app-store-release.yml`); `deliver` runs with `skip_submission` unless explicitly disabled for #296+ | ## Automated locally or on a Mac runner (Fastlane) @@ -31,7 +31,7 @@ This log complements the machine-readable output from `npm run ios:app-store:dry | `bundle exec fastlane metadata_only` | `deliver` metadata only (no binary). | | `bundle exec fastlane release` | Preflight (unless `SKIP_PREFLIGHT=1`) + `gym` + `deliver` (binary + metadata). | -**Submit for review:** CI defaults to **not** submitting (`SUBMIT_FOR_REVIEW` unset). Set the repository variable `IOS_SUBMIT_FOR_REVIEW` to `1` when a release owner explicitly enables programmatic submission. Optional `IOS_AUTOMATIC_RELEASE=1` maps to Deliver’s automatic release after approval. +**Submit for review:** In GitHub Actions, `ios-app-store-release.yml` sets `DELIVER_SKIP_SUBMISSION=1`, so `deliver` uploads the IPA and metadata but does not submit for review (issue #242 / harness default). For a future live submission (#296+), remove that guard in the workflow and set `DELIVER_SKIP_SUBMISSION=0` plus `SUBMIT_FOR_REVIEW=1` only when a release owner explicitly approves. Optional: `AUTOMATIC_RELEASE=1` after submission is enabled. ## Explicit human gates (not fully automatable) diff --git a/ios/RELEASING.md b/ios/RELEASING.md index f737503c..ddae1c27 100644 --- a/ios/RELEASING.md +++ b/ios/RELEASING.md @@ -9,16 +9,18 @@ Treat these files as required merge artifacts for iOS release candidates: ## Prerequisites -The following GitHub repository secrets must be configured (Settings -> Secrets and variables -> Actions): +The following GitHub Actions **secrets** are used by the iOS workflows (already configured for this repo; do not rename). Values never appear in logs or git. -| Secret | Description | How to get it | -|--------|-------------|---------------| -| `BUILD_CERTIFICATE_BASE64` | Base64-encoded `.p12` distribution certificate | Export from Keychain Access, then `base64 -i cert.p12 \| pbcopy` | -| `P12_PASSWORD` | Password used when exporting the `.p12` | The password you set during `.p12` export | -| `BUILD_PROVISION_PROFILE_BASE64` | Base64-encoded `.mobileprovision` file | Download from developer.apple.com, then `base64 -i profile.mobileprovision \| pbcopy` | -| `APPSTORE_API_KEY_ID` | App Store Connect API Key ID | From appstoreconnect.apple.com -> Users and Access -> Integrations | -| `APPSTORE_API_ISSUER_ID` | App Store Connect API Issuer ID | Same page as above | -| `APPSTORE_API_PRIVATE_KEY` | Contents of the `.p8` API key file | Paste the full file contents including BEGIN/END lines | +| Secret | Used for | +|--------|----------| +| `BUILD_CERTIFICATE_BASE64` | Distribution signing (`xcodebuild` / `gym`) | +| `P12_PASSWORD` | `.p12` import password | +| `BUILD_PROVISION_PROFILE_BASE64` | App Store provisioning profile | +| `APPSTORE_API_KEY_ID` | App Store Connect API (JWT `kid`) | +| `APPSTORE_API_ISSUER_ID` | App Store Connect API issuer | +| `APPSTORE_API_PRIVATE_KEY` | API private key (full `.p8` contents as a secret; never commit the file) | + +The App Store release workflow maps these to Fastlane’s standard `APP_STORE_CONNECT_API_*` environment names at runtime (see `.github/workflows/ios-app-store-release.yml`). ## First-time setup @@ -75,7 +77,9 @@ When you are ready to push **metadata + binary** to App Store Connect for the ve ``` 4. [iOS App Store release (Fastlane)](../.github/workflows/ios-app-store-release.yml) runs: smoke tests, checklist gate, `npm run ios:app-store:dry-run`, then `bundle exec fastlane release` (preflight → `gym` → `deliver`). -**Submit for review** is off by default in CI (binary and metadata still upload). To enable programmatic submission for a run, set the GitHub repository variable `IOS_SUBMIT_FOR_REVIEW` to `1`. Optional: `IOS_AUTOMATIC_RELEASE=1` for automatic release after approval. +**Submit for review:** CI sets `DELIVER_SKIP_SUBMISSION=1` so `deliver` uploads the IPA and metadata but does **not** submit the version for review (harness / #242 default). Issue **#296** can turn on full submission by clearing that guard and following the release-owner checklist. + +Optional after #296: set `DELIVER_SKIP_SUBMISSION=0` and `SUBMIT_FOR_REVIEW=1` when programmatic submission is explicitly approved; optional `AUTOMATIC_RELEASE=1`. ### Fastlane (local or debugging) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 39de4401..11973f3e 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -4,11 +4,24 @@ require "shellwords" default_platform(:ios) +def asc_api_key_id + ENV["APP_STORE_CONNECT_API_KEY_ID"] || ENV.fetch("APPSTORE_API_KEY_ID") +end + +def asc_api_key_issuer_id + ENV["APP_STORE_CONNECT_API_ISSUER_ID"] || ENV.fetch("APPSTORE_API_ISSUER_ID") +end + +def asc_api_key_content + raw = ENV["APP_STORE_CONNECT_API_KEY_CONTENT"] || ENV.fetch("APPSTORE_API_PRIVATE_KEY") + raw.gsub("\\n", "\n") +end + def stillpoint_api_key app_store_connect_api_key( - key_id: ENV.fetch("APPSTORE_API_KEY_ID"), - issuer_id: ENV.fetch("APPSTORE_API_ISSUER_ID"), - key_content: ENV.fetch("APPSTORE_API_PRIVATE_KEY").gsub("\\n", "\n"), + key_id: asc_api_key_id, + issuer_id: asc_api_key_issuer_id, + key_content: asc_api_key_content, in_house: false ) end @@ -145,17 +158,20 @@ platform :ios do ) end - desc "Preflight → build → deliver metadata + IPA. Set SUBMIT_FOR_REVIEW=1 to submit for review after upload." + desc "Preflight → build → deliver metadata + IPA. Submission: set DELIVER_SKIP_SUBMISSION=0 and SUBMIT_FOR_REVIEW=1 (issue #296+)." lane :release do preflight unless ENV["SKIP_PREFLIGHT"] == "1" stillpoint_api_key build + # Default skips ASC submission (upload-only). Unset is treated as skip (safe for harness / #242). + skip_submission = ENV["DELIVER_SKIP_SUBMISSION"] != "0" deliver( api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], ipa: lane_context[SharedValues::IPA_OUTPUT_PATH], force: true, skip_binary_upload: false, - submit_for_review: ENV["SUBMIT_FOR_REVIEW"] == "1", + skip_submission: skip_submission, + submit_for_review: !skip_submission && ENV["SUBMIT_FOR_REVIEW"] == "1", automatic_release: ENV["AUTOMATIC_RELEASE"] == "1", run_precheck_before_submit: false ) From 83aec00f70ca47218cfecc0367711f4d95c62b0b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 22:18:18 +0000 Subject: [PATCH 03/10] Fix deliver options: remove invalid skip_submission (Bugbot) - release lane uses only submit_for_review from SUBMIT_FOR_REVIEW; CI omits it for upload-only harness. - Remove DELIVER_SKIP_SUBMISSION from workflow; update RELEASING and automation-evidence accordingly. Co-authored-by: Bretton Auerbach --- .github/workflows/ios-app-store-release.yml | 4 ++-- docs/operations/automation-evidence.md | 4 ++-- ios/RELEASING.md | 4 ++-- ios/fastlane/Fastfile | 10 ++++------ 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml index 2a88d9a6..e6bfa181 100644 --- a/.github/workflows/ios-app-store-release.yml +++ b/.github/workflows/ios-app-store-release.yml @@ -216,8 +216,8 @@ jobs: APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} APPSTORE_API_PRIVATE_KEY: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} RELEASE_OWNER: ${{ github.actor }} - # Harness default: upload binary + metadata only — no Submit for Review (#296 enables submission). - DELIVER_SKIP_SUBMISSION: "1" + # Harness default: upload binary + metadata only — omit SUBMIT_FOR_REVIEW (deliver uses submit_for_review: false). + # Issue #296+: set SUBMIT_FOR_REVIEW=1 (and optionally AUTOMATIC_RELEASE=1) when a release owner approves submission. run: bundle exec fastlane release - name: Clean up keychain and provisioning profile diff --git a/docs/operations/automation-evidence.md b/docs/operations/automation-evidence.md index 6b663778..92657e83 100644 --- a/docs/operations/automation-evidence.md +++ b/docs/operations/automation-evidence.md @@ -19,7 +19,7 @@ This log complements the machine-readable output from `npm run ios:app-store:dry | Release-readiness checklist gate | `grep` on `PARITY_CHECKLIST.md` / `QA_CHECKLIST.md` | Both | | Machine-readable preflight | `npm run ios:app-store:dry-run` | Both | | Archive + upload binary to App Store Connect / TestFlight | `xcodebuild -exportArchive` with API key | TestFlight (`ios-testflight.yml`) | -| Preflight + archive + metadata + binary via Fastlane | `bundle exec fastlane release` (`gym`, `deliver`) | App Store release (`ios-app-store-release.yml`); `deliver` runs with `skip_submission` unless explicitly disabled for #296+ | +| Preflight + archive + metadata + binary via Fastlane | `bundle exec fastlane release` (`gym`, `deliver`) | App Store release (`ios-app-store-release.yml`); `submit_for_review` is false unless `SUBMIT_FOR_REVIEW=1` | ## Automated locally or on a Mac runner (Fastlane) @@ -31,7 +31,7 @@ This log complements the machine-readable output from `npm run ios:app-store:dry | `bundle exec fastlane metadata_only` | `deliver` metadata only (no binary). | | `bundle exec fastlane release` | Preflight (unless `SKIP_PREFLIGHT=1`) + `gym` + `deliver` (binary + metadata). | -**Submit for review:** In GitHub Actions, `ios-app-store-release.yml` sets `DELIVER_SKIP_SUBMISSION=1`, so `deliver` uploads the IPA and metadata but does not submit for review (issue #242 / harness default). For a future live submission (#296+), remove that guard in the workflow and set `DELIVER_SKIP_SUBMISSION=0` plus `SUBMIT_FOR_REVIEW=1` only when a release owner explicitly approves. Optional: `AUTOMATIC_RELEASE=1` after submission is enabled. +**Submit for review:** CI does **not** set `SUBMIT_FOR_REVIEW`, so `deliver` runs with `submit_for_review: false` (upload-only; issue #242 default). For a future live submission (#296+), set `SUBMIT_FOR_REVIEW=1` in the workflow when a release owner approves. Optional: `AUTOMATIC_RELEASE=1` (only applied when submitting). ## Explicit human gates (not fully automatable) diff --git a/ios/RELEASING.md b/ios/RELEASING.md index ddae1c27..28fabb31 100644 --- a/ios/RELEASING.md +++ b/ios/RELEASING.md @@ -77,9 +77,9 @@ When you are ready to push **metadata + binary** to App Store Connect for the ve ``` 4. [iOS App Store release (Fastlane)](../.github/workflows/ios-app-store-release.yml) runs: smoke tests, checklist gate, `npm run ios:app-store:dry-run`, then `bundle exec fastlane release` (preflight → `gym` → `deliver`). -**Submit for review:** CI sets `DELIVER_SKIP_SUBMISSION=1` so `deliver` uploads the IPA and metadata but does **not** submit the version for review (harness / #242 default). Issue **#296** can turn on full submission by clearing that guard and following the release-owner checklist. +**Submit for review:** CI leaves `SUBMIT_FOR_REVIEW` unset, so `deliver` uses `submit_for_review: false` (upload only; harness / #242 default). Issue **#296** can enable programmatic submission by setting `SUBMIT_FOR_REVIEW=1` in the workflow when a release owner approves. -Optional after #296: set `DELIVER_SKIP_SUBMISSION=0` and `SUBMIT_FOR_REVIEW=1` when programmatic submission is explicitly approved; optional `AUTOMATIC_RELEASE=1`. +Optional after #296: set `AUTOMATIC_RELEASE=1` together with `SUBMIT_FOR_REVIEW=1` if automatic release after approval is desired. ### Fastlane (local or debugging) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 11973f3e..9692def7 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -158,21 +158,19 @@ platform :ios do ) end - desc "Preflight → build → deliver metadata + IPA. Submission: set DELIVER_SKIP_SUBMISSION=0 and SUBMIT_FOR_REVIEW=1 (issue #296+)." + desc "Preflight → build → deliver metadata + IPA. Set SUBMIT_FOR_REVIEW=1 for programmatic submit (#296+)." lane :release do preflight unless ENV["SKIP_PREFLIGHT"] == "1" stillpoint_api_key build - # Default skips ASC submission (upload-only). Unset is treated as skip (safe for harness / #242). - skip_submission = ENV["DELIVER_SKIP_SUBMISSION"] != "0" + submit = ENV["SUBMIT_FOR_REVIEW"] == "1" deliver( api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], ipa: lane_context[SharedValues::IPA_OUTPUT_PATH], force: true, skip_binary_upload: false, - skip_submission: skip_submission, - submit_for_review: !skip_submission && ENV["SUBMIT_FOR_REVIEW"] == "1", - automatic_release: ENV["AUTOMATIC_RELEASE"] == "1", + submit_for_review: submit, + automatic_release: submit && ENV["AUTOMATIC_RELEASE"] == "1", run_precheck_before_submit: false ) end From 77761b4525476f187727e3266570af192e4e1fcd Mon Sep 17 00:00:00 2001 From: auerbachb Date: Fri, 1 May 2026 20:54:41 -0400 Subject: [PATCH 04/10] fix: correct security list-keychains subcommand typo `list-keychain` (singular) is not a valid macOS security subcommand; replace with `list-keychains` (plural) and quote \$KEYCHAIN_PATH. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ios-app-store-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml index e6bfa181..3e2bc2eb 100644 --- a/.github/workflows/ios-app-store-release.yml +++ b/.github/workflows/ios-app-store-release.yml @@ -198,7 +198,7 @@ jobs: security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security list-keychain -d user -s $KEYCHAIN_PATH $(security list-keychains -d user | tr -d '"') + security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles PP_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" /dev/stdin <<< "$(/usr/bin/security cms -D -i $PP_PATH)") From 43df4dada21954163b897a3650bcd7f2c1903218 Mon Sep 17 00:00:00 2001 From: auerbachb Date: Fri, 1 May 2026 21:11:54 -0400 Subject: [PATCH 05/10] fix: use separate ExportOptionsFastlane.plist with destination=export ExportOptions.plist has destination=upload for the direct xcodebuild TestFlight workflow. Reusing it in gym+deliver caused double binary upload to App Store Connect. The new ExportOptionsFastlane.plist uses destination=export so gym only builds the IPA; deliver handles upload. Co-Authored-By: Claude Sonnet 4.6 --- ios/ExportOptionsFastlane.plist | 23 +++++++++++++++++++++++ ios/fastlane/Fastfile | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 ios/ExportOptionsFastlane.plist diff --git a/ios/ExportOptionsFastlane.plist b/ios/ExportOptionsFastlane.plist new file mode 100644 index 00000000..cfb5dc94 --- /dev/null +++ b/ios/ExportOptionsFastlane.plist @@ -0,0 +1,23 @@ + + + + + method + app-store-connect + signingStyle + manual + teamID + T5UU4BP6AV + signingCertificate + Apple Distribution + provisioningProfiles + + com.brettonauerbach.stillpoint + StillPoint App Store + + uploadSymbols + + destination + export + + diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 9692def7..8b0067af 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -128,7 +128,7 @@ platform :ios do project: "StillPoint.xcodeproj", scheme: "StillPoint", configuration: "Release", - export_options: "ExportOptions.plist", + export_options: "ExportOptionsFastlane.plist", output_directory: ENV["GYM_OUTPUT_DIRECTORY"] || "build", output_name: "StillPoint.ipa", clean: !ENV["GYM_NO_CLEAN"] From 2a080df414be5742570fca5434f424578827aef5 Mon Sep 17 00:00:00 2001 From: auerbachb Date: Fri, 1 May 2026 21:23:01 -0400 Subject: [PATCH 06/10] fix: deduplicate preflight and align release-owner in release workflow The standalone evidence step used a hard-coded release-owner string that differed from RELEASE_OWNER=github.actor used by fastlane release; now both use github.actor for a consistent audit trail. Added SKIP_PREFLIGHT=1 to the fastlane release env so the preflight dry-run only executes once (evidence step) rather than twice. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ios-app-store-release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml index 3e2bc2eb..ae9526a6 100644 --- a/.github/workflows/ios-app-store-release.yml +++ b/.github/workflows/ios-app-store-release.yml @@ -132,7 +132,7 @@ jobs: fi - name: Generate App Store submission dry-run evidence - run: npm run ios:app-store:dry-run -- --release-owner "GitHub Actions App Store release" + run: npm run ios:app-store:dry-run -- --release-owner "${{ github.actor }}" - name: Upload App Store submission dry-run evidence if: always() @@ -216,6 +216,7 @@ jobs: APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} APPSTORE_API_PRIVATE_KEY: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} RELEASE_OWNER: ${{ github.actor }} + SKIP_PREFLIGHT: "1" # Harness default: upload binary + metadata only — omit SUBMIT_FOR_REVIEW (deliver uses submit_for_review: false). # Issue #296+: set SUBMIT_FOR_REVIEW=1 (and optionally AUTOMATIC_RELEASE=1) when a release owner approves submission. run: bundle exec fastlane release From a25eecb7f17129564d3ab31c33e2339db39684d7 Mon Sep 17 00:00:00 2001 From: auerbachb Date: Mon, 4 May 2026 11:32:39 -0400 Subject: [PATCH 07/10] fix: GHA tag glob, BSD grep/sed portability - Tag filter: `ios-v[0-9]+.[0-9]+.[0-9]+` used regex `+` which is literal in GHA glob patterns; replace with `ios-v*.*.*` - grep/sed: `\s` is not supported by macOS BSD grep/sed; replace with POSIX `[[:space:]]` throughout the validate-tag step Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ios-app-store-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml index ae9526a6..33fe1cf1 100644 --- a/.github/workflows/ios-app-store-release.yml +++ b/.github/workflows/ios-app-store-release.yml @@ -4,7 +4,7 @@ on: push: tags: # Strict semver: ios-v1.2.3 (no -build suffix — those stay on TestFlight workflow). - - 'ios-v[0-9]+.[0-9]+.[0-9]+' + - 'ios-v*.*.*' permissions: contents: read @@ -96,7 +96,7 @@ jobs: exit 1 fi VER="${TAG#ios-v}" - MARKETING="$(grep -E '^\s+MARKETING_VERSION:' ios/project.yml | head -1 | sed -E 's/.*MARKETING_VERSION:\s*"?([^"#]+)"?.*/\1/')" + MARKETING="$(grep -E '^[[:space:]]+MARKETING_VERSION:' ios/project.yml | head -1 | sed -E 's/.*MARKETING_VERSION:[[:space:]]*"?([^"#]+)"?.*/\1/')" if [[ "$VER" != "$MARKETING" ]]; then echo "::error::Tag version $VER does not match ios/project.yml MARKETING_VERSION=$MARKETING" exit 1 From c55bbc617e78b86a32191dce6997b4628accbaa6 Mon Sep 17 00:00:00 2001 From: auerbachb Date: Mon, 4 May 2026 12:06:25 -0400 Subject: [PATCH 08/10] fix: tag exclusion, base64 -D, dry-run check, doc updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add '!ios-v*-build*' exclusion so TestFlight build tags (ios-v*-build*) no longer trigger the App Store release workflow - Fix base64 --decode → -D for macOS BSD base64 compatibility; quote CERTIFICATE_PATH and PP_PATH variables - Update dry-run A4 check to match new glob pattern 'ios-v*.*.*' - Fix Deliverfile comment: CI does not submit unless SUBMIT_FOR_REVIEW=1 - Fix RELEASING.md stale alias: fastlane release now runs preflight + gym + deliver, not screenshots-only Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ios-app-store-release.yml | 5 +++-- ios/RELEASING.md | 2 +- ios/fastlane/Deliverfile | 3 ++- scripts/ios-app-store-dry-run.mjs | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml index 33fe1cf1..646e2603 100644 --- a/.github/workflows/ios-app-store-release.yml +++ b/.github/workflows/ios-app-store-release.yml @@ -5,6 +5,7 @@ on: tags: # Strict semver: ios-v1.2.3 (no -build suffix — those stay on TestFlight workflow). - 'ios-v*.*.*' + - '!ios-v*-build*' permissions: contents: read @@ -189,8 +190,8 @@ jobs: KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db KEYCHAIN_PASSWORD=$(openssl rand -hex 16) - echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH - echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 -D -o "$CERTIFICATE_PATH" + echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 -D -o "$PP_PATH" security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security set-keychain-settings -lut 21600 $KEYCHAIN_PATH diff --git a/ios/RELEASING.md b/ios/RELEASING.md index 28fabb31..37c23040 100644 --- a/ios/RELEASING.md +++ b/ios/RELEASING.md @@ -153,7 +153,7 @@ This runs `StillPointAppUITests/SnapshotTests` with `SP_UI_TEST_SNAPSHOT_SEED=1` bundle exec fastlane upload_app_store_screenshots ``` - Alias: `bundle exec fastlane release` (screenshots-only; does not submit binary). + Use `bundle exec fastlane upload_app_store_screenshots` for screenshot-only uploads. Note: `bundle exec fastlane release` now runs preflight → gym → deliver (builds and uploads the IPA) — do not use it for screenshots only. ### CI diff --git a/ios/fastlane/Deliverfile b/ios/fastlane/Deliverfile index 38a75ca2..b266e035 100644 --- a/ios/fastlane/Deliverfile +++ b/ios/fastlane/Deliverfile @@ -3,7 +3,8 @@ # Localized metadata lives under fastlane/metadata// (en-US canonical). # Screenshots: issue #325 / separate workflow; not required for metadata-only lanes. -# Default: do not auto-submit; CI passes submit_for_review: true when releasing. +# Default: do not auto-submit from CI. +# CI uploads binary + metadata without submission unless SUBMIT_FOR_REVIEW=1 is set (issue #296). submit_for_review(false) automatic_release(false) skip_screenshots(true) diff --git a/scripts/ios-app-store-dry-run.mjs b/scripts/ios-app-store-dry-run.mjs index 4ab6ba34..f4bbd370 100644 --- a/scripts/ios-app-store-dry-run.mjs +++ b/scripts/ios-app-store-dry-run.mjs @@ -397,12 +397,12 @@ async function main() { const testflightTagOk = workflow.includes("tags:") && workflow.includes("'ios-v*-build*'"); const releaseTagOk = - releaseWorkflow.includes("tags:") && releaseWorkflow.includes("'ios-v[0-9]+.[0-9]+.[0-9]+'"); + releaseWorkflow.includes("tags:") && releaseWorkflow.includes("'ios-v*.*.*'"); testflightTagOk && releaseTagOk - ? pass("A4", "TestFlight uses ios-v*-build* tags; App Store release uses strict ios-vMAJOR.MINOR.PATCH tags.", { + ? pass("A4", "TestFlight uses ios-v*-build* tags; App Store release uses ios-v*.*.* (excluding build tags).", { intendedTag, testflightTagPattern: "ios-v*-build*", - appStoreReleaseTagPattern: "ios-vMAJOR.MINOR.PATCH", + appStoreReleaseTagPattern: "ios-v*.*.*", }) : fail("A4", "Workflow tag triggers are misconfigured for TestFlight vs App Store release.", { testflightTagOk, From 6ceb4c29ef0744dea99c83104f4ed40cd3d69943 Mon Sep 17 00:00:00 2001 From: auerbachb Date: Mon, 4 May 2026 12:28:45 -0400 Subject: [PATCH 09/10] fix: pin Node.js in fastlane-release job, trim sed whitespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add actions/setup-node@v4 (node-version: 22) to fastlane-release job before the dry-run step, matching the version pinned in pre-flight-tests - Pipe MARKETING_VERSION sed extraction through xargs to trim trailing whitespace — unquoted YAML values with trailing spaces would cause a false version mismatch Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ios-app-store-release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml index 646e2603..93d1c964 100644 --- a/.github/workflows/ios-app-store-release.yml +++ b/.github/workflows/ios-app-store-release.yml @@ -87,6 +87,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Validate tag matches ios project marketing version shell: bash run: | @@ -97,7 +102,7 @@ jobs: exit 1 fi VER="${TAG#ios-v}" - MARKETING="$(grep -E '^[[:space:]]+MARKETING_VERSION:' ios/project.yml | head -1 | sed -E 's/.*MARKETING_VERSION:[[:space:]]*"?([^"#]+)"?.*/\1/')" + MARKETING="$(grep -E '^[[:space:]]+MARKETING_VERSION:' ios/project.yml | head -1 | sed -E 's/.*MARKETING_VERSION:[[:space:]]*"?([^"#]+)"?.*/\1/' | xargs)" if [[ "$VER" != "$MARKETING" ]]; then echo "::error::Tag version $VER does not match ios/project.yml MARKETING_VERSION=$MARKETING" exit 1 From c5f518d54d2952fe9d1bee5078bc76f0d3e05be1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 5 May 2026 14:47:39 +0000 Subject: [PATCH 10/10] fix(ios): validate required secrets across either workflow The dry-run treated a secret as present only when both TestFlight and App Store release workflows referenced it. Use OR so a secret referenced by either workflow satisfies B2, matching how secrets are actually consumed when workflows diverge. Co-authored-by: Bretton Auerbach --- scripts/ios-app-store-dry-run.mjs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/ios-app-store-dry-run.mjs b/scripts/ios-app-store-dry-run.mjs index f4bbd370..42bfee69 100644 --- a/scripts/ios-app-store-dry-run.mjs +++ b/scripts/ios-app-store-dry-run.mjs @@ -418,7 +418,7 @@ async function main() { : fail("A5", "One or more workflow-required release-readiness checklist items are missing."); const workflowSecretRefs = requiredWorkflowSecrets.filter( - (secret) => workflow.includes(`secrets.${secret}`) && releaseWorkflow.includes(`secrets.${secret}`), + (secret) => workflow.includes(`secrets.${secret}`) || releaseWorkflow.includes(`secrets.${secret}`), ); const githubSecrets = listGithubSecrets(); const missingWorkflowRefs = requiredWorkflowSecrets.filter((secret) => !workflowSecretRefs.includes(secret)); @@ -426,7 +426,7 @@ async function main() { ? requiredWorkflowSecrets.filter((secret) => !githubSecrets.names.includes(secret)) : []; if (missingWorkflowRefs.length > 0) { - fail("B2", "One or both iOS workflows are missing required secret references.", { + fail("B2", "At least one required secret is not referenced by either the TestFlight or App Store release workflow.", { requiredWorkflowSecrets, workflowSecretRefs, missingWorkflowRefs, @@ -436,15 +436,16 @@ async function main() { message: `Workflow(s) missing required secret references: ${missingWorkflowRefs.join(", ")}.`, }; } else if (githubSecrets.available && missingGithubSecrets.length === 0) { - pass("B2", "Both iOS workflows reference every required secret and GitHub reports all required secret names.", { + pass("B2", "Each required secret is referenced by the TestFlight workflow and/or the App Store release workflow, and GitHub reports all required secret names.", { requiredWorkflowSecrets, }); reportSecretStatus = { status: "pass", - message: "Both iOS workflows reference every required secret and GitHub reports all required secret names.", + message: + "Each required secret is referenced by the TestFlight workflow and/or the App Store release workflow, and GitHub reports all required secret names.", }; } else if (githubSecrets.available) { - warn("B2", "Both iOS workflows reference every required secret, but GitHub is missing one or more required secret names.", { + warn("B2", "Each required secret is referenced by the TestFlight workflow and/or the App Store release workflow, but GitHub is missing one or more required secret names.", { requiredWorkflowSecrets, missingGithubSecrets, }); @@ -453,7 +454,7 @@ async function main() { message: `GitHub secret names missing: ${missingGithubSecrets.join(", ")}.`, }; } else { - warn("B2", "Both iOS workflows reference every required secret, but repository secret visibility could not be confirmed.", { + warn("B2", "Each required secret is referenced by the TestFlight workflow and/or the App Store release workflow, but repository secret visibility could not be confirmed.", { requiredWorkflowSecrets, reason: githubSecrets.reason, });