Cloud #50 exported 1.74.0 because project.yml hardcoded MARKETING_VERSION and ci_post_clone regenerated the Xcode project from that YAML. Keep the version in Version.xcconfig and stamp it into the pbxproj before archive.
148 lines
7.5 KiB
Markdown
148 lines
7.5 KiB
Markdown
# iOS App Store builds via Xcode Cloud
|
||
|
||
## Why
|
||
|
||
Local Macs on **macOS beta** stamp binaries with `BuildMachineOSBuild` from the beta OS. App Store Connect then rejects the upload as **ITMS-90111** (often worded as “Unsupported SDK or Xcode version”), even when using a **release** Xcode.
|
||
|
||
**Policy for this repo:**
|
||
|
||
| Work | Where |
|
||
| --- | --- |
|
||
| Day-to-day coding, Simulator, device debug | **Local Mac** (beta OS OK) |
|
||
| Archive / TestFlight / App Store binary | **Xcode Cloud only** (started by store-delivery or a manual control workflow) |
|
||
|
||
Xcode Cloud is billed as **25 compute hours per month** with the Apple Developer Program. Keep the Cloud workflow start condition **Manual** so git pushes do not consume hours; formal Releases start it through the App Store Connect API.
|
||
|
||
Native store target: **`apps/ios`** (SwiftUI). Expo `apps/mobile` is not the iOS store path.
|
||
|
||
## One-time setup
|
||
|
||
### Xcode / App Store Connect
|
||
|
||
1. Open the project:
|
||
```sh
|
||
cd apps/ios
|
||
xcodegen generate # if needed
|
||
open EdgeEver.xcodeproj
|
||
```
|
||
2. **Xcode → Product → Xcode Cloud → Create Workflow…** (or Report navigator → Cloud).
|
||
3. Product: **EdgeEver** (`org.edgeever.mobile`), repository `tianma-if/edgeever`.
|
||
4. When asked for the project path in a monorepo, select **`apps/ios/EdgeEver.xcodeproj`**.
|
||
5. **Workflow settings (important):**
|
||
- **Start condition:** **Manual** only — do **not** start on every git push / PR (saves the 25h quota). Formal Releases start this workflow through the App Store Connect API.
|
||
- **Actions:** **Archive** (scheme `EdgeEver`, platform iOS).
|
||
- **Deployment preparation:** App Store Connect / App Store eligible (or TestFlight Internal Only).
|
||
- **Xcode / macOS:** latest **release** — not a beta toolchain.
|
||
6. Signing: project uses **Automatic** signing so Xcode Cloud can mint Development + Distribution profiles. First run may ask the Account Holder to confirm certificates in App Store Connect.
|
||
7. Grant GitHub access for the primary repo only. Public SPM deps (`GRDB`, `Pow`) do **not** need Connect.
|
||
|
||
### Shared secrets (auto-upload)
|
||
|
||
In **App Store Connect → EdgeEver → Xcode Cloud → Settings → Shared Environment Variables**, add (secret):
|
||
|
||
| Variable | Value |
|
||
| --- | --- |
|
||
| `APP_STORE_CONNECT_API_KEY_ID` | Key ID (e.g. from Users and Access → Integrations → App Store Connect API) |
|
||
| `APP_STORE_CONNECT_API_ISSUER_ID` | Issuer ID |
|
||
| `APP_STORE_CONNECT_API_KEY_P8_BASE64` | Entire `.p8` file contents, base64-encoded |
|
||
|
||
`ci_post_xcodebuild.sh` uses these to run `altool` after the archive so the IPA reaches ASC even if secondary Development/Ad Hoc exports fail.
|
||
|
||
Encode the key:
|
||
|
||
```sh
|
||
base64 -i AuthKey_XXXX.p8 | tr -d '\n'
|
||
```
|
||
|
||
### Build number (source of truth)
|
||
|
||
Xcode Cloud **overwrites** `CFBundleVersion` with its managed counter:
|
||
|
||
**App Store Connect → EdgeEver → Xcode Cloud → Settings → Build Number → Next build number**
|
||
|
||
Rules:
|
||
|
||
1. Next Cloud number must be **greater than** every build already on ASC for this app.
|
||
2. Keep `apps/ios/Config/Version.xcconfig` as the only marketing-version source.
|
||
Do not hardcode `MARKETING_VERSION` in `project.yml`; Cloud runs `xcodegen`
|
||
in `ci_post_clone`, and a stale YAML value would archive the previous App
|
||
Store version. `ci_pre_xcodebuild.sh` copies the xcconfig version into the
|
||
generated pbxproj and stamps Cloud’s `CI_BUILD_NUMBER` into
|
||
`CURRENT_PROJECT_VERSION`.
|
||
3. Before a store run, from a machine with API credentials:
|
||
|
||
```sh
|
||
cd apps/ios
|
||
export APP_STORE_CONNECT_API_KEY_ID=...
|
||
export APP_STORE_CONNECT_API_ISSUER_ID=...
|
||
export APP_STORE_CONNECT_API_KEY_P8_BASE64=... # or install AuthKey_*.p8 under ~/.appstoreconnect/private_keys/
|
||
bash Scripts/ensure-xcode-cloud-build-number.sh
|
||
```
|
||
|
||
Then set the ASC “next build number” UI to the printed `recommended_next_cloud_build` (or higher) if the UI is not already there.
|
||
|
||
## What the repo provides
|
||
|
||
| Path | Role |
|
||
| --- | --- |
|
||
| `scripts/xcode-cloud-control.py` | Start/monitor Cloud and wait until the App Store build is Valid |
|
||
| `apps/ios/ci_scripts/ci_post_clone.sh` | bun + XcodeGen + EditorBundle + `xcodegen generate` |
|
||
| `apps/ios/ci_scripts/ci_pre_xcodebuild.sh` | Stamp `CI_BUILD_NUMBER`; force Automatic signing |
|
||
| `apps/ios/ci_scripts/ci_post_xcodebuild.sh` | Upload App Store IPA via `altool` when secrets are set |
|
||
| `apps/ios/Scripts/upload-app-store-ipa.sh` | Manual upload of `.ipa` or Cloud `app-store.zip` |
|
||
| `apps/ios/Scripts/ensure-xcode-cloud-build-number.sh` | Recommend / sync next build number from ASC history |
|
||
| `apps/ios/EdgeEver.xcodeproj/xcshareddata/xcodecloud/manifest.json` | Cloud product linkage (commit this file) |
|
||
|
||
## Routine: ship a store binary
|
||
|
||
Formal Releases do this automatically when `apps/ios` or the shared editor
|
||
runtime changed: `bun run release` bumps `MARKETING_VERSION`, starts the Manual
|
||
Xcode Cloud Archive workflow, requires the Cloud source's marketing version
|
||
to match the Release tag, waits until the build is **Valid**, and submits App
|
||
Review. The Cloud workflow stays Manual, so Apple rejects starting it on a
|
||
tag that is not in the start condition. `project.yml` must not hardcode
|
||
`MARKETING_VERSION`; Cloud regenerates the Xcode project during `ci_post_clone`.
|
||
|
||
To retry or ship iOS without a new GitHub Release:
|
||
|
||
```sh
|
||
bun run publish:stores -- --release vX.Y.Z --platform ios
|
||
```
|
||
|
||
Pass `--ios-build-number N` only when the IPA is already on App Store Connect.
|
||
|
||
Manual Cloud controls remain available from
|
||
`.github/workflows/ios-xcode-cloud.yml` (`inspect` / `start` / `monitor`). Keep
|
||
Cloud **next build number** above the latest ASC build
|
||
(`ensure-xcode-cloud-build-number.sh`) if a Cloud run stamps a too-low
|
||
`CFBundleVersion`.
|
||
|
||
## Local archive (release macOS only)
|
||
|
||
```sh
|
||
cd apps/ios
|
||
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer bash Scripts/archive-app-store.sh
|
||
bash Scripts/upload-app-store-ipa.sh build/export/EdgeEver.ipa
|
||
```
|
||
|
||
On **macOS beta**, do **not** upload that IPA; use Xcode Cloud instead. `archive-app-store.sh` refuses beta hosts unless `EDGE_EVER_IOS_ALLOW_BETA_HOST=1`.
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Likely cause | Fix |
|
||
| --- | --- | --- |
|
||
| ITMS-90111 after **local** upload | Host macOS is beta (`BuildMachineOSBuild`) | Use Xcode Cloud only for store binaries |
|
||
| Cloud run **FAILED** but `ARCHIVE_EXPORT` / App Store IPA exists | Development/Ad Hoc export lacks profiles | Automatic signing + Cloud managed certs; `ci_post_xcodebuild` still uploads App Store IPA |
|
||
| IPA `CFBundleVersion` is `1` or too low | Cloud “next build number” not advanced | ASC → Xcode Cloud → Build Number; run `ensure-xcode-cloud-build-number.sh` |
|
||
| Build never appears on ASC | Auto-upload secrets missing; or only Dev/Ad Hoc failed without App Store IPA | Set shared env secrets; or manual `upload-app-store-ipa.sh` |
|
||
| Cloud fails in post-clone on bun/xcodegen | Network / Homebrew on Cloud | Re-run; inspect `ci_post_clone` log |
|
||
| Editor blank in app | EditorBundle not built | Confirm `ci_post_clone` log shows bundle build |
|
||
| Hours running out | Workflow not Manual | Disable push/PR start conditions |
|
||
| PrepareBuildForAppStoreConnect fails with the previous live version (for example 1.74.0) | `project.yml` hardcoded `MARKETING_VERSION` and `xcodegen` overwrote `Version.xcconfig` | Keep marketing version only in `Config/Version.xcconfig`; `ci_pre_xcodebuild.sh` must stamp it into the pbxproj |
|
||
|
||
## Related
|
||
|
||
- `apps/ios/README.md` — generate / test / Fastlane submit
|
||
- `apps/ios/Scripts/archive-app-store.sh` — local archive
|
||
- `apps/ios/ci_scripts/*` — Cloud lifecycle hooks
|