chore: prefer canonical ios signing team

This commit is contained in:
joshavant
2026-06-15 12:28:27 +02:00
parent c932bf377b
commit 0944045b75
7 changed files with 223 additions and 11 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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`.
+7 -1
View File
@@ -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
+76 -5
View File
@@ -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
+49
View File
@@ -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");
});
});
+88 -2
View File
@@ -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<void> {
function runScript(
homeDir: string,
extraEnv: Record<string, string> = {},
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);