# Apple App Store Release Runbook This is the operating runbook for the public iOS application and its signed GitHub Actions delivery. Public review submission and public release remain human actions in App Store Connect. ## Storefront record Create or reconcile one App Store Connect record: | Setting | Value | | --- | --- | | Name | Truck Wash Kundeportal | | 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 | Manual 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. 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: ```sh 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. 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 does not rebuild, submit for review, or release publicly. 5. In App Store Connect, review the rendered product page, review account, privacy/export/age answers, and candidate build. Submit manually. 6. Release the first Denmark version manually after approval. 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.