iOS Build & Distribution¶
iOS apps build and ship to TestFlight via GitHub Actions — a build-and-upload.yml workflow in each app repo. Xcode Cloud has been retired in favour of GitHub Actions (repeatable builds + full control over runners and secrets). The historical Xcode Cloud setup is kept under Legacy for reference.
Pipeline (GitHub Actions)¶
Each dashecorp/dashe-* app repo has .github/workflows/build-and-upload.yml:
| Aspect | Value |
|---|---|
| Runner | macos-26 (GitHub-hosted) |
| Trigger | workflow_dispatch (manual) |
| Signing | fastlane match (read-only) — appstore cert + profile pulled from dashecorp/ios-certs (git storage, master), via a token minted from the iBuild-E GitHub App |
| Build number | computed from App Store Connect (latest build + 1) via the ASC API |
| Archive / export | xcodebuild |
| Upload | ASC API key → TestFlight (internal). App Store submission stays human-gated. |
Apps with the workflow: Nutri-E, Fast-E, Count-E, Drink-E, Heart-E, Reward-E (all dashecorp/dashe-*), plus Rig Cockpit (dashecorp/rig-cockpit-ios) — see below.
Rig Cockpit (dashecorp/rig-cockpit-ios)¶
Same pipeline, with the differences below. Full surface map in cockpit.md.
| Default branch | feature/fork-cutie-admin-cockpit — not main. Branch from it, PR into it, read files from it. |
| Trigger | workflow_dispatch, or pushing a v* tag. The tag is only a trigger — the build number still comes from App Store Connect and the marketing version from what is committed. |
| Signing | Signed at archive time (CODE_SIGN_STYLE=Manual + the match profile already in the keychain) so Xcode bakes the app's .entitlements in. Signing only at export would derive entitlements from the profile and silently drop com.apple.developer.applesignin → Sign in with Apple fails at runtime with error 1000. |
| Entitlement check | The workflow asserts the signed binary carries applesignin + associated-domains, and since #53 it derives the expected Universal Link domain from the app's own entitlements file (PlistBuddy on Cuti-E-Admin.entitlements) instead of hardcoding it. Before that it asserted a stale workers.dev domain and failed every build after the app moved to rig-cockpit.dashecorp.com. |
| Release to testers | A second job, release-to-testers (ubuntu-latest, needs: build), polls App Store Connect until the build is VALID, resolves the beta group by name (Internal Testers) and attaches the build. Added in #55. |
Uploading is not releasing. The Internal Testers group has hasAccessToAllBuilds: false, so
every build must be attached to the group explicitly — a green upload leaves the operator looking
at the previous build. Apple refuses to flip that flag via the API
(409 ENTITY_ERROR.ATTRIBUTE.NOT_ALLOWED on UPDATE; it is create-time only), which is why the
attach is a CI job rather than a one-off settings change. Details and the manual fallback in
cockpit.md.
Secrets (per app repo)¶
Provisioned via dashecorp/infra (dashe-secrets.tf, OpenTofu — never hand-set):
| Secret | Purpose |
|---|---|
IBUILD_E_APP_ID / IBUILD_E_PRIVATE_KEY |
GitHub App to read dashecorp/ios-certs |
MATCH_PASSWORD |
fastlane match decryption |
ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_P8_BASE64 |
App Store Connect API key (build number + upload) |
FIREBASE_* (app-specific) |
e.g. Crashlytics GoogleService-Info.plist generation |
Signing (fastlane match)¶
Distribution certs + provisioning profiles live encrypted in dashecorp/ios-certs (git storage). CI runs match … readonly:true and never mints certs in CI. A new app / bundle id therefore needs a one-time fastlane match appstore write run to populate ios-certs before its workflow can sign.
Onboarding a new iOS app¶
- Register the bundle id (Apple Developer / ASC API
register_bundle_id). - Create the App Store Connect app record in the web UI — the ASC API has no create-app endpoint.
- One-time
fastlane match appstorewrite for the bundle id →dashecorp/ios-certs. - Fan the iOS secrets above to the repo via
dashe-secrets.tf. - Add
build-and-upload.yml(copy from anydashe-*app and adaptAPP_IDENTIFIER/SCHEME/ project path).
Legacy — Xcode Cloud (retired)¶
iOS CI previously ran on Apple's Xcode Cloud (archive → App Store Connect → TestFlight, triggered on main push). Retired in favour of the GitHub Actions pipeline above. Historical product IDs (no longer triggered):
| App | Xcode Cloud product ID |
|---|---|
| Nutri-E | A7AEE8F6 |
| Reward-E | 3B4EECB2 |
| Fast-E | AAFB4E73 |
| Count-E | AEB723D3 |
| Drink-E | 275BD2D2 / 8F9F479E (dup) |
| SupTrackAI (pre-Nutri-E) | 8B960BC7 |
| Cuti-E Admin / Feedback / Super Admin | 2F32A418 / 046A65C8 / 063839D0 (cuti-e/cutie) |
Cleanup notes for the retired setup:
- The Xcode Cloud GitHub App may still be installed account-wide on Stig-Johnny (creates check suites on all repos, incl. non-iOS); auto-merge workflows filter out xcode-cloud checks. Restrict/remove it in GitHub Settings → Integrations.
- Disable any monitor-xcode-cloud.yml workflow on a migrated repo — it alarms on absent Xcode Cloud builds.
- Apps use XcodeGen; the old ci_scripts/ci_post_clone.sh ran xcodegen generate on Xcode Cloud. The GitHub Actions workflow handles project generation + Info.plist itself.