fix(ios): harden Xcode Cloud store builds and auto-upload

Use Automatic signing and Cloud lifecycle scripts so App Store IPAs get
correct build numbers and reach ASC even when secondary exports fail.
This commit is contained in:
tianma-if committed 2026-08-07 22:51:36 +08:00
1 parent 7ceed4ea16
commit 73d3bad9a3
13 files changed
+521 -76

No files matched your search

+73 -38
View File
@@ -15,9 +15,9 @@ Xcode Cloud is billed as **25 compute hours per month** with the Apple Developer
Native store target: **`apps/ios`** (SwiftUI). Expo `apps/mobile` is not the iOS store path.
## One-time setup (Xcode UI)
## One-time setup
Do this once on any Mac that can open the project (beta Mac is fine for *configuring* Cloud; Cloud still builds on Apple’s release OS images).
### Xcode / App Store Connect
1. Open the project:
```sh
@@ -27,39 +27,75 @@ Do this once on any Mac that can open the project (beta Mac is fine for *configu
```
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`** (or the `apps/ios` workspace root Xcode shows).
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).
- **Actions:** **Archive** (App Store / TestFlight distribution).
- **Deployment:** App Store Connect (upload the archive).
- **Xcode version:** latest **release** (or RC when Apple requires it) — not a beta toolchain.
6. Signing: prefer **Xcode Cloud managed signing** for Distribution, or ensure the App Store profiles already used by `ExportOptions.plist` / `project.yml` are available to the team. First Cloud run may ask the Account Holder to confirm certificates in App Store Connect.
7. Grant GitHub access if Xcode prompts (read the monorepo so `ci_post_clone` can see `apps/ios` + `EditorSource`).
- **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.
### What the repo already provides
### Shared secrets (auto-upload)
After clone, Xcode Cloud runs:
In **App Store Connect → EdgeEver → Xcode Cloud → Settings → Shared Environment Variables**, add (secret):
`apps/ios/ci_scripts/ci_post_clone.sh`
| 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 |
That script:
`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.
1. Installs **bun** (if missing) and **XcodeGen** (via Homebrew if needed)
2. Builds the TipTap **EditorBundle** (`Scripts/build-editor-bundle.sh`)
3. Runs **`xcodegen generate`**
Encode the key:
You should not need to commit a machine-local `DerivedData` tree. SPM packages (GRDB, Pow) resolve during the Cloud build.
```sh
base64 -i AuthKey_XXXX.p8 | tr -d '\n'
```
## Routine: submit a build (uses Cloud hours)
### Build number (source of truth)
1. Land the code you want on the branch Xcode Cloud tracks (usually `main`).
2. Bump versions if this is a store upload:
- `apps/ios/Config/Version.xcconfig` → `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`
- Keep monorepo release rules in `AGENTS.md` when cutting a formal GitHub Release.
3. In Xcode (or App Store Connect → Xcode Cloud): **Start Build** on the **manual** Archive workflow only.
4. Wait for Archive + upload to succeed.
5. In App Store Connect → TestFlight / version page: select the new build, complete compliance, **Submit for Review**.
6. Optional: submit metadata-only steps with Fastlane from any machine (does not re-archive):
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` → `CURRENT_PROJECT_VERSION` in the same ballpark (the repo stamps Cloud’s `CI_BUILD_NUMBER` into the archive via `ci_pre_xcodebuild.sh`).
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 |
| --- | --- |
| `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
1. Land the code on the branch Cloud tracks (usually `main`).
2. Bump `MARKETING_VERSION` when the user-facing version changes; ensure Cloud **next build number** > latest ASC build (`ensure-xcode-cloud-build-number.sh`).
3. Start the **manual** Archive workflow (Xcode or ASC → Xcode Cloud → Start Build).
4. Wait for Archive. Prefer a green run; if the run is red only because Development/Ad Hoc export failed, check that `ci_post_xcodebuild` still uploaded the App Store IPA (or download `ARCHIVE_EXPORT` and run `upload-app-store-ipa.sh`).
5. In ASC: wait until the build is **Valid**, attach it to the version, **Submit for Review**.
Optional (metadata already on ASC, binary already uploaded):
```sh
cd apps/ios
@@ -70,31 +106,30 @@ You should not need to commit a machine-local `DerivedData` tree. SPM packages (
fastlane ios submit_review
```
(`skip_binary_upload: true` in the lane — binary must already be on ASC from Cloud.)
## Local archive (release macOS only)
`Scripts/archive-app-store.sh` remains for machines on a **non-beta** macOS:
```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. The script prints a warning when it detects a beta host OS build.
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 |
| --- | --- |
| ITMS-90111 after **local** upload | Host macOS is beta (`BuildMachineOSBuild`) |
| Cloud fails in post-clone on bun/xcodegen | Network / Homebrew on Cloud; re-run; check script logs |
| Editor blank in app | EditorBundle not built — confirm `ci_post_clone` log shows bundle build |
| Signing errors on Cloud | Confirm Distribution cert/profile for `org.edgeever.mobile` + share extension; or enable Cloud-managed signing |
| Hours running out | Workflow not Manual — disable push/PR start conditions |
| 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 |
## Related
- `apps/ios/README.md` — generate / test / Fastlane submit
- `apps/ios/Scripts/archive-app-store.sh` — local archive
- `apps/ios/ci_scripts/ci_post_clone.sh` — Cloud prepare step
- `apps/ios/ci_scripts/*` — Cloud lifecycle hooks