diff --git a/apps/ios/README.md b/apps/ios/README.md index 59afe821..cc1ad843 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -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 diff --git a/apps/ios/Scripts/archive-app-store.sh b/apps/ios/Scripts/archive-app-store.sh index 8d01e40f..50ce392f 100755 --- a/apps/ios/Scripts/archive-app-store.sh +++ b/apps/ios/Scripts/archive-app-store.sh @@ -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 diff --git a/apps/ios/ci_scripts/ci_post_clone.sh b/apps/ios/ci_scripts/ci_post_clone.sh new file mode 100755 index 00000000..d6b8c228 --- /dev/null +++ b/apps/ios/ci_scripts/ci_post_clone.sh @@ -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" diff --git a/docs/ios-xcode-cloud.md b/docs/ios-xcode-cloud.md new file mode 100644 index 00000000..93a089e5 --- /dev/null +++ b/docs/ios-xcode-cloud.md @@ -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