This document describes how releases ship today and the target flow once TestFlight upload is automated on CircleCI.
App Store promotion (TestFlight to public App Store) is intentionally manual in both flows.
-
Create the release PR. On a clean
develop, run:./scripts/release.sh
The script creates
release/X.Y.Z, bumpsMARKETING_VERSIONinPlayolaRadio.xcodeproj/project.pbxproj, runsagvtoolto bump the build number, lists the PRs merged since the last release, and opens a release PR targetingmain. -
Merge the release PR using "Create a merge commit". Do NOT squash.
scripts/release.shcomputes the "unreleased PRs" list by walking the merge history ofdevelopsince the last sync-back frommain. Squashing the release PR collapses the merge commits onmaininto a single commit that no longer shares SHAs withdevelop's history. The next run ofrelease.shthen re-lists PRs that have already shipped. Using "Create a merge commit" preserves the first-parent chain and keeps the PR list accurate. -
Auto-tag fires.
.github/workflows/auto-tag-release.ymlruns on push tomain, readsMARKETING_VERSIONandCURRENT_PROJECT_VERSIONfrom the pbxproj, and pushes tagvX.Y.Z-bNif it does not already exist. Each TestFlight build gets a uniquev<version>-b<build>tag so hotfix build bumps trigger a fresh CircleCI upload under the same version. -
Sync-back PR opens.
.github/workflows/sync-main-to-develop.ymlfires on the same push and opens a PR mergingmainback intodevelop. Merge it promptly so the next release diff is accurate. -
Build and upload from the developer laptop. On the machine that holds the production signing auth and secrets:
git checkout main && git pull bundle exec fastlane release_production
The lane runs
ensure_git_status_clean,ensure_git_branch('main'),scan,build_app,upload_to_testflight(falling back to local Apple auth), andsentry_debug_files_upload. -
Promote manually. Once the TestFlight build finishes processing, promote it to the App Store from App Store Connect when ready.
Steps 1 through 4 above are unchanged. Steps 5 and 6 become:
-
CircleCI builds and uploads on tag push. Pushing
vX.Y.Ztriggers arelease_buildjob that decodes secrets from env vars, runsmatchreadonly, runsscan, runsbuild_app, uploads to TestFlight with the App Store Connect API key, and uploads dSYMs to Sentry. No developer laptop is involved. -
Promote manually. Unchanged.
The incremental rollout is:
- PR 2: make the
release_productionFastlane lane CI-runnable. - PR 3: add
scripts/encode-secrets.shandscripts/ci-write-secrets.sh. - PR 4: add a tag-filtered CircleCI job that builds but does not upload.
- PR 5: enable
upload_to_testflightandsentry_debug_files_upload.
For a build-number-only bump on an existing version (for example, to fix a
bug found by TestFlight testers on a shipped version, or to re-upload after
a TestFlight processing failure), on a clean develop run:
./scripts/bump-build.shThe script:
- Verifies you are on
developwith a clean working tree. - Fetches
origin/mainandorigin/develop. - Bumps the build number via
agvtool(version stays the same). - Creates
hotfix/<version>-b<new_build>, commits, and pushes. - Opens a PR targeting
mainwith the PRs merged todevelopsince the last release tag.
Merge the hotfix PR using "Create a merge commit" (same rule as the
release PR — do not squash). Auto-tag then fires with vX.Y.Z-bN and
CircleCI uploads a new TestFlight build under the unchanged version.
If the fix warrants a version bump (not just a build rebuild), use
./scripts/release.sh with the patch option instead.
Once automation is in place, the following env vars must be set in a
CircleCI context (suggested name ios-release) and attached to the
release workflow. Names only; values live in CircleCI.
| Variable | Purpose |
|---|---|
APP_STORE_CONNECT_API_KEY_ID |
ASC API key identifier. |
APP_STORE_CONNECT_API_ISSUER_ID |
ASC issuer UUID. |
APP_STORE_CONNECT_API_KEY_CONTENT |
The .p8 private key for App Store Connect auth. Prefer base64 — set it to base64 -i AuthKey_XXXX.p8 | tr -d '\n'. (The raw .p8 also works, but CircleCI env vars often strip the multi-line PEM's newlines, which corrupts the key and fails the release with OpenSSL invalid curve name. The Fastfile auto-detects base64 vs raw via the -----BEGIN header.) |
MATCH_PASSWORD |
Passphrase that decrypts the fastlane-match certificate repo. |
MATCH_GIT_PRIVATE_KEY |
SSH key with read access to the fastlane-match-certs-and-profiles repo. |
SECRETS_XCCONFIG_B64 |
Base64 of PlayolaRadio/Config/Secrets.xcconfig. |
SECRETS_LOCAL_XCCONFIG_B64 |
Base64 of PlayolaRadio/Config/Secrets-Local.xcconfig. |
SECRETS_DEVELOPMENT_XCCONFIG_B64 |
Base64 of PlayolaRadio/Config/Secrets-Development.xcconfig. |
SECRETS_STAGING_XCCONFIG_B64 |
Base64 of PlayolaRadio/Config/Secrets-Staging.xcconfig. |
SENTRY_AUTH_TOKEN |
Auth token used by sentry_debug_files_upload to push dSYMs. |
To rotate the xcconfig secrets, update the source of truth under
~/playola/playola-radio-ios/PlayolaRadio/Config/ on the release machine,
then run ./scripts/encode-secrets.sh to print VAR=<base64> lines for
each xcconfig. Paste each line's value into the matching CircleCI context
variable. CircleCI decodes them at the start of the release job by running
./scripts/ci-write-secrets.sh, which reads the four SECRETS_*_B64 env
vars and writes the xcconfig files back into PlayolaRadio/Config/.
For the ASC API key and Match passphrase, update the CircleCI context variable directly; there is no file to regenerate. After any rotation, re-run the release workflow on a test tag to confirm CI can still sign and upload.
Other developers can refresh their local xcconfig files from the release
machine's ~/playola/playola-radio-ios/PlayolaRadio/Config/ by running
./scripts/setup-secrets.sh. That script skips files that already exist,
so delete the stale xcconfig under PlayolaRadio/Config/ first when
pulling a rotated value.