Files
pleno-vue/docs/app-store-release.md
T
Jeppe B 5702d45bc6 fix(ios): harden App Store release automation (#221)
## Summary

- replace the unsupported top-level App Store version collection with
Apple's app-scoped version endpoint
- add tested release-policy and availability readback for exact
version/build, `AFTER_APPROVAL`, Denmark only, no preorder, and no
automatic future territories
- strengthen the stable `App Store Readiness` check and align
Fastlane/candidate handoff with the approved 1.0.0 release policy

## Task contract

`truckwash-ios-release-20260723` — R4 (`ci-policy`, `release-policy`,
`credential-handling`, `branch-protection-or-rules`,
`mobile-store-submission`). The user explicitly approved implementation,
protected-master delivery, and the App Store release path.

## Changed files

- App Store Connect client and dependency-free Node tests
- App Store readiness and candidate workflows
- Fastlane candidate release configuration
- Apple App Store release runbook

## Verification

- `node --test tests/node/app-store-connect.test.mjs` — 10 passed
- `node scripts/mobile/validate-app-store.mjs --strict` — passed
- `node scripts/mobile/check-permissions.mjs` — passed
- App Store product-readiness Vitest — 5 passed
- ESLint on changed Node files — passed
- workflow YAML parsing — passed
- `git diff --check` — passed
- local Fastlane validation unavailable because Ruby/Bundler is not
installed on this host; `App Store Readiness` runs it on GitHub

## Release target

- iOS App Store
- bundle `io.truckwash.app`
- version `1.0.0`
- App Store Connect app `6792777794`
- Denmark only
- automatic release after approval
- no preorder or phased release for 1.0.0

The repository App Store automation switch remains disabled until this
change is merged and credential health is reverified.
2026-07-23 12:59:17 +00:00

9.7 KiB
Raw Blame History

Apple App Store Release Runbook

This is the operating runbook for the public iOS application and its signed GitHub Actions delivery. Public review submission remains a human action in App Store Connect; the approved version releases automatically after Apple approval.

Storefront record

Create or reconcile one App Store Connect record:

Setting Value
Name Truck Wash
Bundle ID io.truckwash.app
SKU truckwash-ios
Primary language Danish
Category Business
Price Free
Availability Denmark only
Support URL https://truckwash.io/support
Privacy URL https://truckwash.io/privacy-policy
Marketing URL https://truckwash.io/
Release Automatically after approval

Use the standard Apple EULA and do not configure in-app purchases. Payments in the product cover physical truck-wash services. Keep iPhone and iPad enabled; disable Apple-silicon Mac and Vision Pro compatibility until those targets have been tested deliberately. Do not enable preorder or phased release for version 1.0.0, and disable automatic availability in newly added territories.

The Account Holder or Admin must complete these console-only items before the first candidate:

  • Accept current Apple developer and business agreements.
  • Verify Truck Wash ApS's EU Digital Services Act trader identity and contact information.
  • Complete the current age-rating questionnaire. Do not hard-code an expected rating in automation.
  • Approve the privacy data matrix and enter matching App Privacy answers, including third-party SDK behavior.
  • Decide export compliance after reviewing the final binary. Only add ITSAppUsesNonExemptEncryption=false when the exempt determination is approved.
  • Complete accessibility declarations only for behavior verified on devices.
  • Store a durable, sanitized review account in App Store Connect. Never commit its password, OTP seed, or recovery data.

Review notes must explain customer and driver login, the review account's 2FA path, QR/hardware behavior, camera/location denial fallbacks, and the physical service payment model.

Metadata and assets in Git

fastlane/metadata/da-DK/ is the Danish storefront source of truth. ios/release.json is the release-version source of truth. Its version is numeric X.Y.Z; its bundle ID must remain io.truckwash.app.

Run the readiness validation locally:

npm run mobile:ios:storefront:check

Readiness mode validates all present assets and reports missing screenshot sets as warnings. A candidate tag runs strict mode and requires exactly six reviewed images in each set:

  • fastlane/screenshots/da-DK/iphone-6.9-01-*.png through 06, 1320×2868.
  • fastlane/screenshots/da-DK/ipad-13-01-*.png through 06, 2064×2752.

Use Xcode 26 simulators and the real Capacitor app. Capture dashboard, booking, self-wash/QR, vehicles, orders/history, and invoices. Screenshots must contain sanitized fixture data, no alpha channel, no real customer data, and no placeholder content. Linux CI cannot honestly synthesize authenticated native captures; capture and approve them on a controlled macOS machine before tagging.

The validator also rejects the known default Capacitor icon and splash artwork. Native permission strings must exist in Danish and English and are included via the InfoPlist.strings Xcode variant group.

Apple identities and GitHub configuration

Create:

  1. A dedicated App Store Connect team API key named GitHub App Store CI with the App Manager role. Team JWTs use the account issuer ID in the iss claim.
  2. A dedicated Apple Distribution certificate for CI.
  3. An App Store distribution provisioning profile for io.truckwash.app.
  4. An internal TestFlight group named Internal QA with automatic distribution.

Configure two GitHub environments:

  • app-store-signing, branch policy limited to protected master.
  • app-store-candidate, tag policy limited to protected ios-v* tags.

Private repositories on the Team plan cannot rely on environment required reviewers. Protect ios-v* creation/update/deletion with a repository ruleset limited to release managers. Manual App Review submission is the final human approval. App Store Connect API readback must show AFTER_APPROVAL, Denmark (DNK) as the only available territory, preorder disabled, and automatic future territories disabled.

Environment secrets:

  • IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64
  • IOS_DISTRIBUTION_CERTIFICATE_PASSWORD
  • IOS_APP_STORE_PROFILE_BASE64
  • APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64

Environment variables:

  • APPLE_TEAM_ID=HP3FJ4GVL7
  • IOS_BUNDLE_ID=io.truckwash.app
  • IOS_SCHEME=App
  • IOS_PROJECT=ios/App/App.xcodeproj
  • APP_STORE_CONNECT_API_KEY_ID
  • APP_STORE_CONNECT_APP_ID (Apple's numeric app resource ID)
  • TESTFLIGHT_INTERNAL_GROUP_ID (Apple's beta-group resource ID)
  • APP_STORE_CONNECT_ISSUER_ID=074cc671-edc3-403d-b85f-98470f3b16bd

The repository variable APP_STORE_AUTOMATION_ENABLED is the authoritative activation switch. Missing or any value other than true makes all signing, credential-health, and candidate workflows succeed as safe no-ops without selecting an App Store environment or reading Apple secrets.

Enablement and first canary

Keep APP_STORE_AUTOMATION_ENABLED=false while configuring Apple/GitHub state. Then:

  1. Merge all product-readiness work and confirm App Store Readiness passes. After its first successful default-branch run, add that job to the protected master ruleset's required status checks.
  2. Verify the privacy policy, account-deletion flow, icons, localized permission copy, and privacy manifest on a device.
  3. Set the repository switch to true during a controlled release window.
  4. Dispatch iOS Internal TestFlight from the master workflow definition, supplying the full current master SHA and confirmation UPLOAD IOS INTERNAL BUILD.
  5. Confirm the workflow validates Xcode 26.3/iOS 26, certificate/profile identity and expiry, the signed IPA, App Store processing, and exact Internal QA assignment.
  6. Install the result on a clean supported iPhone and iPad. Verify fresh install, upgrade, login, resume, offline/reconnect, permission allow/deny, booking, self-wash/QR, vehicles, orders, invoices, support/privacy, and account deletion.
  7. Leave the switch enabled only after the canary is accepted.

If the first live credential attempt fails, set the repository switch back to false before investigating. This avoids red master releases while credentials are incomplete.

Continuous TestFlight delivery

Frontend Release publishes a signed-by-CI evidence artifact only after the production deployment, public live gate, credentialed live gate, Release Manager gate, and server-version update all pass for current master.

iOS Internal TestFlight consumes that exact proof. It refuses a stale SHA, uses /Applications/Xcode_26.3.app, requires an iOS 26 SDK, queries App Store Connect for the next build number under serialized concurrency, signs and inspects the IPA, uploads through pinned Fastlane, waits for processing, and idempotently assigns the exact build to Internal QA.

Outputs include:

  • Signed IPA, retained for 30 days.
  • dSYMs, SHA-256 checksums, and ios-release-manifest.json, retained for 90 days.
  • Source SHA, marketing/build versions, App Store build ID, Xcode/SDK versions, and workflow identity in the manifest.

Every successful future Frontend Release for current master triggers this delivery automatically. Stale or proofless releases do not sign or upload.

Select a public candidate

  1. Verify the desired TestFlight build on iPhone and iPad.
  2. Confirm its commit's ios/release.json contains the public version.
  3. Create a new protected tag such as ios-v1.0.0 on that exact commit. Never move or reuse an existing release tag.
  4. iOS App Store Candidate locates the release manifest for that exact SHA, verifies the exact processed App Store build, enforces complete screenshots, synchronizes Danish metadata, attaches the existing build, and reads it back. It also writes and verifies automatic release after approval, then verifies Denmark-only availability and no preorder. It does not rebuild or submit for review.
  5. In App Store Connect, review the rendered product page, review account, privacy/export/age answers, and candidate build. Submit manually.
  6. Submit version 1.0.0 for review. Apple releases it automatically after approval. Do not use phased release for 1.0.0; use phased release for later updates unless there is a reason not to.
  7. Merge the next ios/release.json version bump before further delivery after Apple closes the released version to new builds.

Rotation and recovery

iOS Credential Health runs every Monday and fails when certificate/profile identity drifts or either expires within 30 days. Rotate one credential at a time, keep automation disabled during rotation, and repeat the canary.

  • Bad TestFlight build: expire it, fix master, and produce a new build number.
  • Bad candidate: detach it in App Store Connect and tag a corrected tested SHA with a new version; never move the tag.
  • Bad phased update: pause the phase.
  • Compromised key/certificate: disable automation, revoke it in Apple, rotate GitHub secrets, inspect audit logs, and run a fresh canary.
  • Public emergency: remove from sale only when necessary and prepare an expedited corrective version.