## 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.
9.7 KiB
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=falsewhen 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-*.pngthrough06, 1320×2868.fastlane/screenshots/da-DK/ipad-13-01-*.pngthrough06, 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:
- A dedicated App Store Connect team API key named
GitHub App Store CIwith the App Manager role. Team JWTs use the account issuer ID in theissclaim. - A dedicated Apple Distribution certificate for CI.
- An App Store distribution provisioning profile for
io.truckwash.app. - An internal TestFlight group named
Internal QAwith automatic distribution.
Configure two GitHub environments:
app-store-signing, branch policy limited to protectedmaster.app-store-candidate, tag policy limited to protectedios-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_BASE64IOS_DISTRIBUTION_CERTIFICATE_PASSWORDIOS_APP_STORE_PROFILE_BASE64APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64
Environment variables:
APPLE_TEAM_ID=HP3FJ4GVL7IOS_BUNDLE_ID=io.truckwash.appIOS_SCHEME=AppIOS_PROJECT=ios/App/App.xcodeprojAPP_STORE_CONNECT_API_KEY_IDAPP_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:
- Merge all product-readiness work and confirm
App Store Readinesspasses. After its first successful default-branch run, add that job to the protected master ruleset's required status checks. - Verify the privacy policy, account-deletion flow, icons, localized permission copy, and privacy manifest on a device.
- Set the repository switch to
trueduring a controlled release window. - Dispatch
iOS Internal TestFlightfrom themasterworkflow definition, supplying the full current master SHA and confirmationUPLOAD IOS INTERNAL BUILD. - 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.
- 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.
- 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
- Verify the desired TestFlight build on iPhone and iPad.
- Confirm its commit's
ios/release.jsoncontains the public version. - Create a new protected tag such as
ios-v1.0.0on that exact commit. Never move or reuse an existing release tag. iOS App Store Candidatelocates 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.- In App Store Connect, review the rendered product page, review account, privacy/export/age answers, and candidate build. Submit manually.
- Submit version
1.0.0for review. Apple releases it automatically after approval. Do not use phased release for1.0.0; use phased release for later updates unless there is a reason not to. - Merge the next
ios/release.jsonversion 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.