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:
1 parent
7ceed4ea16
commit
73d3bad9a3
13 files changed
+521
-76
No files matched your search
@@ -1,5 +1,11 @@
|
||||
// Bump MARKETING_VERSION with monorepo release X.Y.Z when shipping App Store builds.
|
||||
// CURRENT_PROJECT_VERSION must increase for every store upload (TestFlight / App Store).
|
||||
// Build 30+ marks the native SwiftUI client (RN EAS builds ended at 29).
|
||||
//
|
||||
// Xcode Cloud overwrites CFBundleVersion with its managed "next build number"
|
||||
// (App Store Connect → Xcode Cloud → Settings → Build Number). Keep that value
|
||||
// ≥ this file and ≥ the latest ASC build. ci_pre_xcodebuild.sh also stamps
|
||||
// CI_BUILD_NUMBER into the archive when present.
|
||||
// Last ASC store build: 32 (2026-08-07, Cloud + altool). Floor for next Cloud run: 33+.
|
||||
MARKETING_VERSION = 1.12.6
|
||||
CURRENT_PROJECT_VERSION = 31
|
||||
CURRENT_PROJECT_VERSION = 33
|
||||
@@ -524,9 +524,11 @@
|
||||
};
|
||||
7B4AF236874DD8459683F02B = {
|
||||
DevelopmentTeam = 9KA3NM38B6;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
E55B18ED6114D23320B85FA0 = {
|
||||
DevelopmentTeam = 9KA3NM38B6;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -667,15 +669,13 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 9KA3NM38B6;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.edgeever.mobile.expo-sharing-extension";
|
||||
PRODUCT_NAME = ShareExtension;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "*[expo] org.edgeever.mobile.expo-sharing-extension AppStore 2026-08-01T04:09:04.014Z";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -718,7 +718,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 31;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
DEVELOPMENT_TEAM = 9KA3NM38B6;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -748,8 +748,8 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = EdgeEver/Resources/EdgeEver.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 9KA3NM38B6;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
@@ -760,7 +760,6 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.edgeever.mobile;
|
||||
PRODUCT_NAME = EdgeEver;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "*[expo] org.edgeever.mobile AppStore 2026-08-01T04:08:38.265Z";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -803,7 +802,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 31;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
DEVELOPMENT_TEAM = 9KA3NM38B6;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -838,6 +837,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 9KA3NM38B6;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
@@ -891,6 +891,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = EdgeEver/Resources/EdgeEver.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 9KA3NM38B6;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id" : "2b362ad9-6c95-487d-ab5d-1789d9155dc7",
|
||||
"targets" : [
|
||||
{
|
||||
"id" : "54d8f7fd-92fc-4182-bb3f-83b8d3a89163",
|
||||
"name" : "EdgeEver"
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
-3
@@ -83,7 +83,9 @@ xcrun simctl launch booted org.edgeever.mobile \
|
||||
| Field | File | Notes |
|
||||
| --- | --- | --- |
|
||||
| `MARKETING_VERSION` | `Config/Version.xcconfig` | Align with monorepo release `X.Y.Z` on store submissions |
|
||||
| `CURRENT_PROJECT_VERSION` | `Config/Version.xcconfig` | Monotonic build number for every TestFlight / App Store upload |
|
||||
| `CURRENT_PROJECT_VERSION` | `Config/Version.xcconfig` | Local floor for build numbers |
|
||||
|
||||
**Xcode Cloud** stamps `CFBundleVersion` from App Store Connect → Xcode Cloud → **Build Number** (not only this file). Before a Cloud store run, set that counter **above** the latest ASC build (`Scripts/ensure-xcode-cloud-build-number.sh` prints the recommended value).
|
||||
|
||||
## App Store archive
|
||||
|
||||
@@ -92,8 +94,18 @@ xcrun simctl launch booted org.edgeever.mobile \
|
||||
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`)
|
||||
- Cloud hooks: `ci_scripts/ci_post_clone.sh` → `ci_pre_xcodebuild.sh` → `ci_post_xcodebuild.sh`
|
||||
- Workflow start condition must stay **Manual** so hours are not burned on every push
|
||||
- Put ASC API secrets in Xcode Cloud **shared environment variables** so `ci_post_xcodebuild.sh` auto-uploads the App Store IPA
|
||||
|
||||
```sh
|
||||
# Before Start Build (optional but recommended)
|
||||
cd apps/ios
|
||||
bash Scripts/ensure-xcode-cloud-build-number.sh
|
||||
|
||||
# If Cloud produced ARCHIVE_EXPORT but ASC has no build:
|
||||
bash Scripts/upload-app-store-ipa.sh /path/to/EdgeEver-*-app-store.zip
|
||||
```
|
||||
|
||||
### Local archive (release macOS only)
|
||||
|
||||
@@ -102,11 +114,12 @@ 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
|
||||
bash Scripts/upload-app-store-ipa.sh build/export/EdgeEver.ipa
|
||||
```
|
||||
|
||||
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:
|
||||
Signing in the Xcode project is **Automatic** (Cloud-friendly). Local export still uses `ExportOptions.plist` with App Store profiles. Then submit the exact build with:
|
||||
|
||||
```sh
|
||||
cd apps/ios
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
# Ensure App Store Connect "Xcode Cloud next build number" is high enough
|
||||
# before starting a Cloud archive. Cloud overwrites CFBundleVersion with that
|
||||
# value; if it is lower than the latest ASC build, the IPA cannot be used.
|
||||
#
|
||||
# Required env:
|
||||
# APP_STORE_CONNECT_API_KEY_ID
|
||||
# APP_STORE_CONNECT_API_ISSUER_ID
|
||||
# APP_STORE_CONNECT_API_KEY_P8_BASE64 OR ~/.appstoreconnect/private_keys/AuthKey_<id>.p8
|
||||
#
|
||||
# Optional:
|
||||
# APP_STORE_CONNECT_APP_ID (default: 6792625631 EdgeEver)
|
||||
# EDGE_EVER_IOS_MIN_BUILD (override floor; else Config/Version.xcconfig)
|
||||
# EDGE_EVER_IOS_APPLY=1 (default) also rewrite Version.xcconfig to recommended next
|
||||
#
|
||||
# Usage (from apps/ios):
|
||||
# bash Scripts/ensure-xcode-cloud-build-number.sh
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_ID="${APP_STORE_CONNECT_APP_ID:-6792625631}"
|
||||
KEY_ID="${APP_STORE_CONNECT_API_KEY_ID:?APP_STORE_CONNECT_API_KEY_ID required}"
|
||||
ISSUER="${APP_STORE_CONNECT_API_ISSUER_ID:?APP_STORE_CONNECT_API_ISSUER_ID required}"
|
||||
APPLY="${EDGE_EVER_IOS_APPLY:-1}"
|
||||
|
||||
KEY_DIR="${HOME}/.appstoreconnect/private_keys"
|
||||
mkdir -p "$KEY_DIR"
|
||||
KEY_FILE="${KEY_DIR}/AuthKey_${KEY_ID}.p8"
|
||||
if [[ ! -f "$KEY_FILE" ]]; then
|
||||
P8_B64="${APP_STORE_CONNECT_API_KEY_P8_BASE64:?Need $KEY_FILE or APP_STORE_CONNECT_API_KEY_P8_BASE64}"
|
||||
printf '%s' "$P8_B64" | base64 -d >"$KEY_FILE"
|
||||
chmod 600 "$KEY_FILE"
|
||||
fi
|
||||
|
||||
FLOOR="${EDGE_EVER_IOS_MIN_BUILD:-}"
|
||||
if [[ -z "$FLOOR" && -f "$ROOT/Config/Version.xcconfig" ]]; then
|
||||
FLOOR="$(sed -nE 's/^CURRENT_PROJECT_VERSION[[:space:]]*=[[:space:]]*([0-9]+).*/\1/p' "$ROOT/Config/Version.xcconfig" | head -1)"
|
||||
fi
|
||||
FLOOR="${FLOOR:-1}"
|
||||
|
||||
export EDGE_EVER_ASC_KEY_ID="$KEY_ID"
|
||||
export EDGE_EVER_ASC_ISSUER="$ISSUER"
|
||||
export EDGE_EVER_ASC_KEY_FILE="$KEY_FILE"
|
||||
export EDGE_EVER_ASC_APP_ID="$APP_ID"
|
||||
export EDGE_EVER_ASC_FLOOR="$FLOOR"
|
||||
export EDGE_EVER_ASC_APPLY="$APPLY"
|
||||
export EDGE_EVER_IOS_ROOT="$ROOT"
|
||||
|
||||
python3 <<'PY'
|
||||
import json, os, pathlib, re, sys, time, urllib.request, urllib.error
|
||||
|
||||
def ensure_jwt():
|
||||
try:
|
||||
import jwt # type: ignore
|
||||
return jwt
|
||||
except ImportError:
|
||||
import subprocess
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "pip", "install", "PyJWT", "cryptography", "-q"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
import jwt # type: ignore
|
||||
return jwt
|
||||
|
||||
jwt = ensure_jwt()
|
||||
key_id = os.environ["EDGE_EVER_ASC_KEY_ID"]
|
||||
issuer = os.environ["EDGE_EVER_ASC_ISSUER"]
|
||||
key_file = pathlib.Path(os.environ["EDGE_EVER_ASC_KEY_FILE"])
|
||||
app_id = os.environ["EDGE_EVER_ASC_APP_ID"]
|
||||
floor = int(os.environ["EDGE_EVER_ASC_FLOOR"])
|
||||
apply = os.environ.get("EDGE_EVER_ASC_APPLY", "1") == "1"
|
||||
ios_root = pathlib.Path(os.environ["EDGE_EVER_IOS_ROOT"])
|
||||
|
||||
private_key = key_file.read_text()
|
||||
now = int(time.time())
|
||||
token = jwt.encode(
|
||||
{"iss": issuer, "iat": now, "exp": now + 20 * 60, "aud": "appstoreconnect-v1"},
|
||||
private_key,
|
||||
algorithm="ES256",
|
||||
headers={"kid": key_id, "typ": "JWT"},
|
||||
)
|
||||
if isinstance(token, bytes):
|
||||
token = token.decode()
|
||||
|
||||
|
||||
def api(method, path, body=None):
|
||||
url = "https://api.appstoreconnect.apple.com" + path
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
req = urllib.request.Request(url, data=data, method=method)
|
||||
req.add_header("Authorization", f"Bearer {token}")
|
||||
if body is not None:
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
raw = resp.read().decode()
|
||||
return resp.status, json.loads(raw) if raw else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
err = e.read().decode()
|
||||
try:
|
||||
err = json.loads(err)
|
||||
except Exception:
|
||||
pass
|
||||
raise SystemExit(f"API {method} {path} -> {e.code}: {err}")
|
||||
|
||||
|
||||
st, builds = api(
|
||||
"GET",
|
||||
f"/v1/builds?filter[app]={app_id}&sort=-uploadedDate&limit=30&fields[builds]=version,processingState",
|
||||
)
|
||||
latest = 0
|
||||
for d in builds.get("data", []):
|
||||
try:
|
||||
latest = max(latest, int(str(d["attributes"]["version"])))
|
||||
except (KeyError, TypeError, ValueError):
|
||||
pass
|
||||
|
||||
desired = max(floor, latest + 1)
|
||||
print(f"latest_asc_build={latest}")
|
||||
print(f"local_floor={floor}")
|
||||
print(f"recommended_next_cloud_build={desired}")
|
||||
print(
|
||||
"Set App Store Connect → EdgeEver → Xcode Cloud → Settings → Build Number → "
|
||||
f"next = {desired} (or higher) before Start Build."
|
||||
)
|
||||
|
||||
# Best-effort: list ciProducts for this app (manual UI remains source of truth for next number)
|
||||
st, products = api("GET", f"/v1/apps/{app_id}/ciProducts?limit=10")
|
||||
if isinstance(products, dict):
|
||||
for p in products.get("data", []):
|
||||
attrs = p.get("attributes") or {}
|
||||
print(f"ciProduct id={p.get('id')} name={attrs.get('name')} productType={attrs.get('productType')}")
|
||||
|
||||
if apply:
|
||||
version_file = ios_root / "Config" / "Version.xcconfig"
|
||||
if version_file.is_file():
|
||||
text = version_file.read_text()
|
||||
new_text, n = re.subn(
|
||||
r"^(CURRENT_PROJECT_VERSION\s*=\s*)\d+",
|
||||
rf"\g<1>{desired}",
|
||||
text,
|
||||
count=1,
|
||||
flags=re.M,
|
||||
)
|
||||
if n and new_text != text:
|
||||
version_file.write_text(new_text)
|
||||
print(f"Updated {version_file} CURRENT_PROJECT_VERSION={desired}")
|
||||
else:
|
||||
print(f"Version.xcconfig already at CURRENT_PROJECT_VERSION>={desired} or unchanged")
|
||||
yml = ios_root / "project.yml"
|
||||
if yml.is_file():
|
||||
ytext = yml.read_text()
|
||||
ynew, yn = re.subn(
|
||||
r"(CURRENT_PROJECT_VERSION:\s*)\d+",
|
||||
rf"\g<1>{desired}",
|
||||
ytext,
|
||||
count=1,
|
||||
)
|
||||
if yn and ynew != ytext:
|
||||
yml.write_text(ynew)
|
||||
print(f"Updated {yml} CURRENT_PROJECT_VERSION={desired}")
|
||||
|
||||
print("OK")
|
||||
print(f"NEXT_BUILD={desired}")
|
||||
PY
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# Upload an already-exported App Store IPA to App Store Connect (altool).
|
||||
# Use after Xcode Cloud ARCHIVE_EXPORT download, or after local archive-app-store.sh.
|
||||
#
|
||||
# Required env:
|
||||
# APP_STORE_CONNECT_API_KEY_ID
|
||||
# APP_STORE_CONNECT_API_ISSUER_ID
|
||||
# APP_STORE_CONNECT_API_KEY_P8_BASE64 OR key file at:
|
||||
# ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
|
||||
#
|
||||
# Usage:
|
||||
# bash Scripts/upload-app-store-ipa.sh /path/to/EdgeEver.ipa
|
||||
# bash Scripts/upload-app-store-ipa.sh /path/to/app-store.zip # zip from Cloud
|
||||
set -euo pipefail
|
||||
|
||||
INPUT="${1:-}"
|
||||
if [[ -z "$INPUT" || ! -e "$INPUT" ]]; then
|
||||
echo "Usage: $0 <app.ipa|app-store-export.zip>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
KEY_ID="${APP_STORE_CONNECT_API_KEY_ID:?APP_STORE_CONNECT_API_KEY_ID required}"
|
||||
ISSUER="${APP_STORE_CONNECT_API_ISSUER_ID:?APP_STORE_CONNECT_API_ISSUER_ID required}"
|
||||
|
||||
KEY_DIR="${HOME}/.appstoreconnect/private_keys"
|
||||
mkdir -p "$KEY_DIR"
|
||||
KEY_FILE="${KEY_DIR}/AuthKey_${KEY_ID}.p8"
|
||||
if [[ ! -f "$KEY_FILE" ]]; then
|
||||
P8_B64="${APP_STORE_CONNECT_API_KEY_P8_BASE64:?Need API key file at $KEY_FILE or APP_STORE_CONNECT_API_KEY_P8_BASE64}"
|
||||
printf '%s' "$P8_B64" | base64 -d >"$KEY_FILE"
|
||||
chmod 600 "$KEY_FILE"
|
||||
fi
|
||||
|
||||
IPA=""
|
||||
WORKDIR=""
|
||||
cleanup() {
|
||||
if [[ -n "$WORKDIR" && -d "$WORKDIR" ]]; then
|
||||
rm -rf "$WORKDIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ "$INPUT" == *.ipa ]]; then
|
||||
IPA="$INPUT"
|
||||
elif [[ "$INPUT" == *.zip ]]; then
|
||||
WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/edgeever-ipa.XXXXXX")"
|
||||
unzip -q -o "$INPUT" -d "$WORKDIR"
|
||||
IPA="$(find "$WORKDIR" -name '*.ipa' | head -1 || true)"
|
||||
if [[ -z "$IPA" ]]; then
|
||||
echo "No .ipa inside $INPUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Expected .ipa or .zip, got: $INPUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${DEVELOPER_DIR:-}" && -d /Applications/Xcode.app/Contents/Developer ]]; then
|
||||
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
fi
|
||||
|
||||
echo "IPA=$IPA ($(wc -c <"$IPA" | tr -d ' ') bytes)"
|
||||
# Show version stamp for operator confirmation
|
||||
TMP_UNPACK="$(mktemp -d "${TMPDIR:-/tmp}/edgeever-ipa-info.XXXXXX")"
|
||||
unzip -q -o "$IPA" -d "$TMP_UNPACK" || true
|
||||
PLIST="$(find "$TMP_UNPACK" -path '*/Payload/*.app/Info.plist' | head -1 || true)"
|
||||
if [[ -n "$PLIST" ]]; then
|
||||
echo "CFBundleShortVersionString=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$PLIST" 2>/dev/null || true)"
|
||||
echo "CFBundleVersion=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$PLIST" 2>/dev/null || true)"
|
||||
echo "BuildMachineOSBuild=$(/usr/libexec/PlistBuddy -c 'Print :BuildMachineOSBuild' "$PLIST" 2>/dev/null || true)"
|
||||
fi
|
||||
rm -rf "$TMP_UNPACK"
|
||||
|
||||
echo "Uploading..."
|
||||
xcrun altool --upload-app --type ios --file "$IPA" --apiKey "$KEY_ID" --apiIssuer "$ISSUER"
|
||||
echo "UPLOAD SUCCEEDED"
|
||||
@@ -3,6 +3,7 @@
|
||||
# Keep this lightweight: local day-to-day builds stay on the developer Mac;
|
||||
# Cloud is only for App Store / TestFlight archives (manual workflow).
|
||||
#
|
||||
# Lifecycle: ci_post_clone → ci_pre_xcodebuild → xcodebuild → ci_post_xcodebuild
|
||||
# Docs: docs/ios-xcode-cloud.md
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/bin/sh
|
||||
# Xcode Cloud — runs after xcodebuild (success or failure).
|
||||
# Upload App Store IPA when present so Development/Ad Hoc export failures do not
|
||||
# leave a store-ready binary stranded without an ASC upload.
|
||||
#
|
||||
# Requires Xcode Cloud shared environment variables (secret):
|
||||
# APP_STORE_CONNECT_API_KEY_ID
|
||||
# APP_STORE_CONNECT_API_ISSUER_ID
|
||||
# APP_STORE_CONNECT_API_KEY_P8_BASE64 (entire .p8 file, base64)
|
||||
#
|
||||
# Docs: docs/ios-xcode-cloud.md
|
||||
set -euo pipefail
|
||||
|
||||
echo "==> EdgeEver Xcode Cloud post-xcodebuild"
|
||||
echo "CI_XCODEBUILD_EXIT_CODE=${CI_XCODEBUILD_EXIT_CODE:-unset}"
|
||||
echo "CI_BUILD_NUMBER=${CI_BUILD_NUMBER:-unset}"
|
||||
echo "CI_ARCHIVE_PATH=${CI_ARCHIVE_PATH:-unset}"
|
||||
echo "CI_APP_STORE_SIGNED_APP_PATH=${CI_APP_STORE_SIGNED_APP_PATH:-unset}"
|
||||
|
||||
# Locate signed App Store product (.ipa or .app under export)
|
||||
IPA=""
|
||||
if [ -n "${CI_APP_STORE_SIGNED_APP_PATH:-}" ] && [ -e "$CI_APP_STORE_SIGNED_APP_PATH" ]; then
|
||||
if [ -f "$CI_APP_STORE_SIGNED_APP_PATH" ] && echo "$CI_APP_STORE_SIGNED_APP_PATH" | grep -qi '\.ipa$'; then
|
||||
IPA="$CI_APP_STORE_SIGNED_APP_PATH"
|
||||
elif [ -d "$CI_APP_STORE_SIGNED_APP_PATH" ]; then
|
||||
# Sometimes this points at .app; search sibling/export dirs for ipa
|
||||
PARENT="$(dirname "$CI_APP_STORE_SIGNED_APP_PATH")"
|
||||
IPA="$(find "$PARENT" -maxdepth 3 -name '*.ipa' 2>/dev/null | head -1 || true)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$IPA" ]; then
|
||||
# Common Cloud workspace layouts after archive+export
|
||||
for root in \
|
||||
"${CI_PRIMARY_REPOSITORY_PATH:-}" \
|
||||
"${CI_WORKSPACE:-}" \
|
||||
/Volumes/workspace \
|
||||
/Volumes/workspace/repository
|
||||
do
|
||||
[ -n "$root" ] && [ -d "$root" ] || continue
|
||||
# pipefail-safe: find may return 1 when empty
|
||||
found="$(find "$root" -name '*.ipa' 2>/dev/null | head -1)" || true
|
||||
if [ -n "${found:-}" ]; then
|
||||
IPA="$found"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$IPA" ] || [ ! -f "$IPA" ]; then
|
||||
echo "No App Store IPA found — skip upload (archive/export may have failed earlier)."
|
||||
# Do not fail the script: preserve original xcodebuild outcome.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found IPA: $IPA ($(wc -c < "$IPA" | tr -d ' ') bytes)"
|
||||
|
||||
KEY_ID="${APP_STORE_CONNECT_API_KEY_ID:-}"
|
||||
ISSUER="${APP_STORE_CONNECT_API_ISSUER_ID:-}"
|
||||
P8_B64="${APP_STORE_CONNECT_API_KEY_P8_BASE64:-}"
|
||||
|
||||
if [ -z "$KEY_ID" ] || [ -z "$ISSUER" ] || [ -z "$P8_B64" ]; then
|
||||
echo "ASC API secrets not set in Xcode Cloud env — skip auto-upload."
|
||||
echo "Set APP_STORE_CONNECT_API_KEY_ID / ISSUER_ID / API_KEY_P8_BASE64 (shared env)."
|
||||
echo "Fallback: download ARCHIVE_EXPORT from the build and run Scripts/upload-app-store-ipa.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Install API key where altool expects it
|
||||
KEY_DIR="${HOME}/.appstoreconnect/private_keys"
|
||||
mkdir -p "$KEY_DIR"
|
||||
KEY_FILE="${KEY_DIR}/AuthKey_${KEY_ID}.p8"
|
||||
printf '%s' "$P8_B64" | base64 -d >"$KEY_FILE"
|
||||
chmod 600 "$KEY_FILE"
|
||||
echo "Installed API key AuthKey_${KEY_ID}.p8"
|
||||
|
||||
# Prefer non-beta Xcode for delivery tools when both exist
|
||||
if [ -z "${DEVELOPER_DIR:-}" ] && [ -d /Applications/Xcode.app/Contents/Developer ]; then
|
||||
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
fi
|
||||
|
||||
echo "Uploading IPA to App Store Connect via altool..."
|
||||
if xcrun altool --upload-app --type ios --file "$IPA" --apiKey "$KEY_ID" --apiIssuer "$ISSUER"; then
|
||||
echo "UPLOAD SUCCEEDED"
|
||||
else
|
||||
echo "error: altool upload failed" >&2
|
||||
# Fail the Cloud action so the dashboard is red when the store binary is not delivered.
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> post-xcodebuild complete"
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
# Xcode Cloud — runs before xcodebuild (after ci_post_clone).
|
||||
# Align CFBundleVersion with Cloud's managed build number and keep signing Automatic.
|
||||
#
|
||||
# Docs: docs/ios-xcode-cloud.md
|
||||
set -euo pipefail
|
||||
|
||||
echo "==> EdgeEver Xcode Cloud pre-xcodebuild"
|
||||
|
||||
REPO_ROOT="${CI_PRIMARY_REPOSITORY_PATH:-}"
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
REPO_ROOT="$(CDPATH= cd -- "$(dirname "$0")/../../.." && pwd)"
|
||||
fi
|
||||
IOS_ROOT="$REPO_ROOT/apps/ios"
|
||||
cd "$IOS_ROOT"
|
||||
|
||||
# Cloud stamps CFBundleVersion from App Store Connect "next build number".
|
||||
# Mirror that into Version.xcconfig so the archive and any tooling agree.
|
||||
if [ -n "${CI_BUILD_NUMBER:-}" ]; then
|
||||
echo "CI_BUILD_NUMBER=$CI_BUILD_NUMBER"
|
||||
if [ -f Config/Version.xcconfig ]; then
|
||||
# portable in-place edit (macOS sed)
|
||||
sed -i '' -E "s/^(CURRENT_PROJECT_VERSION[[:space:]]*=[[:space:]]*).*/\\1${CI_BUILD_NUMBER}/" Config/Version.xcconfig
|
||||
echo "Stamped CURRENT_PROJECT_VERSION=${CI_BUILD_NUMBER} into Config/Version.xcconfig"
|
||||
grep -E '^(MARKETING_VERSION|CURRENT_PROJECT_VERSION)' Config/Version.xcconfig || true
|
||||
fi
|
||||
# Also inject into the generated project settings if present
|
||||
if [ -f EdgeEver.xcodeproj/project.pbxproj ]; then
|
||||
/usr/libexec/PlistBuddy -c "Print" EdgeEver.xcodeproj/project.pbxproj >/dev/null 2>&1 || true
|
||||
# pbxproj is not a plist; use sed for CURRENT_PROJECT_VERSION assignments
|
||||
sed -i '' -E "s/(CURRENT_PROJECT_VERSION = )[0-9]+;/\\1${CI_BUILD_NUMBER};/g" EdgeEver.xcodeproj/project.pbxproj
|
||||
echo "Stamped CURRENT_PROJECT_VERSION in project.pbxproj"
|
||||
fi
|
||||
else
|
||||
echo "CI_BUILD_NUMBER unset (local dry-run); leaving Version.xcconfig unchanged"
|
||||
fi
|
||||
|
||||
# Defensive: ensure Automatic signing so Cloud can create Dev + Distribution profiles.
|
||||
# (project.yml already prefers Automatic; this repairs accidental Manual settings.)
|
||||
if [ -f EdgeEver.xcodeproj/project.pbxproj ]; then
|
||||
sed -i '' 's/CODE_SIGN_STYLE = Manual;/CODE_SIGN_STYLE = Automatic;/g' EdgeEver.xcodeproj/project.pbxproj
|
||||
# Drop fixed profile specifiers that force App Store-only profiles (break Dev/Ad Hoc export)
|
||||
sed -i '' '/PROVISIONING_PROFILE_SPECIFIER/d' EdgeEver.xcodeproj/project.pbxproj
|
||||
echo "Ensured CODE_SIGN_STYLE=Automatic and cleared PROVISIONING_PROFILE_SPECIFIER"
|
||||
fi
|
||||
|
||||
echo "==> pre-xcodebuild complete"
|
||||
+9
-13
@@ -20,10 +20,11 @@ settings:
|
||||
TARGETED_DEVICE_FAMILY: "1"
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: false
|
||||
IPHONEOS_DEPLOYMENT_TARGET: "17.0"
|
||||
CURRENT_PROJECT_VERSION: 31
|
||||
CURRENT_PROJECT_VERSION: 33
|
||||
MARKETING_VERSION: 1.12.6
|
||||
# Team enables Automatic signing for local Debug/simulator; App Store
|
||||
# archives override to Manual + distribution profiles in archive-app-store.sh.
|
||||
# Automatic signing: required for Xcode Cloud (managed certs/profiles for
|
||||
# App Store + Development exports). Local ExportOptions.plist still pins
|
||||
# App Store profiles for Scripts/archive-app-store.sh export.
|
||||
DEVELOPMENT_TEAM: 9KA3NM38B6
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
configs:
|
||||
@@ -66,12 +67,11 @@ targets:
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
||||
SWIFT_STRICT_CONCURRENCY: complete
|
||||
ENABLE_TESTABILITY: true
|
||||
# Always Automatic: Xcode Cloud needs managed Development + Distribution
|
||||
# profiles. Local App Store export still uses ExportOptions.plist (manual
|
||||
# profiles) in Scripts/archive-app-store.sh.
|
||||
DEVELOPMENT_TEAM: 9KA3NM38B6
|
||||
configs:
|
||||
Release:
|
||||
CODE_SIGN_STYLE: Manual
|
||||
CODE_SIGN_IDENTITY: "Apple Distribution"
|
||||
PROVISIONING_PROFILE_SPECIFIER: "*[expo] org.edgeever.mobile AppStore 2026-08-01T04:08:38.265Z"
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
info:
|
||||
path: EdgeEver/Resources/Info.plist
|
||||
properties:
|
||||
@@ -121,11 +121,7 @@ targets:
|
||||
SKIP_INSTALL: true
|
||||
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
|
||||
DEVELOPMENT_TEAM: 9KA3NM38B6
|
||||
configs:
|
||||
Release:
|
||||
CODE_SIGN_STYLE: Manual
|
||||
CODE_SIGN_IDENTITY: "Apple Distribution"
|
||||
PROVISIONING_PROFILE_SPECIFIER: "*[expo] org.edgeever.mobile.expo-sharing-extension AppStore 2026-08-01T04:09:04.014Z"
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
entitlements:
|
||||
path: ShareExtension/ShareExtension.entitlements
|
||||
properties:
|
||||
|
||||
+73
-38
@@ -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
|
||||
+10
-8
@@ -88,11 +88,13 @@ explicitly requested.
|
||||
|
||||
### App Store Connect
|
||||
|
||||
EAS Build creates the signed iOS archive from the selected tag and increments
|
||||
the remote iOS build number. EAS Submit uploads that exact build to App Store
|
||||
Connect, where it becomes available to TestFlight after Apple finishes
|
||||
processing it. Fastlane then selects the exact app version and build number,
|
||||
submits it to App Review with an App Store Connect API key, and configures
|
||||
automatic release after approval. Missing metadata, agreements, review
|
||||
information, or credentials cause the workflow to fail without submitting a
|
||||
different build.
|
||||
Native iOS store binaries come from **`apps/ios`** (SwiftUI), not Expo EAS.
|
||||
On macOS beta hosts, archives must go through **Xcode Cloud** (manual Archive
|
||||
workflow) so `BuildMachineOSBuild` is a release OS image — see
|
||||
[iOS Xcode Cloud](ios-xcode-cloud.md). Cloud stamps `CFBundleVersion` from the
|
||||
product’s next build number; `ci_post_xcodebuild.sh` uploads the App Store IPA
|
||||
with an App Store Connect API key when shared environment secrets are set.
|
||||
Fastlane (`apps/ios` `submit_review`) then selects the exact app version and
|
||||
build number, submits App Review, and configures automatic release after
|
||||
approval. Missing metadata, agreements, review information, or credentials
|
||||
cause the workflow to fail without submitting a different build.
|
||||
@@ -82,8 +82,12 @@ Release。默认命令直接使用 Production;只有明确要求测试交付
|
||||
|
||||
### App Store Connect
|
||||
|
||||
EAS Build 从指定 tag 创建签名 iOS Archive,并递增远端 iOS Build Number。
|
||||
EAS Submit 将同一个构建上传到 App Store Connect;Apple 处理完成后,它会出现在
|
||||
TestFlight 中。随后 Fastlane 使用 App Store Connect API Key 精确选择相同的
|
||||
App Version 和 Build Number,提交 App Review,并设置为审核通过后自动发布。
|
||||
如果元数据、协议、审核信息或凭据不完整,工作流会失败,不会改为提交其他构建。
|
||||
原生 iOS 商店二进制来自 **`apps/ios`**(SwiftUI),不再走 Expo EAS。
|
||||
在 macOS beta 本机上,Archive 必须通过 **Xcode Cloud**(仅手动触发的 Archive
|
||||
工作流),保证 `BuildMachineOSBuild` 来自正式系统镜像——见
|
||||
[iOS Xcode Cloud](ios-xcode-cloud.md)。Cloud 用产品「下一个构建版本编号」写入
|
||||
`CFBundleVersion`;配置共享环境变量后,`ci_post_xcodebuild.sh` 会用
|
||||
App Store Connect API Key 上传 App Store IPA。随后 Fastlane(`apps/ios` 的
|
||||
`submit_review`)精确选择相同的 App Version 与 Build Number,提交 App Review,
|
||||
并设置为审核通过后自动发布。元数据、协议、审核信息或凭据不完整时工作流会失败,
|
||||
不会改为提交其他构建。
|
||||
Reference in new issue
Block a user