feat(ios): route App Store archives through Xcode Cloud on beta macOS

Add Cloud post-clone setup and docs so store binaries build on Apple's
release hosts; block local archive-app-store on seed OS to avoid ITMS-90111.
This commit is contained in:
tianma-if committed 2026-08-07 19:54:28 +08:00
1 parent 5124ef3539
commit 7ceed4ea16
4 files changed
+197 -2

No files matched your search

+16 -2
View File
@@ -4,6 +4,8 @@ Native SwiftUI client for EdgeEver. This replaces the React Native / Expo iOS ta
**Design document:** [`docs/ios-swift-rewrite.md`](../../docs/ios-swift-rewrite.md)
**App Store binaries on macOS beta:** use **Xcode Cloud (manual)** — [`docs/ios-xcode-cloud.md`](../../docs/ios-xcode-cloud.md).
## Status
UI/interaction is aligned with the Android RN app (`apps/mobile`):
@@ -85,14 +87,26 @@ xcrun simctl launch booted org.edgeever.mobile \
## App Store archive
Use the stable Xcode app (not beta) so App Store Connect accepts the SDK:
### Preferred: Xcode Cloud (manual only)
If the Mac runs **macOS beta**, local archives are rejected by App Store Connect (**ITMS-90111**) even with release Xcode. Day-to-day development stays local; **only store / TestFlight binaries** should use **Xcode Cloud** (25 compute hours/month with Apple Developer Program).
- Setup and workflow: **[`docs/ios-xcode-cloud.md`](../../docs/ios-xcode-cloud.md)**
- Cloud prepare script: `ci_scripts/ci_post_clone.sh` (EditorBundle + `xcodegen`)
- Workflow start condition must stay **Manual** so hours are not burned on every push
### Local archive (release macOS only)
Use the stable Xcode app (not beta) on a **non-beta** host OS:
```sh
cd apps/ios
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer bash Scripts/archive-app-store.sh
```
The script builds the TipTap EditorBundle, regenerates the Xcode project, archives with the App Store distribution profiles, and exports `build/export/EdgeEver.ipa`. Upload with `xcrun altool` / Transporter, then submit the exact build with:
On a beta host the script **exits** unless you set `EDGE_EVER_IOS_ALLOW_BETA_HOST=1` (upload will still likely fail ASC).
The script builds the TipTap EditorBundle, regenerates the Xcode project, archives with the App Store distribution profiles, and exports `build/export/EdgeEver.ipa`. Upload with `xcrun altool` / Transporter (or use Cloud upload), then submit the exact build with:
```sh
cd apps/ios
+16
View File
@@ -16,6 +16,22 @@ if [[ -z "${DEVELOPER_DIR:-}" ]]; then
fi
echo "Using $(xcodebuild -version | tr '\n' ' ')"
# Beta host macOS stamps BuildMachineOSBuild; ASC often rejects as ITMS-90111.
# Prefer Xcode Cloud for store binaries (docs/ios-xcode-cloud.md).
# Heuristic: Apple seed builds usually end with a lowercase letter (e.g. 26A5368g);
# GM/release builds typically end with digits (e.g. 24G90).
HOST_OS_BUILD="$(sw_vers -buildVersion 2>/dev/null || true)"
HOST_OS_VER="$(sw_vers -productVersion 2>/dev/null || true)"
if [[ "$HOST_OS_BUILD" =~ [a-z]$ ]]; then
echo "WARNING: Host macOS ${HOST_OS_VER} (${HOST_OS_BUILD}) looks like a seed/beta build." >&2
echo "WARNING: App Store Connect may reject this IPA with ITMS-90111 even if Xcode is a release build." >&2
echo "WARNING: Use Xcode Cloud (manual Archive workflow) for store uploads — see docs/ios-xcode-cloud.md" >&2
if [[ "${EDGE_EVER_IOS_ALLOW_BETA_HOST:-0}" != "1" ]]; then
echo "Refusing local App Store archive on beta host. Set EDGE_EVER_IOS_ALLOW_BETA_HOST=1 to override." >&2
exit 1
fi
fi
cd "$ROOT"
if [[ "${EDGE_EVER_IOS_SKIP_EDITOR_BUILD:-0}" != "1" ]]; then
+65
View File
@@ -0,0 +1,65 @@
#!/bin/sh
# Xcode Cloud — runs after the repository is cloned, before xcodebuild.
# Keep this lightweight: local day-to-day builds stay on the developer Mac;
# Cloud is only for App Store / TestFlight archives (manual workflow).
#
# Docs: docs/ios-xcode-cloud.md
set -euo pipefail
echo "==> EdgeEver Xcode Cloud post-clone"
# Resolve monorepo paths. CI_PRIMARY_REPOSITORY_PATH is the Git clone root.
REPO_ROOT="${CI_PRIMARY_REPOSITORY_PATH:-}"
if [ -z "$REPO_ROOT" ]; then
# Local dry-run: this file lives at apps/ios/ci_scripts/
REPO_ROOT="$(CDPATH= cd -- "$(dirname "$0")/../../.." && pwd)"
fi
IOS_ROOT="$REPO_ROOT/apps/ios"
if [ ! -d "$IOS_ROOT" ]; then
echo "error: expected apps/ios at $IOS_ROOT" >&2
exit 1
fi
cd "$IOS_ROOT"
echo "IOS_ROOT=$IOS_ROOT"
echo "Xcode: $(xcodebuild -version | tr '\n' ' ')"
# --- Toolchain: bun (EditorSource) + XcodeGen ---
export PATH="${HOME}/.bun/bin:/opt/homebrew/bin:/usr/local/bin:${PATH}"
if ! command -v bun >/dev/null 2>&1; then
echo "==> Installing bun"
curl -fsSL https://bun.sh/install | bash
export PATH="${HOME}/.bun/bin:${PATH}"
fi
command -v bun >/dev/null 2>&1 || {
echo "error: bun install failed; EditorSource build needs bun or npm" >&2
exit 1
}
echo "bun: $(bun --version)"
if ! command -v xcodegen >/dev/null 2>&1; then
echo "==> Installing XcodeGen"
if command -v brew >/dev/null 2>&1; then
brew install xcodegen
else
echo "error: brew not found; cannot install xcodegen" >&2
exit 1
fi
fi
echo "xcodegen: $(xcodegen --version 2>/dev/null || true)"
# --- TipTap EditorBundle + regenerate Xcode project ---
echo "==> Building EditorBundle"
bash "$IOS_ROOT/Scripts/build-editor-bundle.sh"
echo "==> Running xcodegen generate"
xcodegen generate
if [ ! -f "$IOS_ROOT/EdgeEver.xcodeproj/project.pbxproj" ]; then
echo "error: EdgeEver.xcodeproj missing after xcodegen" >&2
exit 1
fi
echo "==> post-clone complete"
+100
View File
@@ -0,0 +1,100 @@
# 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** (manual start) |
Xcode Cloud is billed as **25 compute hours per month** with the Apple Developer Program. Start workflows **manually** so hours go only to real store submissions.
Native store target: **`apps/ios`** (SwiftUI). Expo `apps/mobile` is not the iOS store path.
## One-time setup (Xcode UI)
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).
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`** (or the `apps/ios` workspace root Xcode shows).
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`).
### What the repo already provides
After clone, Xcode Cloud runs:
`apps/ios/ci_scripts/ci_post_clone.sh`
That script:
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`**
You should not need to commit a machine-local `DerivedData` tree. SPM packages (GRDB, Pow) resolve during the Cloud build.
## Routine: submit a build (uses Cloud hours)
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):
```sh
cd apps/ios
APP_STORE_VERSION=X.Y.Z APP_STORE_BUILD_NUMBER=N \
APP_STORE_CONNECT_API_KEY_ID=... \
APP_STORE_CONNECT_API_ISSUER_ID=... \
APP_STORE_CONNECT_API_KEY_P8_BASE64=... \
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
```
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.
## 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 |
## 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