diff --git a/.github/workflows/ios-app-store-release.yml b/.github/workflows/ios-app-store-release.yml new file mode 100644 index 00000000..93d1c964 --- /dev/null +++ b/.github/workflows/ios-app-store-release.yml @@ -0,0 +1,235 @@ +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*.*.*' + - '!ios-v*-build*' + +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: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - 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 '^[[: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 + 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.actor }}" + + - 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 -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 + 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-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)") + cp "$PP_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/${PP_UUID}.mobileprovision + + - 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 }} + 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 + + - 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..92657e83 --- /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 | 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. | + +## 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`); `submit_for_review` is false unless `SUBMIT_FOR_REVIEW=1` | + +## 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 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) + +| 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/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/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..37c23040 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 @@ -43,12 +45,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 +64,50 @@ 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:** 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 `AUTOMATIC_RELEASE=1` together with `SUBMIT_FOR_REVIEW=1` if automatic release after approval is desired. + +### 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) @@ -118,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 @@ -126,7 +161,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..b266e035 --- /dev/null +++ b/ios/fastlane/Deliverfile @@ -0,0 +1,12 @@ +# 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 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) +skip_metadata(false) +precheck_include_in_app_purchases(false) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index a13bd776..8b0067af 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -1,7 +1,31 @@ # frozen_string_literal: true +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: asc_api_key_id, + issuer_id: asc_api_key_issuer_id, + key_content: asc_api_key_content, + in_house: false + ) +end + platform :ios do desc "Regenerate App Store screenshot candidates (15–20 per device via SnapshotTests)" lane :screenshots do @@ -91,8 +115,63 @@ 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: "ExportOptionsFastlane.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 for programmatic submit (#296+)." lane :release do - upload_app_store_screenshots + preflight unless ENV["SKIP_PREFLIGHT"] == "1" + stillpoint_api_key + build + 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, + submit_for_review: submit, + automatic_release: submit && 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..42bfee69 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*.*.*'"); + testflightTagOk && releaseTagOk + ? pass("A4", "TestFlight uses ios-v*-build* tags; App Store release uses ios-v*.*.* (excluding build tags).", { + intendedTag, + testflightTagPattern: "ios-v*-build*", + appStoreReleaseTagPattern: "ios-v*.*.*", + }) + : 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,35 @@ 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", "At least one required secret is not referenced by either the TestFlight or App Store release workflow.", { 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", "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: "TestFlight workflow references 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", "TestFlight workflow references 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, }); @@ -435,13 +454,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", "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, }); 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 +533,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 +556,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"