Automate signed iOS App Store releases (#192)

## What changed

- adds production iOS identity, localized storefront metadata, native
privacy declarations, App Store-safe artwork, and account-deletion UX
- mirrors the live Danish Google Play title, short description, and long
description in the App Store metadata source
- generates Android launcher/store icons from the opaque iOS marketing
master so both platforms use the same white background
- adds guarded GitHub Actions workflows for storefront readiness,
credential health, signed TestFlight uploads, and App Store candidate
preparation
- adds pinned Fastlane configuration with a committed dependency lock,
release manifest tooling, and an operational App Store runbook
- preserves the upstream iOS safe-area implementation while retaining
opaque App Store icon assets

## Why

The repository previously supported development-signed device bundles
but had no production App Store identity, reproducible storefront source
of truth, or protected signed-release pipeline. Apple also requires
in-app account deletion for apps that support account creation. The
Android icon master was transparent, which rendered as black on dark
store/device surfaces.

## Impact

Automation remains fail-closed behind
`APP_STORE_AUTOMATION_ENABLED=false`. No build can upload to TestFlight
or change App Store metadata until the switch is deliberately enabled
after merge and the remaining release gates are satisfied.

## Validation

- focused App Store, iOS icon, and cross-platform icon-background tests
pass
- every generated Android store/launcher icon is opaque with pure-white
corners; iOS marketing artwork is checked the same way
- Android icon drift check passes for all 19 generated files
- production Vite build and the broader focused release checks completed
successfully
- storefront metadata is valid; only the two expected screenshot-set
warnings remain
- App Store Readiness is green at head `4445fecc`
- Apple Distribution certificate and App Store profile were
independently verified for `HP3FJ4GVL7.io.truckwash.app`
- live App Store Connect API authentication succeeded for app
`6792777794`
- App Store record, free Denmark-only availability, and automatic
`Internal QA` TestFlight group are configured
- EU trader status, Content Rights, 4+ age rating, and the published App
Privacy label are completed in App Store Connect
- iPhone and iPad accessibility declarations are configured honestly as
pre-release drafts

## Remaining external gates

- reviewed iPhone and iPad screenshot sets are still required
- an App Review login must be supplied without creating or exposing
customer credentials
- the first signed TestFlight candidate must run after merge and
deliberate automation enablement
This commit is contained in:
Jeppe B
2026-07-20 17:59:43 +02:00
committed by GitHub
parent 133e53cfa6
commit 88eda43560
109 changed files with 4069 additions and 529 deletions
+181 -80
View File
@@ -1,102 +1,203 @@
# Apple App Store Release Runbook
This runbook covers the public iOS App Store release path for the Truck Wash
Capacitor app.
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.
## Account And App Record
## Storefront record
- Use the Truck Wash ApS Apple Developer account. The Account Holder must accept
the latest Apple agreements before builds can be uploaded.
- Create or verify the App Store Connect app record:
- Platform: iOS
- Name: Truck Wash Kundeportal
- Bundle ID: `io.truckwash.app`
- SKU: `truckwash-ios`
- Primary language: Danish
- Category: Business
- Price: Free
- Initial availability: Denmark
- Keep the GitHub environment `mobile-store-production` configured with the
iOS signing, App Store Connect, Android signing, and Google Play upload
secrets used by the mobile workflow.
Create or reconcile one App Store Connect record:
## Build And Upload
| 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 |
1. Merge the release commit to `master`.
2. Confirm `Automated Tests` and `Frontend Release` are green for that commit.
3. Create a release tag such as `mobile-v1.0.0`.
4. The `Mobile Store Artifacts` workflow builds Android and iOS artifacts from
the tested commit. By default it uploads Android to the Google Play
production track and uploads the iOS IPA to App Store Connect.
5. For a manual upload, dispatch `Mobile Store Artifacts` with `version_name`
and `version_code`. Leave `upload_ios_to_app_store` enabled for the iOS
upload, or disable it to produce only the signed GitHub artifact.
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 same workflow also runs automatically after a successful `Automated Tests`
run on current `master`. It skips stale workflow-run commits if `master` has
advanced before the mobile jobs start.
The Account Holder or Admin must complete these console-only items before the
first candidate:
The iOS workflow expects these environment secrets:
- 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.
- `IOS_CERTIFICATE_BASE64`
- `IOS_CERTIFICATE_PASSWORD`
- `IOS_PROVISION_PROFILE_BASE64`
- `IOS_KEYCHAIN_PASSWORD`
- `APPLE_TEAM_ID`
- `APP_STORE_CONNECT_API_KEY_ID`
- `APP_STORE_CONNECT_ISSUER_ID`
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`
The workflow installs the signing certificate and provisioning profile in a
temporary keychain on the `macos-15` runner, archives the Capacitor Xcode
project, exports an App Store IPA, validates it with `xcrun altool`, uploads it
with the App Store Connect API key, and removes temporary signing assets in the
cleanup step.
Environment variables:
## Product Page Defaults
- `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`
- Support URL: `https://truckwash.io/support`
- Privacy URL: `https://truckwash.io/privacy-policy`
- Subtitle: `Book og start truckvask`
- Promotional text: `Administrer vask, koeretoejer, ordrer og fakturaer fra mobilen.`
- Keywords: `truck wash,lastbilvask,vask,booking,kundeportal`
- Expected age rating: 4+, subject to the App Store Connect questionnaire.
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.
Use real iOS simulator or device screenshots. Provide at least:
## Enablement and first canary
- iPhone 6.9-inch portrait screenshots
- iPad 13-inch portrait screenshots
Keep `APP_STORE_AUTOMATION_ENABLED=false` while configuring Apple/GitHub state.
Then:
Recommended screenshot scenes: dashboard, booking flow, self-service wash start,
vehicles/orders, and invoices/payment history. Do not include real customer
data, private tokens, or placeholder copy.
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.
## Privacy And Review Notes
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.
App Store Connect privacy labels must match the actual app and backend behavior.
Expected minimum disclosures include account/contact data, identifiers such as
customer number, vehicle/license plate data, order and invoice history, payment
state, approximate/precise location when used, and photos or attachments when
users upload them. Tracking should remain false unless analytics/ad tracking is
introduced.
## Continuous TestFlight delivery
Review notes must include:
`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`.
- A demo account and password.
- OTP/2FA/passkey fallback instructions when enabled for the account.
- A clear statement that Stripe/card payments are for physical truck-wash
services consumed outside the app, so Apple in-app purchase is not used.
- Any hardware-dependent functionality that reviewers cannot reproduce, with a
short demo video if needed.
- Confirmation that the backend environment is online for the whole review
window.
`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.
## TestFlight And Release
Outputs include:
1. Wait for App Store Connect processing to finish.
2. Distribute the processed build to internal TestFlight testers.
3. Run clean-device QA on iPhone and iPad.
4. Fix issues using the same marketing version and an incremented build number.
5. Submit for App Review with manual release after approval.
6. After approval, release to Denmark first and monitor crashes, support mail,
and App Store Connect feedback before expanding availability.
- 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.
+37 -84
View File
@@ -1,35 +1,20 @@
# Mobile Store Artifacts
# Mobile Store Delivery
The `Mobile Store Artifacts` workflow builds signed Android and iOS store artifacts from the Vue/Vite web app through Capacitor, then uploads them to Google Play and App Store Connect by default.
Android and iOS delivery are intentionally independent. An iOS release or tag
must never publish an Android production artifact.
Use the Capacitor project under `android/` for the Google Play Store package. The Bubblewrap/TWA files at the repository root are not the path used by `mobile:android:bundle`.
## Android
## Triggers
`Android Store Artifacts` remains in
`.github/workflows/mobile-artifacts.yml`. It builds the Capacitor Android package
`io.truckwash.twa` and supports:
- Manual: run `Mobile Store Artifacts` from GitHub Actions and optionally provide `version_name`, `version_code`, upload toggles, and Android track/status overrides.
- Tag: push a tag named `mobile-vX.Y.Z`; the workflow uses `X.Y.Z` as the store version name.
- Automatic store upload: after the `Automated Tests` workflow completes successfully on current `master`, GitHub Actions builds signed Android and iOS artifacts from that tested commit and uploads them to the stores.
- Stale workflow-run protection: if a newer commit reaches `master` before the mobile workflow runs, both store-upload jobs skip the stale commit.
- Automatic delivery after successful current-master `Automated Tests`.
- Manual dispatch with version, version code, upload toggle, track, and status.
- Existing `mobile-v*` tags for the Android workflow.
Default upload behavior:
- Android uploads package `io.truckwash.twa` to the Google Play `production` track with release status `completed`.
- iOS uploads bundle `io.truckwash.app` to App Store Connect for TestFlight/App Review processing. Public App Store release still depends on App Store Connect review and release settings.
- Manual dispatch can disable either upload path while still producing signed GitHub artifacts.
## Required Secrets
Store secrets are expected in the GitHub environment `mobile-store-production`.
Non-secret environment variables:
- `ANDROID_PACKAGE_NAME=io.truckwash.twa`
- `ANDROID_AAB_PATH=android/app/build/outputs/bundle/release/app-release.aab`
- `PLAY_STORE_TRACK=production`
- `PLAY_STORE_RELEASE_STATUS=completed`
- `PLAY_STORE_USER_FRACTION` only when using `PLAY_STORE_RELEASE_STATUS=inProgress`
Android:
The Android job continues using GitHub environment `mobile-store-production`.
Its required secrets are:
- `ANDROID_KEYSTORE_BASE64`
- `ANDROID_KEYSTORE_PASSWORD`
@@ -37,75 +22,43 @@ Android:
- `ANDROID_KEY_PASSWORD`
- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64`
iOS:
Its variables are `ANDROID_PACKAGE_NAME`, `ANDROID_AAB_PATH`,
`PLAY_STORE_TRACK`, `PLAY_STORE_RELEASE_STATUS`, and optional
`PLAY_STORE_USER_FRACTION`. See the Google Play Console runbook for production
track policy.
- `IOS_CERTIFICATE_BASE64`
- `IOS_CERTIFICATE_PASSWORD`
- `IOS_PROVISION_PROFILE_BASE64`
- `IOS_KEYCHAIN_PASSWORD`
- `APPLE_TEAM_ID`
- `APP_STORE_CONNECT_API_KEY_ID`
- `APP_STORE_CONNECT_ISSUER_ID`
- `APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64`
## iOS
The Google Play secret is a base64-encoded service-account JSON file with Android Publisher API access to the Play Console app. The App Store Connect private key secret is the base64-encoded `.p8` API key file.
iOS uses three separate workflows:
## Local Checks
- `iOS Internal TestFlight`: exact verified master release to signed internal
TestFlight build.
- `iOS App Store Candidate`: protected `ios-vX.Y.Z` tag to exact-build
storefront candidate, without rebuilding or submission.
- `iOS Credential Health`: weekly identity, access, and expiry preflight.
Run the native permission validation after changing Capacitor, native manifests, or store metadata:
The GitHub environments and variables are documented in
`docs/app-store-release.md`. The repository-level
`APP_STORE_AUTOMATION_ENABLED` variable gates all access to them and must remain
`false` until the signed credential canary is approved.
Local source/storefront checks:
```sh
npm run mobile:permissions:check
npm run mobile:ios:storefront:check
```
Build a local unsigned Android App Bundle for packaging verification:
Strict candidate asset check:
```sh
npm run mobile:android:bundle:unsigned
npm run mobile:ios:storefront:check-strict
```
Build a signed Play Console upload bundle after exporting the Android upload-key variables:
Version identity is deliberately different between platforms:
```sh
export ANDROID_KEYSTORE_FILE=/path/to/upload-key.jks
export ANDROID_KEYSTORE_PASSWORD=...
export ANDROID_KEY_ALIAS=...
export ANDROID_KEY_PASSWORD=...
npm run mobile:android:bundle
```
- Android package: `io.truckwash.twa`
- iOS App Store bundle: `io.truckwash.app`
The signed Android bundle is written to:
```text
android/app/build/outputs/bundle/release/app-release.aab
```
Upload a locally built signed App Bundle to Google Play after exporting the Play service-account secret and release metadata:
```sh
export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64=...
export ANDROID_PACKAGE_NAME=io.truckwash.twa
export ANDROID_AAB_PATH=android/app/build/outputs/bundle/release/app-release.aab
export MOBILE_VERSION_NAME=1.4.0
export MOBILE_VERSION_CODE=10400
export PLAY_STORE_TRACK=production
export PLAY_STORE_RELEASE_STATUS=completed
npm run mobile:android:play-upload
```
Use `PLAY_STORE_RELEASE_STATUS=inProgress` only with `PLAY_STORE_USER_FRACTION` set to a value greater than `0` and less than `1`.
Android artifacts use package id `io.truckwash.twa`. iOS artifacts use bundle id `io.truckwash.app`.
The Android project currently targets SDK 36. Google Play requires new apps and updates to target Android 15/API 35 or higher starting August 31, 2025: https://developer.android.com/google/play/requirements/target-sdk
Play Store graphics are generated in the workspace-level `playstoregraphics/` folder:
- App icon: `playstoregraphics/universal/app-icon/truck-wash-icon-512.png`
- Feature graphic: `playstoregraphics/universal/feature-graphic/truck-wash-feature-1024x500.jpg`
- Phone screenshots: `playstoregraphics/phone/screenshots/`
- 7-inch tablet screenshots: `playstoregraphics/tablet-7/screenshots/`
- 10-inch tablet screenshots: `playstoregraphics/tablet-10/screenshots/`
- Chromebook screenshots: `playstoregraphics/chromebook/screenshots/`
The native manifests declare camera and foreground location access for the store binaries. Keep the App Store Connect and Play Console privacy questionnaires aligned with the app's actual camera and location data handling before submitting a release.
The iOS release build verifies the final signed IPA rather than relying on the
Capacitor `appId`, which remains the Android package identifier.