How do you design a release pipeline: hermetic builds, secrets, caching, artefact promotion and the order of stages?
Predict first
A team rebuilds the app separately for QA, for staging and for the store, each time from the latest commit. What is wrong?
// A: nothing, each build is fresh
// B: the artefact that was tested is not the one that ships; build once and promote it
// C: the builds are too fast
- A
- B
- C
Mental model. A good pipeline is a factory line with one product moving through inspection stations. You never rebuild the car at the end; you check the one you built.
Design the pipeline around one artefact. A commit triggers a build that produces a signed archive once; the same archive is then tested, uploaded to TestFlight and, if approved, submitted, so what testers validated is exactly what ships (rebuilding per stage means you tested something else). Order stages by cost and signal: fast checks first (lint, build, unit tests, snapshot tests), then slower ones (integration and UI tests, performance and accessibility audits, Q453), then archive, sign, upload and notify. Run the cheap stages on every pull request and the expensive ones on merge or nightly (Q373 for speed). Make builds hermetic: pin the Xcode version and runner image, lock dependencies (a resolved package file, a lockfile), build from a clean checkout, and take everything environment-specific (endpoints, flags, the build number) from configuration, not from the machine.
Secrets (the signing key, the App Store Connect API key, service tokens) live in the CI provider’s secret store or a vault, are injected only into the jobs that need them, are never printed in logs, and are scoped narrowly and rotated; pull-request builds from forks must not receive them. Cache what is slow and safe to reuse (resolved packages, derived data for unchanged modules) with keys that include the Xcode version and a hash of the lockfile, and treat a cache as an optimisation that can be dropped, never a source of truth. Make failures diagnosable: keep the result bundle, logs and the .xcarchive with dSYMs as build artefacts, and upload dSYMs to the crash reporter as a pipeline step that fails the release if it fails (Q461). Add guardrails to the release stage (tests green, size and launch budgets within limits, version and build valid, required approvals), a protected branch for release candidates and an audit trail of who released what. Test the pipeline itself: keep a lane that runs end to end against a test app regularly, so a signing change or an Xcode update is found on a quiet Tuesday and not on release day.
Promoting one signed artefact is what makes “we tested this” true for the thing users download.
Common traps
- Rebuilding the app in each stage. The binary that was tested is not the binary that ships.
- Exposing signing secrets to pull requests from forks. Anyone who can open a PR could extract your signing key.
They might follow up with
- What do you pin? Xcode version, runner image, dependency versions and tool versions, so a build is reproducible months later.
- Why keep artefacts? To reproduce, symbolicate and roll forward from any shipped build.
Related questions
- 25.2.1 What are the main ways to run iOS CI/CD (Xcode Cloud, GitHub Actions, Fastlane, others), and what do they all…
- 25.2.2 How do marketing versions and build numbers work, how should CI generate them, and which rules does App Store…
- 25.4.2 How do you run a release train for many apps: cadence, branch cuts, ownership, go/no-go criteria, hotfix…
- 16.6.1 A CI pipeline takes hours on a large modular app. How do you analyse and cut it: affected builds, caching…
- 22.7.1 Why do tests flake, how do you find and fix them, and what makes a CI test run hermetic?
- 22.7.2 How do you design a quality system for a large team: test tiers, ownership, coverage, flake budgets and…
- 23.5.1 How do you read a crash report, what do the exception types and Swift runtime messages mean, and how does…
← How do marketing versions and build numbers work, how should CI…