From 0944045b75d41343bd527e43a2890db5305b077e Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:28:27 +0200 Subject: [PATCH] chore: prefer canonical ios signing team --- apps/ios/Config/Signing.xcconfig | 2 +- apps/ios/Signing.xcconfig | 2 +- apps/ios/fastlane/SETUP.md | 2 +- scripts/ios-beta-prepare.sh | 8 ++- scripts/ios-team-id.sh | 81 ++++++++++++++++++++++-- test/scripts/ios-beta-prepare.test.ts | 49 +++++++++++++++ test/scripts/ios-team-id.test.ts | 90 ++++++++++++++++++++++++++- 7 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 test/scripts/ios-beta-prepare.test.ts diff --git a/apps/ios/Config/Signing.xcconfig b/apps/ios/Config/Signing.xcconfig index cb34902955d6..646bba255fc9 100644 --- a/apps/ios/Config/Signing.xcconfig +++ b/apps/ios/Config/Signing.xcconfig @@ -1,7 +1,7 @@ // Shared iOS signing defaults for local development + CI. #include "Version.xcconfig" -OPENCLAW_IOS_DEFAULT_TEAM = Y5PE65HELJ +OPENCLAW_IOS_DEFAULT_TEAM = FWJYW4S8P8 OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM) OPENCLAW_DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM) OPENCLAW_CODE_SIGN_STYLE = Automatic diff --git a/apps/ios/Signing.xcconfig b/apps/ios/Signing.xcconfig index 4ec128571fe5..9c49f55e62dc 100644 --- a/apps/ios/Signing.xcconfig +++ b/apps/ios/Signing.xcconfig @@ -5,7 +5,7 @@ #include "Config/Version.xcconfig" OPENCLAW_CODE_SIGN_STYLE = Manual -OPENCLAW_DEVELOPMENT_TEAM = Y5PE65HELJ +OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8 OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share diff --git a/apps/ios/fastlane/SETUP.md b/apps/ios/fastlane/SETUP.md index 136282c3bc23..059f250e8a7d 100644 --- a/apps/ios/fastlane/SETUP.md +++ b/apps/ios/fastlane/SETUP.md @@ -53,7 +53,7 @@ Code signing variable (optional in `.env`): IOS_DEVELOPMENT_TEAM=YOUR_TEAM_ID ``` -Tip: run `scripts/ios-team-id.sh` from repo root to print a Team ID for `.env`. The helper prefers the canonical OpenClaw team (`Y5PE65HELJ`) when present locally; otherwise it prefers the first non-personal team from your Xcode account (then personal team if needed). Fastlane uses this helper automatically if `IOS_DEVELOPMENT_TEAM` is missing. +Tip: run `scripts/ios-team-id.sh --require-canonical` from repo root to verify the canonical OpenClaw iOS team (`FWJYW4S8P8`) is available locally. Fastlane uses the same canonical-only path when `IOS_DEVELOPMENT_TEAM` is missing, and rejects non-canonical teams for beta archives. For local/manual iOS builds that stay on direct APNs, configure the gateway host separately with `OPENCLAW_APNS_TEAM_ID`, `OPENCLAW_APNS_KEY_ID`, and either `OPENCLAW_APNS_PRIVATE_KEY_P8` or `OPENCLAW_APNS_PRIVATE_KEY_PATH`. Those gateway runtime env vars are separate from Fastlane's `.env`. diff --git a/scripts/ios-beta-prepare.sh b/scripts/ios-beta-prepare.sh index 70706bfd96d2..4101110316aa 100755 --- a/scripts/ios-beta-prepare.sh +++ b/scripts/ios-beta-prepare.sh @@ -26,6 +26,7 @@ TEAM_HELPER="${ROOT_DIR}/scripts/ios-team-id.sh" VERSION_HELPER="${ROOT_DIR}/scripts/ios-write-version-xcconfig.sh" IOS_VERSION_HELPER="${ROOT_DIR}/scripts/ios-version.ts" VERSION_SYNC_HELPER="${ROOT_DIR}/scripts/ios-sync-versioning.ts" +CANONICAL_TEAM_ID="FWJYW4S8P8" BUILD_NUMBER="" TEAM_ID="${IOS_DEVELOPMENT_TEAM:-}" @@ -114,7 +115,7 @@ if [[ -z "${BUILD_NUMBER}" ]]; then fi if [[ -z "${TEAM_ID}" ]]; then - TEAM_ID="$(IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK=1 bash "${TEAM_HELPER}")" + TEAM_ID="$(IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK=1 bash "${TEAM_HELPER}" --require-canonical)" fi if [[ -z "${TEAM_ID}" ]]; then @@ -122,6 +123,11 @@ if [[ -z "${TEAM_ID}" ]]; then exit 1 fi +if [[ "${TEAM_ID}" != "${CANONICAL_TEAM_ID}" ]]; then + echo "iOS beta release must use canonical OpenClaw Team ID ${CANONICAL_TEAM_ID}; got ${TEAM_ID}." >&2 + exit 1 +fi + validate_push_relay_base_url "${PUSH_RELAY_BASE_URL}" # `.xcconfig` treats `//` as a comment opener. Break the URL with a helper setting diff --git a/scripts/ios-team-id.sh b/scripts/ios-team-id.sh index 0963d8d84994..c56d417b1a04 100755 --- a/scripts/ios-team-id.sh +++ b/scripts/ios-team-id.sh @@ -1,12 +1,56 @@ #!/usr/bin/env bash set -euo pipefail +usage() { + cat <<'EOF' +Usage: + scripts/ios-team-id.sh [--require-canonical] + +Prints an Apple Developer Team ID for iOS signing. + +Default behavior: +- return IOS_DEVELOPMENT_TEAM when set +- prefer the canonical OpenClaw iOS team when available in Xcode +- otherwise fall back to a local Xcode team for local development builds + +Options: + --require-canonical fail unless the resolved team is FWJYW4S8P8 +EOF +} + +canonical_team="FWJYW4S8P8" +require_canonical="0" +while [[ $# -gt 0 ]]; do + case "$1" in + --require-canonical) + require_canonical="1" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +canonical_team="${canonical_team//$'\r'/}" + if [[ -n "${IOS_DEVELOPMENT_TEAM:-}" ]]; then - printf '%s\n' "${IOS_DEVELOPMENT_TEAM}" + explicit_team="${IOS_DEVELOPMENT_TEAM//$'\r'/}" + if [[ "$require_canonical" == "1" && "$explicit_team" != "$canonical_team" ]]; then + echo "Resolved iOS Team ID '${explicit_team}' is not the canonical OpenClaw iOS team '${canonical_team}'." >&2 + exit 1 + fi + printf '%s\n' "$explicit_team" exit 0 fi -preferred_team="${IOS_PREFERRED_TEAM_ID:-${OPENCLAW_IOS_DEFAULT_TEAM_ID:-Y5PE65HELJ}}" +preferred_team="${IOS_PREFERRED_TEAM_ID:-}" preferred_team_name="${IOS_PREFERRED_TEAM_NAME:-}" allow_keychain_fallback="${IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK:-0}" prefer_non_free_team="${IOS_PREFER_NON_FREE_TEAM:-1}" @@ -53,7 +97,8 @@ append_team() { team_names+=("$candidate_name") } -load_teams_from_xcode_preferences() { +load_teams_from_xcode_team_key() { + local key="$1" local plist_path="${HOME}/Library/Preferences/com.apple.dt.Xcode.plist" [[ -f "$plist_path" ]] || return 0 [[ -n "$python_cmd" ]] || return 0 @@ -62,7 +107,7 @@ load_teams_from_xcode_preferences() { [[ -z "$team_id" ]] && continue append_team "$team_id" "${is_free:-0}" "${team_name:-}" done < <( - plutil -extract IDEProvisioningTeams json -o - "$plist_path" 2>/dev/null \ + plutil -extract "$key" json -o - "$plist_path" 2>/dev/null \ | "$python_cmd" -c ' import json import sys @@ -91,6 +136,11 @@ for teams in data.values(): ) } +load_teams_from_xcode_preferences() { + load_teams_from_xcode_team_key IDEProvisioningTeamByIdentifier + load_teams_from_xcode_team_key IDEProvisioningTeams +} + load_teams_from_legacy_defaults_key() { while IFS= read -r team; do [[ -z "$team" ]] && continue @@ -156,6 +206,12 @@ if [[ ${#team_ids[@]} -eq 0 && "$allow_keychain_fallback" == "1" ]]; then fi if [[ ${#team_ids[@]} -eq 0 ]]; then + if [[ "$require_canonical" == "1" ]]; then + echo "Canonical OpenClaw iOS Team ID '${canonical_team}' is not available in Xcode on this machine." >&2 + echo "Sign into the Apple Developer account that owns the canonical team, or set IOS_DEVELOPMENT_TEAM=${canonical_team}." >&2 + exit 1 + fi + if has_xcode_account; then echo "An Apple account is signed in to Xcode, but no Team ID could be resolved." >&2 echo "" >&2 @@ -178,12 +234,27 @@ if [[ ${#team_ids[@]} -eq 0 ]]; then fi for i in "${!team_ids[@]}"; do - if [[ "${team_ids[$i]}" == "$preferred_team" ]]; then + if [[ "${team_ids[$i]}" == "$canonical_team" ]]; then printf '%s\n' "${team_ids[$i]}" exit 0 fi done +if [[ "$require_canonical" == "1" ]]; then + echo "Canonical OpenClaw iOS Team ID '${canonical_team}' is not available in Xcode on this machine." >&2 + echo "Sign into the Apple Developer account that owns the canonical team, or set IOS_DEVELOPMENT_TEAM=${canonical_team}." >&2 + exit 1 +fi + +if [[ -n "$preferred_team" ]]; then + for i in "${!team_ids[@]}"; do + if [[ "${team_ids[$i]}" == "$preferred_team" ]]; then + printf '%s\n' "${team_ids[$i]}" + exit 0 + fi + done +fi + if [[ -n "$preferred_team_name" ]]; then preferred_team_name_lc="$(printf '%s' "$preferred_team_name" | tr '[:upper:]' '[:lower:]')" for i in "${!team_ids[@]}"; do diff --git a/test/scripts/ios-beta-prepare.test.ts b/test/scripts/ios-beta-prepare.test.ts new file mode 100644 index 000000000000..78026b4afa19 --- /dev/null +++ b/test/scripts/ios-beta-prepare.test.ts @@ -0,0 +1,49 @@ +// iOS beta prepare tests cover release-signing guardrails. +import { execFileSync } from "node:child_process"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const SCRIPT = path.join(process.cwd(), "scripts", "ios-beta-prepare.sh"); +const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash"; +const BASH_ARGS = process.platform === "win32" ? [SCRIPT] : ["--noprofile", "--norc", SCRIPT]; + +function runPrepare(extraArgs: string[]): { ok: boolean; stdout: string; stderr: string } { + try { + const stdout = execFileSync(BASH_BIN, [...BASH_ARGS, ...extraArgs], { + env: { + ...process.env, + IOS_DEVELOPMENT_TEAM: "Y3YUZP442G", + }, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + return { ok: true, stdout: stdout.trim(), stderr: "" }; + } catch (error) { + const e = error as { stdout?: unknown; stderr?: unknown }; + const stdout = + typeof e.stdout === "string" + ? e.stdout + : Buffer.isBuffer(e.stdout) + ? e.stdout.toString("utf8") + : ""; + const stderr = + typeof e.stderr === "string" + ? e.stderr + : Buffer.isBuffer(e.stderr) + ? e.stderr.toString("utf8") + : ""; + return { ok: false, stdout: stdout.trim(), stderr: stderr.trim() }; + } +} + +describe("scripts/ios-beta-prepare.sh", () => { + it("rejects non-canonical beta signing teams before generating release inputs", () => { + const result = runPrepare(["--build-number", "7"]); + + expect(result.ok).toBe(false); + expect(result.stderr).toContain( + "iOS beta release must use canonical OpenClaw Team ID FWJYW4S8P8", + ); + expect(result.stderr).toContain("got Y3YUZP442G"); + }); +}); diff --git a/test/scripts/ios-team-id.test.ts b/test/scripts/ios-team-id.test.ts index b83b1a5922f3..aea36ad1b077 100644 --- a/test/scripts/ios-team-id.test.ts +++ b/test/scripts/ios-team-id.test.ts @@ -11,6 +11,7 @@ const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash"; const BASH_ARGS = process.platform === "win32" ? [SCRIPT] : ["--noprofile", "--norc", SCRIPT]; const BASE_PATH = process.env.PATH ?? "/usr/bin:/bin"; const BASE_LANG = process.env.LANG ?? "C"; +const CANONICAL_TEAM_ID = "FWJYW4S8P8"; let fixtureRoot = ""; let sharedBinDir = ""; let sharedHomeDir = ""; @@ -50,10 +51,20 @@ function parseTeamCandidateRows(raw: string): TeamCandidate[] { function pickTeamIdFromCandidates(params: { candidates: TeamCandidate[]; + canonicalTeamId?: string; preferredTeamId?: string; preferredTeamName?: string; preferNonFreeTeam?: boolean; + requireCanonical?: boolean; }): string | undefined { + const canonicalTeamId = (params.canonicalTeamId ?? CANONICAL_TEAM_ID).trim(); + if (canonicalTeamId) { + const canonical = params.candidates.find((candidate) => candidate.teamId === canonicalTeamId); + if (canonical || params.requireCanonical) { + return canonical?.teamId; + } + } + const preferredTeamId = (params.preferredTeamId ?? "").trim(); if (preferredTeamId) { const preferred = params.candidates.find((candidate) => candidate.teamId === preferredTeamId); @@ -90,6 +101,7 @@ async function writeExecutable(filePath: string, body: string): Promise { function runScript( homeDir: string, extraEnv: Record = {}, + scriptArgs: string[] = [], ): { ok: boolean; stdout: string; @@ -99,7 +111,7 @@ function runScript( .toSorted((a, b) => a.localeCompare(b)) .map((key) => `${key}=${extraEnv[key] ?? ""}`) .join("\u0001"); - const cacheKey = `${homeDir}\u0000${extraEnvKey}`; + const cacheKey = `${homeDir}\u0000${extraEnvKey}\u0000${scriptArgs.join("\u0001")}`; const cached = runScriptCache.get(cacheKey); if (cached) { return cached; @@ -112,7 +124,7 @@ function runScript( ...extraEnv, }; try { - const stdout = execFileSync(BASH_BIN, BASH_ARGS, { + const stdout = execFileSync(BASH_BIN, [...BASH_ARGS, ...scriptArgs], { env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], @@ -218,6 +230,12 @@ printf 'BBBBB22222\\t0\\tBeta Team\\r\\n'`, }); expect(preferred).toBe("BBBBB22222"); + const missingCanonical = pickTeamIdFromCandidates({ + candidates: rows, + requireCanonical: true, + }); + expect(missingCanonical).toBeUndefined(); + const fallback = pickTeamIdFromCandidates({ candidates: rows, preferredTeamId: "CCCCCC3333", @@ -225,12 +243,80 @@ printf 'BBBBB22222\\t0\\tBeta Team\\r\\n'`, expect(fallback).toBe("BBBBB22222"); }); + it("prefers the canonical OpenClaw iOS team when it is present", async () => { + const homeDir = makeTempDir(tempDirs, "openclaw-ios-team-id-canonical-"); + const binDir = path.join(homeDir, "bin"); + await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true }); + await mkdir(binDir, { recursive: true }); + await writeFile( + path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), + "", + "utf8", + ); + const fakePythonPath = path.join(binDir, "fake-python"); + await writeExecutable( + fakePythonPath, + `#!/usr/bin/env bash +printf 'AAAAA11111\\t0\\tAlpha Team\\r\\n' +printf '${CANONICAL_TEAM_ID}\\t0\\tOpenClaw\\r\\n'`, + ); + + const result = runScript(homeDir, { IOS_PYTHON_BIN: fakePythonPath }); + expect(result.ok).toBe(true); + expect(result.stdout).toBe(CANONICAL_TEAM_ID); + }); + + it("loads teams from Xcode account identifier team metadata", async () => { + const homeDir = makeTempDir(tempDirs, "openclaw-ios-team-id-by-identifier-"); + const binDir = path.join(homeDir, "bin"); + await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true }); + await mkdir(binDir, { recursive: true }); + await writeFile( + path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), + "", + "utf8", + ); + await writeExecutable( + path.join(binDir, "plutil"), + `#!/usr/bin/env bash +if [[ "$1" == "-extract" && "$2" == "IDEProvisioningTeamByIdentifier" ]]; then + cat <<'JSON' +{"account-id":[{"teamID":"FWJYW4S8P8","teamName":"OpenClaw Foundation","isFreeProvisioningTeam":false,"teamType":"Company"}]} +JSON + exit 0 +fi +echo '{}'`, + ); + + const result = runScript(homeDir, { IOS_PYTHON_BIN: "python3" }, ["--require-canonical"]); + expect(result.ok).toBe(true); + expect(result.stdout).toBe(CANONICAL_TEAM_ID); + }); + it("resolves a fallback team ID from Xcode team listings (smoke)", () => { const fallbackResult = runScript(sharedHomeDir, { IOS_PYTHON_BIN: sharedFakePythonPath }); expect(fallbackResult.ok).toBe(true); expect(fallbackResult.stdout).toBe("AAAAA11111"); }); + it("fails canonical-only resolution when only fallback teams are available", () => { + const result = runScript(sharedHomeDir, { IOS_PYTHON_BIN: sharedFakePythonPath }, [ + "--require-canonical", + ]); + expect(result.ok).toBe(false); + expect(result.stderr).toContain( + `Canonical OpenClaw iOS Team ID '${CANONICAL_TEAM_ID}' is not available`, + ); + }); + + it("rejects explicit non-canonical teams in canonical-only mode", () => { + const result = runScript(sharedHomeDir, { IOS_DEVELOPMENT_TEAM: "BBBBB22222" }, [ + "--require-canonical", + ]); + expect(result.ok).toBe(false); + expect(result.stderr).toContain("is not the canonical OpenClaw iOS team"); + }); + it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", () => { const result = runScript(sharedHomeDir); expect(result.ok).toBe(false);