|
| 1 | +# Releasing Session iOS |
| 2 | + |
| 3 | +A small suite of scripts under `Scripts/` builds a distribution IPA and submits it to |
| 4 | +TestFlight without opening Xcode. They run locally (a maintainer just runs them) and are |
| 5 | +CI-ready (all credentials come from environment variables). No Fastlane. |
| 6 | + |
| 7 | +## Prerequisites |
| 8 | + |
| 9 | +- Xcode + command-line tools, `xcbeautify`, and the GitHub CLI (`gh`, authenticated). |
| 10 | +- An **App Store Connect API key** (`.p8`) — see below. For cloud signing it must be an |
| 11 | + **Admin Team Key** (an Individual key, or an App Manager key, will not work). |
| 12 | + |
| 13 | +Signing uses **cloud-managed certificates** (the default when a team sets signing up through |
| 14 | +Xcode — Apple holds the distribution private key). With the Admin API key and |
| 15 | +`-allowProvisioningUpdates`, `xcodebuild` cloud-signs the distribution build without any |
| 16 | +local certificate. **You do not need to create or export a `.p12`.** Providing one is only a |
| 17 | +fallback for teams not using cloud-managed signing (see the last section). |
| 18 | + |
| 19 | +## Obtaining credentials |
| 20 | + |
| 21 | +### App Store Connect API key |
| 22 | +Create it in App Store Connect → **Users and Access → Integrations**. Two requirements, |
| 23 | +both confirmed necessary in practice for the cloud-signed release: |
| 24 | + |
| 25 | +- It must be a **Team Key** (the "Team Keys" tab), **not an Individual Key**. An Individual |
| 26 | + key — even one owned by an Admin user — was rejected for cloud signing (export failed with |
| 27 | + `Cloud signing permission error` and xcodebuild fell back to a local Apple ID account). |
| 28 | +- The key must have the **Admin** role. App Manager is not sufficient for cloud signing / |
| 29 | + distribution-certificate operations (same `Cloud signing permission error`). App Manager is |
| 30 | + only enough if you sign with a local `.p12` (see the fallback section). |
| 31 | + |
| 32 | +Then **Download** the `.p8` (only downloadable once). Note the **Key ID** (10 chars) and the |
| 33 | +**Issuer ID** (UUID) shown on that page. |
| 34 | +- Apple: [Creating API keys for the App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api) |
| 35 | +- Apple: [App Store Connect API — Get started](https://developer.apple.com/help/app-store-connect/get-started/app-store-connect-api/) |
| 36 | + |
| 37 | +### Cloud-managed signing certificate |
| 38 | +Nothing to export — it is managed remotely by Apple and used automatically during the |
| 39 | +cloud-signed archive/export. Background: |
| 40 | +- Apple: [Cloud-managed certificates](https://developer.apple.com/help/account/certificates/cloud-managed-certificates/) |
| 41 | +- WWDC21: [Distribute apps in Xcode with cloud signing](https://developer.apple.com/videos/play/wwdc2021/10204/) |
| 42 | + |
| 43 | +## Environment variables |
| 44 | + |
| 45 | +| Variable | Description | |
| 46 | +|---|---| |
| 47 | +| `ASC_KEY_ID` | 10-char App Store Connect API key ID | |
| 48 | +| `ASC_ISSUER_ID` | App Store Connect issuer UUID | |
| 49 | +| `ASC_KEY_P8_BASE64` | base64 of the `.p8` private key | |
| 50 | +| `SKIP_KEYCHAIN=1` | **use cloud-managed signing** — no local certificate/keychain. This is the normal path for this project. | |
| 51 | +| `ASC_DIST_CERT_P12_BASE64` | *(fallback only)* base64 of an Apple Distribution `.p12` (incl. private key) | |
| 52 | +| `ASC_DIST_CERT_PASSWORD` | *(fallback only)* the `.p12` export password | |
| 53 | +| `KEYCHAIN_PASSWORD` | *(fallback only, optional)* temp-keychain password (generated if unset) | |
| 54 | + |
| 55 | +To base64-encode the key: |
| 56 | + |
| 57 | +```sh |
| 58 | +base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy # -> ASC_KEY_P8_BASE64 |
| 59 | +``` |
| 60 | + |
| 61 | +All secrets are written to restricted temp locations and deleted on exit (success, |
| 62 | +failure, or Ctrl-C) by a trap in `Scripts/release_env.sh`. |
| 63 | + |
| 64 | +## How versioning fits the branch model |
| 65 | + |
| 66 | +Version numbers live at the **project level** in `project.pbxproj` (inherited by the app + |
| 67 | +extension targets). A version bump is a normal change that lands on **`dev`** and reaches |
| 68 | +**`master`** through the reviewed `dev → master` merge — the release scripts **never** change |
| 69 | +the version. Bumping is a separate step (`bump_version.sh`), and a release just reads whatever |
| 70 | +version is already on `master` and tags it. |
| 71 | + |
| 72 | +### Bumping the version (on dev) |
| 73 | + |
| 74 | +```sh |
| 75 | +Scripts/bump_version.sh 2.15.4 # optional 2nd arg sets the build number (default +1) |
| 76 | +``` |
| 77 | + |
| 78 | +This branches off `dev`, bumps the project-level `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`, |
| 79 | +pushes, and opens a PR into `dev` for review. Once merged and promoted to `master`, release it. |
| 80 | + |
| 81 | +## Quick start (one command) |
| 82 | + |
| 83 | +```sh |
| 84 | +Scripts/release.sh # reads the version already on master; add a version to sanity-check |
| 85 | +``` |
| 86 | + |
| 87 | +`release.sh` orchestrates the release: it loads credentials from `.env` if present, prompts |
| 88 | +for anything missing (offering to save them), reads the version on `master`, checks the tag, |
| 89 | +prints a plan, and pauses before each outward-facing step. It runs: |
| 90 | + |
| 91 | +1. `prepare_github_release.sh` — create the tag + GitHub draft release on `master` (no version change) |
| 92 | +2. `build_release.sh --attach` — build, sign, export `session-<version>.ipa`, attach to the draft |
| 93 | +3. `testflight_upload.sh` — upload the IPA to TestFlight |
| 94 | + |
| 95 | +**Tag gating:** if `master`'s version has **no** tag yet, it creates the tag + draft and |
| 96 | +proceeds. If a tag for that version **already exists** (i.e. the version hasn't been bumped |
| 97 | +since the last release), it pauses so you can (1) re-check after merging a bump, (2) build and |
| 98 | +submit with the current version anyway, or (3) abort. |
| 99 | + |
| 100 | +Then it prints (and offers to open) the GitHub releases page and App Store Connect for the two |
| 101 | +remaining manual actions: publishing the GitHub release and managing/submitting in TestFlight. |
| 102 | + |
| 103 | +Useful flags: `-y/--yes` (no prompts, for CI), `--allow-existing-version` (build even if the |
| 104 | +tag exists), `--skip-upload`, `--skip-draft`. Env: `RELEASE_REMOTE` (default `origin`), |
| 105 | +`RELEASE_BRANCH` (default `master`). Run `Scripts/release.sh --help` for details. |
| 106 | + |
| 107 | +### Using a `.env` |
| 108 | + |
| 109 | +Put credentials in a `.env` at the repo root (git-ignored — never committed) so you don't |
| 110 | +re-enter them each time. First interactive run offers to create it for you. Format: |
| 111 | + |
| 112 | +```sh |
| 113 | +ASC_KEY_ID="XXXXXXXXXX" |
| 114 | +ASC_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" |
| 115 | +ASC_KEY_P8_BASE64="<base64 of AuthKey_XXXXXXXXXX.p8>" |
| 116 | +SKIP_KEYCHAIN=1 |
| 117 | +``` |
| 118 | + |
| 119 | +## Running the steps individually |
| 120 | + |
| 121 | +The orchestrator just calls these; run them directly if you need finer control. Credentials |
| 122 | +come from the environment or `.env` (`build_release.sh` and `testflight_upload.sh` auto-load |
| 123 | +`.env` via `release_env.sh`; set them explicitly if you're not using a `.env`): |
| 124 | + |
| 125 | +```sh |
| 126 | +export ASC_KEY_ID="XXXXXXXXXX" |
| 127 | +export ASC_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" |
| 128 | +export ASC_KEY_P8_BASE64="$(base64 -i AuthKey_XXXXXXXXXX.p8)" |
| 129 | +export SKIP_KEYCHAIN=1 |
| 130 | + |
| 131 | +Scripts/prepare_github_release.sh 2.15.4 # create tag + draft release on master |
| 132 | +Scripts/build_release.sh 2.15.4 --attach # version defaults to the project's if omitted |
| 133 | +Scripts/testflight_upload.sh 2.15.4 |
| 134 | +gh release edit 2.15.4 --draft=false # publish the GitHub release when ready |
| 135 | +``` |
| 136 | + |
| 137 | +Outputs: |
| 138 | +- `build/Session.xcarchive` — the archive, including `dSYMs/` for crash symbolication. |
| 139 | +- `build/export/session-<version>.ipa` — the signed, App-Store-ready IPA (naming matches |
| 140 | + the historical builds in `~/Projects/Builds`). |
| 141 | + |
| 142 | +## Notes |
| 143 | + |
| 144 | +- **Version numbers** are set at the project level in `project.pbxproj` and inherited by |
| 145 | + the app + extension targets. `bump_version.sh` changes only those project-level values (on |
| 146 | + `dev`); the release scripts never modify them. |
| 147 | +- **dSYMs**: `App_Store_Release` defaults to `DEBUG_INFORMATION_FORMAT = dwarf`; the device |
| 148 | + archive (`build_ci.sh archive-device`) overrides this to `dwarf-with-dsym` so symbols are |
| 149 | + produced and uploaded (`uploadSymbols` in `exportOptions.plist`). |
| 150 | +- **CI**: there is intentionally no Drone deploy pipeline yet (matching Session Android). |
| 151 | + The scripts already read everything from env vars, so wiring them into CI later is just a |
| 152 | + matter of injecting the secrets above. |
| 153 | +- `Scripts/testflight_upload.sh` is decoupled so you can re-upload an existing IPA without |
| 154 | + re-archiving. It uses `xcrun altool`; if Apple removes `altool`, switch it to |
| 155 | + `exportOptions.plist` `destination=upload` (drops the local artifact) or Transporter. |
| 156 | + |
| 157 | +## Fallback: manual `.p12` signing (not needed for this project) |
| 158 | + |
| 159 | +If a team is *not* using cloud-managed signing, provide an Apple Distribution certificate |
| 160 | +instead of setting `SKIP_KEYCHAIN=1`: export the identity from Keychain Access as a `.p12` |
| 161 | +(with its private key), then set `ASC_DIST_CERT_P12_BASE64` (=`base64 -i cert.p12`) and |
| 162 | +`ASC_DIST_CERT_PASSWORD`. `release_env.sh` imports it into a temporary keychain for the build |
| 163 | +and deletes it on exit. This project uses cloud signing, so this path should not be required. |
0 commit comments