aiblueprint-cli 1.4.79 → 1.4.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: appstore-connect
|
|
3
|
+
description: Interact with App Store Connect via the asc CLI - apps, builds, TestFlight, beta testers, reviews, sales/analytics, metadata, IAP, signing, submissions. Use for "check my app", "App Store Connect", "TestFlight status", "app review", "my app sales", or "asc".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# App Store Connect (asc)
|
|
7
|
+
|
|
8
|
+
Read, manage, and ship any of the user's App Store apps through the **`asc`** CLI (App Store Connect CLI by Rork). `asc` covers nearly the entire ASC surface; reach for the raw API only for the rare gap.
|
|
9
|
+
|
|
10
|
+
<auth>
|
|
11
|
+
`asc` is already authenticated on this machine (a default keychain profile). Verify before doing real work:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
asc auth status # shows stored credential profiles + which is default
|
|
15
|
+
asc auth status --validate # confirms a credential actually works
|
|
16
|
+
asc doctor # diagnose auth/config issues
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Multiple accounts/teams: `asc --profile <name> <command>` selects a profile.
|
|
20
|
+
- A NEW account with no stored key: run the `appstore-connect-setup` skill (locates the `.p8`, key id, and issuer id, then `asc auth login`). Never print or commit `.p8` keys, key ids, or issuer ids.
|
|
21
|
+
</auth>
|
|
22
|
+
|
|
23
|
+
<how_to_drive>
|
|
24
|
+
Do NOT guess flags. `asc` is large and self-documenting — discover, then act:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
asc --help # the full command map (areas below)
|
|
28
|
+
asc <area> --help # subcommands + flags for an area, e.g. `asc builds --help`
|
|
29
|
+
asc docs list # embedded guides; `asc docs <topic>` to read one
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **Read before you write.** List/inspect/validate first; mutate second.
|
|
33
|
+
- **Machine output:** most commands accept `--output json` (or `--output table` for humans) — use JSON when you need to parse ids.
|
|
34
|
+
- Resolve an app id once and reuse it: `asc apps list --output json` → the numeric App Store app id.
|
|
35
|
+
</how_to_drive>
|
|
36
|
+
|
|
37
|
+
<command_map>
|
|
38
|
+
| Goal | Area |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| List/inspect apps, app id | `asc apps`, `asc status --app <id>` |
|
|
41
|
+
| Builds (processing, ids) | `asc builds` |
|
|
42
|
+
| TestFlight: beta groups, testers, distribute a build | `asc testflight` |
|
|
43
|
+
| Versions / release state | `asc versions`, `asc release`, `asc status` |
|
|
44
|
+
| Metadata, localizations, keywords | `asc metadata`, `asc localizations` |
|
|
45
|
+
| Screenshots / previews | `asc screenshots`, `asc video-previews` |
|
|
46
|
+
| Pricing & availability | `asc pricing` |
|
|
47
|
+
| In-app purchases / subscriptions | `asc iap`, `asc subscriptions` |
|
|
48
|
+
| Age rating / privacy / encryption / EULA | `asc age-rating`, `asc encryption`, `asc eula` |
|
|
49
|
+
| Submission readiness + submit | `asc validate`, `asc review`, `asc submit`, `asc publish` |
|
|
50
|
+
| Customer reviews | `asc reviews` |
|
|
51
|
+
| Sales / analytics / finance | `asc analytics`, `asc insights`, `asc finance`, `asc performance` |
|
|
52
|
+
| Signing: certs, profiles, bundle ids | `asc signing`, `asc certificates`, `asc profiles`, `asc bundle-ids` |
|
|
53
|
+
| Team / users / devices | `asc users`, `asc devices`, `asc account` |
|
|
54
|
+
| Xcode Cloud, webhooks, workflows | `asc xcode-cloud`, `asc webhooks`, `asc workflow` |
|
|
55
|
+
</command_map>
|
|
56
|
+
|
|
57
|
+
<common_tasks>
|
|
58
|
+
```bash
|
|
59
|
+
# What apps do I have / what's an app's id?
|
|
60
|
+
asc apps list --output table
|
|
61
|
+
|
|
62
|
+
# Where is an app in the release pipeline?
|
|
63
|
+
asc status --app <APP_ID> --output table
|
|
64
|
+
|
|
65
|
+
# Latest builds and processing state
|
|
66
|
+
asc builds list --app <APP_ID> --output json
|
|
67
|
+
|
|
68
|
+
# TestFlight: see groups, add a tester, distribute a build
|
|
69
|
+
asc testflight groups list --app <APP_ID>
|
|
70
|
+
asc publish testflight --app <APP_ID> --ipa <path.ipa> --group "<GROUP_ID>" --wait
|
|
71
|
+
|
|
72
|
+
# Is a version ready to submit? (fix every error it reports first)
|
|
73
|
+
asc validate --app <APP_ID> --version <VERSION> --platform IOS --output table
|
|
74
|
+
|
|
75
|
+
# Read customer reviews / respond
|
|
76
|
+
asc reviews list --app <APP_ID> --output table
|
|
77
|
+
|
|
78
|
+
# Sales & analytics
|
|
79
|
+
asc analytics --help # request/download reports
|
|
80
|
+
asc insights --help # weekly/daily insights
|
|
81
|
+
```
|
|
82
|
+
</common_tasks>
|
|
83
|
+
|
|
84
|
+
<safety>
|
|
85
|
+
- **Confirm before any mutation that is externally visible or hard to reverse:** `asc submit`, `asc publish ... --submit`, `asc review submit`, `asc pricing` changes, `asc iap`/`asc subscriptions` edits, `asc users` invites/removals, certificate/profile deletion. State the app id, version, and exact change, and get explicit user approval.
|
|
86
|
+
- Prefer dry runs / validation first: many commands accept `--dry-run`; always run `asc validate` before submitting.
|
|
87
|
+
- Never delete a signing certificate or provisioning profile without confirmation — it can break other apps signed with it.
|
|
88
|
+
- Never print or commit `.p8` keys, key ids, issuer ids, app-specific passwords, or downloaded financial reports with PII.
|
|
89
|
+
- This skill manages the store side only. Producing the build (.ipa/.aab) and project config belongs to the app's own deploy workflow.
|
|
90
|
+
</safety>
|
|
91
|
+
|
|
92
|
+
<raw_api_fallback>
|
|
93
|
+
For the rare endpoint `asc` doesn't expose, call the App Store Connect API directly with the bundled helper (zero deps, ES256 JWT). Credentials come from env vars — never hardcode them:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export ASC_KEY_ID=... # 10-char key id (from the AuthKey_<KEY_ID>.p8 filename)
|
|
97
|
+
export ASC_ISSUER_ID=... # team issuer UUID (App Store Connect > Users and Access > Integrations)
|
|
98
|
+
export ASC_P8_PATH=/path/to/AuthKey_<KEY_ID>.p8
|
|
99
|
+
node "$SKILL_DIR/scripts/asc-api.mjs" GET "/v1/apps?filter[bundleId]=com.example.app"
|
|
100
|
+
node "$SKILL_DIR/scripts/asc-api.mjs" POST /v1/betaGroups body.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`$SKILL_DIR` is this skill's directory. The helper prints `{"status", "body"}` and exits non-zero on HTTP >= 400.
|
|
104
|
+
</raw_api_fallback>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal App Store Connect API client. Zero dependencies.
|
|
4
|
+
* Used by the setup-testflight / deploy-ios-app skills to manage certificates,
|
|
5
|
+
* provisioning profiles, beta groups, and testers without the web UI.
|
|
6
|
+
*
|
|
7
|
+
* Credentials come from env vars (never hardcode them):
|
|
8
|
+
* ASC_KEY_ID - 10-char API key ID (from the AuthKey_<KEY_ID>.p8 filename)
|
|
9
|
+
* ASC_ISSUER_ID - team issuer UUID (App Store Connect > Users and Access > Integrations)
|
|
10
|
+
* ASC_P8_PATH - absolute path to the AuthKey_*.p8 private key file
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/asc-api.mjs GET "/v1/apps?filter[bundleId]=com.example.app"
|
|
14
|
+
* node scripts/asc-api.mjs POST /v1/betaGroups body.json
|
|
15
|
+
* node scripts/asc-api.mjs DELETE /v1/profiles/<id>
|
|
16
|
+
*
|
|
17
|
+
* Prints {"status": <http status>, "body": <json|null>} on stdout.
|
|
18
|
+
* Exits non-zero on HTTP >= 400 so shell scripts can chain safely.
|
|
19
|
+
*/
|
|
20
|
+
import crypto from "node:crypto";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
|
|
23
|
+
const KEY_ID = process.env.ASC_KEY_ID;
|
|
24
|
+
const ISSUER_ID = process.env.ASC_ISSUER_ID;
|
|
25
|
+
const P8_PATH = process.env.ASC_P8_PATH;
|
|
26
|
+
|
|
27
|
+
if (!KEY_ID || !ISSUER_ID || !P8_PATH) {
|
|
28
|
+
console.error(
|
|
29
|
+
"Missing credentials. Set ASC_KEY_ID, ASC_ISSUER_ID, and ASC_P8_PATH env vars.\n" +
|
|
30
|
+
"Key ID is in the .p8 filename (AuthKey_<KEY_ID>.p8); issuer ID is in\n" +
|
|
31
|
+
"App Store Connect > Users and Access > Integrations > App Store Connect API.",
|
|
32
|
+
);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
if (!fs.existsSync(P8_PATH)) {
|
|
36
|
+
console.error(`ASC_P8_PATH not found: ${P8_PATH}`);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const b64url = (buf) =>
|
|
41
|
+
Buffer.from(buf)
|
|
42
|
+
.toString("base64")
|
|
43
|
+
.replace(/=/g, "")
|
|
44
|
+
.replace(/\+/g, "-")
|
|
45
|
+
.replace(/\//g, "_");
|
|
46
|
+
|
|
47
|
+
function makeJwt() {
|
|
48
|
+
const header = { alg: "ES256", kid: KEY_ID, typ: "JWT" };
|
|
49
|
+
const now = Math.floor(Date.now() / 1000);
|
|
50
|
+
const payload = {
|
|
51
|
+
iss: ISSUER_ID,
|
|
52
|
+
iat: now,
|
|
53
|
+
exp: now + 900,
|
|
54
|
+
aud: "appstoreconnect-v1",
|
|
55
|
+
};
|
|
56
|
+
const signingInput = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
|
|
57
|
+
const key = crypto.createPrivateKey(fs.readFileSync(P8_PATH, "utf8"));
|
|
58
|
+
const sig = crypto.sign("sha256", Buffer.from(signingInput), {
|
|
59
|
+
key,
|
|
60
|
+
dsaEncoding: "ieee-p1363",
|
|
61
|
+
});
|
|
62
|
+
return `${signingInput}.${b64url(sig)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const [method, apiPath, bodyFile] = process.argv.slice(2);
|
|
66
|
+
if (!method || !apiPath) {
|
|
67
|
+
console.error("usage: asc-api.mjs <GET|POST|PATCH|DELETE> </v1/...> [bodyFile.json]");
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const res = await fetch(`https://api.appstoreconnect.apple.com${encodeURI(apiPath)}`, {
|
|
72
|
+
method: method.toUpperCase(),
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${makeJwt()}`,
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
},
|
|
77
|
+
body: bodyFile ? fs.readFileSync(bodyFile, "utf8") : undefined,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const text = await res.text();
|
|
81
|
+
let body = null;
|
|
82
|
+
try {
|
|
83
|
+
body = text ? JSON.parse(text) : null;
|
|
84
|
+
} catch {
|
|
85
|
+
body = { raw: text.slice(0, 2000) };
|
|
86
|
+
}
|
|
87
|
+
console.log(JSON.stringify({ status: res.status, body }, null, 2));
|
|
88
|
+
if (res.status >= 400) process.exit(1);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: appstore-connect-setup
|
|
3
|
+
description: Find and configure App Store Connect API credentials for asc auth. Use when asc auth is missing, credentials are unknown, the user says login to App Store Connect, or before TestFlight/App Store release work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# App Store Connect Setup
|
|
7
|
+
|
|
8
|
+
<objective>
|
|
9
|
+
`asc auth login` needs three things: a **key ID**, an **issuer ID**, and a **.p8 private key file**. Users rarely remember where these are. This skill is a battle-tested workflow for finding all three without asking the user to dig through App Store Connect manually.
|
|
10
|
+
|
|
11
|
+
Key insight from a real session: the `.p8` files and key IDs live on disk, but the **issuer ID is almost never stored locally** — it only exists in the App Store Connect web UI. The trick is to read it from the user's already-signed-in browser session via CDP, since Apple login requires 2FA and cannot be automated.
|
|
12
|
+
</objective>
|
|
13
|
+
|
|
14
|
+
<credentials_anatomy>
|
|
15
|
+
| Credential | What it looks like | Where it lives |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Key ID | 10 chars, e.g. `T397KWC8K7` | In the `.p8` filename: `AuthKey_<KEY_ID>.p8` |
|
|
18
|
+
| Issuer ID | UUID, e.g. `35b197bd-...` | App Store Connect → Users and Access → Integrations → App Store Connect API (top of Keys page). Team-level, same for all keys. |
|
|
19
|
+
| Private key | `AuthKey_*.p8` file, ~257 bytes | User's disk — Downloads is the most common spot (Apple only lets you download it once) |
|
|
20
|
+
|
|
21
|
+
Beware look-alikes that are NOT API auth keys:
|
|
22
|
+
- `SubscriptionKey_*.p8` — in-app purchase key, won't work for `asc auth login`.
|
|
23
|
+
- `.p8` files for APNs (push notifications).
|
|
24
|
+
</credentials_anatomy>
|
|
25
|
+
|
|
26
|
+
<step n="1" title="Check if already authenticated">
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
asc auth status --validate
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If a credential validates as "works", stop — nothing to do.
|
|
33
|
+
</step>
|
|
34
|
+
|
|
35
|
+
<step n="2" title="Hunt for .p8 files on disk">
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Standard locations first
|
|
39
|
+
ls ~/.asc ~/private_keys ~/.appstoreconnect/private_keys 2>/dev/null
|
|
40
|
+
|
|
41
|
+
# Then the places people actually put them (Downloads wins in practice)
|
|
42
|
+
find ~/Downloads ~/Documents ~/Desktop ~/Developer -maxdepth 5 -name "*.p8" 2>/dev/null
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Real finding: keys were in `~/Downloads/AuthKey_X.p8` and `~/Downloads/Dev/AuthKey_Y.p8`, downloaded months earlier. The key ID is the filename suffix.
|
|
46
|
+
</step>
|
|
47
|
+
|
|
48
|
+
<step n="3" title="Try to find the issuer ID locally (usually fails)">
|
|
49
|
+
|
|
50
|
+
Worth 30 seconds, but expect nothing:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
grep -riE "issuer" ~/Developer --include="*.json" --include="*.env*" --include="*.rb" --include="*.yml" -l 2>/dev/null | grep -v node_modules
|
|
54
|
+
grep -iE "issuer" ~/.zsh_history 2>/dev/null
|
|
55
|
+
grep -ri "issuer" ~/.fastlane ~/.expo 2>/dev/null
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Real finding: zero hits across the entire `~/Developer` tree, shell history, fastlane and expo config. Docs in repos only contain `ISSUER_ID` placeholders. **Do not burn time here — go to step 4.**
|
|
59
|
+
</step>
|
|
60
|
+
|
|
61
|
+
<step n="4" title="Read the issuer ID from the user's signed-in browser (the key move)">
|
|
62
|
+
|
|
63
|
+
Apple login = password + 2FA, so never try to log in yourself. Instead, attach to the browser where the user is **already signed in** and read the page.
|
|
64
|
+
|
|
65
|
+
**4a. Confirm with the user** which browser is signed in to App Store Connect (Chrome, Helium, etc.).
|
|
66
|
+
|
|
67
|
+
**4b. Check if the browser exposes CDP:**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl -s http://127.0.0.1:9222/json/version # Chrome default
|
|
71
|
+
curl -s http://127.0.0.1:9334/json/version # custom port
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**4c. If not (the usual case), quit and relaunch it with a debug port.** Session cookies survive a graceful quit, and `--restore-last-session` brings the tabs back:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
osascript -e 'quit app "Helium"' # or "Google Chrome"
|
|
78
|
+
sleep 3
|
|
79
|
+
nohup "/Applications/Helium.app/Contents/MacOS/Helium" --remote-debugging-port=9334 --restore-last-session >/dev/null 2>&1 &
|
|
80
|
+
sleep 5
|
|
81
|
+
curl -s http://127.0.0.1:9334/json/version | head -3 # must answer before continuing
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**4d. Navigate to the API keys page and extract the issuer ID:**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
dev-browser --browser helium --connect http://127.0.0.1:9334 --timeout 60 <<'EOF'
|
|
88
|
+
const page = await browser.getPage("asc");
|
|
89
|
+
await page.goto("https://appstoreconnect.apple.com/access/integrations/api", { waitUntil: "domcontentloaded" });
|
|
90
|
+
await page.waitForTimeout(8000);
|
|
91
|
+
const text = await page.evaluate(() => document.body.innerText);
|
|
92
|
+
const m = text.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
93
|
+
console.log(JSON.stringify({ issuerId: m ? m[1] : null, loggedOut: page.url().includes("/login") }));
|
|
94
|
+
EOF
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If `loggedOut` is true, ask the user to sign in in that browser window, then re-run.
|
|
98
|
+
|
|
99
|
+
**4e. Cross-check which keys are actually ACTIVE.** The same page lists active keys with their key IDs. A `.p8` found on disk may belong to a revoked key — match the filename key ID against the active list before using it:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
dev-browser --browser helium --connect http://127.0.0.1:9334 --timeout 30 <<'EOF'
|
|
103
|
+
const page = await browser.getPage("asc");
|
|
104
|
+
const text = await page.evaluate(() => document.body.innerText);
|
|
105
|
+
console.log(text); // active keys table: NAME / KEY ID / LAST USED / ACCESS
|
|
106
|
+
EOF
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Real finding: the first `.p8` we picked (`AuthKey_6QD5RMPU9F.p8`) was NOT in the active list — login would have failed. The second one matched an active Admin key and worked.
|
|
110
|
+
</step>
|
|
111
|
+
|
|
112
|
+
<step n="5" title="Organize keys, then log in">
|
|
113
|
+
|
|
114
|
+
Move keys to the canonical folder so the next agent finds them instantly:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
mkdir -p ~/Developer/app-store
|
|
118
|
+
mv ~/Downloads/AuthKey_*.p8 ~/Developer/app-store/ 2>/dev/null
|
|
119
|
+
chmod 600 ~/Developer/app-store/*.p8
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Then authenticate and verify end-to-end.
|
|
123
|
+
|
|
124
|
+
**Naming the credential:** use a stable, descriptive name tied to the machine + user, not the app — the same ASC API key is team-level and works across every app, so naming it per-app is misleading. Convention: `asc-macos-<user>-key` (e.g. `asc-macos-melvynx-key`). **Prefer a key with Admin access** when several active keys exist (Admin can read/write everything the release flow needs); check the ACCESS column from step 4e and pick the Admin key.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
asc auth login \
|
|
128
|
+
--name "asc-macos-melvynx-key" \
|
|
129
|
+
--key-id "KEY_ID" \
|
|
130
|
+
--issuer-id "ISSUER_ID" \
|
|
131
|
+
--private-key ~/Developer/app-store/AuthKey_KEY_ID.p8 \
|
|
132
|
+
--network
|
|
133
|
+
|
|
134
|
+
asc auth status --validate # must report "works"
|
|
135
|
+
asc apps list # must list the target app
|
|
136
|
+
```
|
|
137
|
+
</step>
|
|
138
|
+
|
|
139
|
+
<step n="6" title="Persist the findings">
|
|
140
|
+
|
|
141
|
+
Record in the agent memory / AGENTS.md so this hunt never happens twice:
|
|
142
|
+
|
|
143
|
+
- The keys folder (`~/Developer/app-store/`), each key ID and whether it is active
|
|
144
|
+
- The issuer ID (team-level, stable)
|
|
145
|
+
- The `asc` credential name (convention `asc-macos-<user>-key`), its access level (prefer Admin), and that it is stored in the system keychain
|
|
146
|
+
- The app's numeric App Store Connect ID and bundle ID from `asc apps list`
|
|
147
|
+
</step>
|
|
148
|
+
|
|
149
|
+
<critical_safety>
|
|
150
|
+
- Never commit `.p8` files or paste their contents anywhere. `chmod 600` them.
|
|
151
|
+
- Never attempt the Apple login form yourself — 2FA makes it pointless and looks like account takeover. Always reuse the user's signed-in session, with their explicit OK to attach to/restart their browser.
|
|
152
|
+
- Restarting the browser closes the user's windows; warn them and rely on session restore.
|
|
153
|
+
</critical_safety>
|
|
154
|
+
|
|
155
|
+
<success_criteria>
|
|
156
|
+
- `asc auth status --validate` reports the credential as "works".
|
|
157
|
+
- `asc apps list` returns the expected app(s).
|
|
158
|
+
- Keys live in `~/Developer/app-store/` and the findings are written to memory/AGENTS.md.
|
|
159
|
+
</success_criteria>
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ios-testflight
|
|
3
|
+
description: Build a NowStack Mobile iOS app and upload it to TestFlight. Defaults to local `eas build --local`; use `--expo` only when the user explicitly wants an EAS cloud build.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# iOS TestFlight - NowStack Mobile
|
|
7
|
+
|
|
8
|
+
Take a working NowStack Mobile app from local dev to an installable TestFlight build. Run phases A-D for setup only (stop before the build, report readiness); run all phases end to end to build and upload. This is the iOS beta surface; for App Store review use `ns-ios-distribute`.
|
|
9
|
+
|
|
10
|
+
Default build mode is **local Mac build** with `eas build --local`, so it does not consume Expo/EAS cloud build credits. If the user passes `--expo`, opt into the EAS cloud build path instead.
|
|
11
|
+
|
|
12
|
+
<objective>
|
|
13
|
+
Go from "the app works in the simulator" to "a build is installable from TestFlight" with maximum automation. Default to the local Mac build path to preserve Expo cloud credits; use `--expo` only when the user asks for a cloud build. Everything used here ships in this repo or is a public CLI (`eas-cli`, `asc`, `openssl`, `node`).
|
|
14
|
+
|
|
15
|
+
The proven flow (battle-tested on a real app built from this boilerplate):
|
|
16
|
+
|
|
17
|
+
1. Create the EAS project and wire `easProjectId` into `site-config.ts`.
|
|
18
|
+
2. Deploy Convex to production and set prod env vars.
|
|
19
|
+
3. Point `eas.json` production env at the prod Convex URLs.
|
|
20
|
+
4. Create Apple signing credentials (distribution cert + App Store provisioning profile) through the **App Store Connect API** using `mobile-app/scripts/asc-api.mjs` — no browser, no Apple ID 2FA.
|
|
21
|
+
5. Run a local signed App Store IPA build with `eas build --local`, `credentialsSource: "local"`, and an explicit `--output /tmp/{slug}.ipa`.
|
|
22
|
+
6. Upload the `.ipa` to TestFlight with `asc publish testflight` into an internal beta group.
|
|
23
|
+
|
|
24
|
+
The key insight: interactive `eas build` credential setup requires Apple ID 2FA and cannot be automated reliably. The ASC API key path avoids 2FA entirely, and local `eas build --local` avoids Expo cloud build credits while still using the same local signing files.
|
|
25
|
+
</objective>
|
|
26
|
+
|
|
27
|
+
<arguments>
|
|
28
|
+
- Default / no flag: run setup, local build, upload, and verify.
|
|
29
|
+
- `--expo`: use the EAS cloud build phase instead of local build. This consumes Expo/EAS build quota and requires explicit user confirmation.
|
|
30
|
+
- Setup-only requests (`ios setup`, "prepare TestFlight", "credentials only"): run phases A-D, then stop before any build/upload.
|
|
31
|
+
</arguments>
|
|
32
|
+
|
|
33
|
+
<prerequisites>
|
|
34
|
+
The user must have (ask once, as a group — these cannot be created by the agent):
|
|
35
|
+
|
|
36
|
+
1. **Apple Developer Program membership** (paid, enrolled).
|
|
37
|
+
2. **App Store Connect API key**: the `AuthKey_<KEY_ID>.p8` file, its key ID, and the team issuer ID. If unknown, run the `appstore-connect-setup` skill, or point the user to App Store Connect > Users and Access > Integrations > App Store Connect API (key must have Admin or App Manager role).
|
|
38
|
+
3. **Expo account** logged in: `npx eas-cli@latest whoami` (else `npx eas-cli@latest login`).
|
|
39
|
+
4. **App record in App Store Connect** for the bundle ID. The public API cannot create app records — if missing, the user creates it once at appstoreconnect.apple.com (My Apps > + > New App, selecting the bundle ID). Verify with Phase D step 1 and stop with clear instructions if absent.
|
|
40
|
+
5. `asc` CLI installed (`brew install asc`) — used only for the final upload; everything else goes through `scripts/asc-api.mjs`.
|
|
41
|
+
6. For the default local build: macOS with Xcode command line tools and CocoaPods (`xcodebuild -version`, `command -v pod`). If those are unavailable, stop and ask whether to use `--expo`.
|
|
42
|
+
</prerequisites>
|
|
43
|
+
|
|
44
|
+
<state_variables>
|
|
45
|
+
| Variable | Source |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `{bundle_id}` | `SiteConfig.bundleId` |
|
|
48
|
+
| `{apple_team_id}` | `SiteConfig.appleTeamId` |
|
|
49
|
+
| `{eas_project_id}` | output of `eas project:init`, then written to `site-config.ts` |
|
|
50
|
+
| `{asc_key_id}` / `{asc_issuer_id}` / `{asc_p8_path}` | from the user / `appstore-connect-setup`. Never commit, never print. |
|
|
51
|
+
| `{convex_prod_url}` / `{convex_prod_site_url}` | from `npx convex deploy` output / Convex dashboard |
|
|
52
|
+
| `{asc_app_id}` | numeric app ID resolved from the bundle ID (Phase D) |
|
|
53
|
+
| `{build_mode}` | default `local`; `expo` only when the user passes `--expo` |
|
|
54
|
+
| `{ipa_path}` | `/tmp/{slug}.ipa` from local build, or downloaded cloud artifact |
|
|
55
|
+
</state_variables>
|
|
56
|
+
|
|
57
|
+
<critical_safety>
|
|
58
|
+
- Never commit or print `.p8` keys, `.p12` files, p12 passwords, provisioning profiles, or `credentials.json`. The repo `.gitignore` already excludes them — verify before any commit.
|
|
59
|
+
- Export ASC credentials as env vars for `scripts/asc-api.mjs`; do not write them into files inside the repo.
|
|
60
|
+
- Get explicit user confirmation before: creating Apple certificates (accounts have a low cert limit), using `--expo` cloud EAS builds (consumes build credits), and the TestFlight upload.
|
|
61
|
+
- Local builds do not consume Expo cloud credits, but they still sign a production App Store IPA and may increment the remote EAS build number when `appVersionSource` is `remote`.
|
|
62
|
+
- Builds bake env vars in permanently: confirm `eas.json` points at the PROD Convex deployment before building, or testers ship with a dev backend.
|
|
63
|
+
</critical_safety>
|
|
64
|
+
|
|
65
|
+
<phase n="A" title="Preflight">
|
|
66
|
+
From the repo root:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm run check-setup # must be free of errors (easProjectId warning is expected pre-init)
|
|
70
|
+
cd mobile-app && npx tsc --noEmit && npm run lint
|
|
71
|
+
npx eas-cli@latest whoami # Expo account logged in?
|
|
72
|
+
command -v asc && asc version
|
|
73
|
+
xcodebuild -version && command -v pod # required for default local builds
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Read `site-config.ts` and confirm with the user: `title`, `bundleId`, `appleTeamId`, payment product IDs. The bundle ID is permanent once the app record exists — it must be final now.
|
|
77
|
+
|
|
78
|
+
Export the ASC credentials for the rest of the session:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export ASC_KEY_ID="<10-char key id>"
|
|
82
|
+
export ASC_ISSUER_ID="<issuer uuid>"
|
|
83
|
+
export ASC_P8_PATH="/path/to/AuthKey_<KEY_ID>.p8"
|
|
84
|
+
```
|
|
85
|
+
</phase>
|
|
86
|
+
|
|
87
|
+
<phase n="B" title="EAS project init">
|
|
88
|
+
Skip if `easProjectId` in `site-config.ts` is already a real UUID.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd mobile-app
|
|
92
|
+
npx eas-cli@latest project:init --non-interactive --force
|
|
93
|
+
npx eas-cli@latest project:info
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Write the printed project ID into `site-config.ts > easProjectId`. Re-run `npm run check-setup` — the easProjectId warning must be gone.
|
|
97
|
+
</phase>
|
|
98
|
+
|
|
99
|
+
<phase n="C" title="Convex production + eas.json">
|
|
100
|
+
Follow `docs/production-checklist.md` stage 1. Summary:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx convex deploy -y
|
|
104
|
+
# prod env starts EMPTY — set every required var:
|
|
105
|
+
npx convex env set --prod SITE_URL "https://<the-app-domain>"
|
|
106
|
+
npx convex env set --prod BETTER_AUTH_SECRET "$(openssl rand -hex 32)" # fresh, never reuse dev
|
|
107
|
+
npx convex env set --prod RESEND_API_KEY "$(npx convex env get RESEND_API_KEY)"
|
|
108
|
+
npx convex env set --prod EMAIL_FROM "$(npx convex env get EMAIL_FROM)"
|
|
109
|
+
# if Apple Sign In is enabled: APPLE_CLIENT_ID = {bundle_id}, APPLE_CLIENT_SECRET copied from dev
|
|
110
|
+
# if Google Sign In is enabled: GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET copied from dev
|
|
111
|
+
npx convex env list --prod
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then put the prod URLs into `mobile-app/eas.json > build.production`:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
"production": {
|
|
118
|
+
"autoIncrement": true,
|
|
119
|
+
"credentialsSource": "local",
|
|
120
|
+
"env": {
|
|
121
|
+
"EXPO_PUBLIC_CONVEX_URL": "https://<prod-deployment>.convex.cloud",
|
|
122
|
+
"EXPO_PUBLIC_CONVEX_SITE_URL": "https://<prod-deployment>.convex.site"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`credentialsSource: "local"` is what makes the build fully non-interactive (Phase E).
|
|
128
|
+
</phase>
|
|
129
|
+
|
|
130
|
+
<phase n="D" title="Apple signing credentials via ASC API">
|
|
131
|
+
All API calls use `node mobile-app/scripts/asc-api.mjs <METHOD> <path> [body.json]` with the env vars from Phase A. Work in a directory OUTSIDE the repo (e.g. `~/ios-credentials/<slug>/`) so nothing secret can be committed.
|
|
132
|
+
|
|
133
|
+
**1. Resolve the app record (hard gate):**
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
node mobile-app/scripts/asc-api.mjs GET "/v1/apps?filter[bundleId]={bundle_id}"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Empty `data` → STOP. Tell the user to create the app record at appstoreconnect.apple.com (My Apps > + > New App) with this exact bundle ID, then resume. Otherwise save `{asc_app_id}` = `data[0].id`.
|
|
140
|
+
|
|
141
|
+
**2. Bundle ID registration + capabilities:**
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
node mobile-app/scripts/asc-api.mjs GET "/v1/bundleIds?filter[identifier]={bundle_id}"
|
|
145
|
+
# if missing, register it:
|
|
146
|
+
# POST /v1/bundleIds {"data":{"type":"bundleIds","attributes":{"identifier":"{bundle_id}","name":"{title}","platform":"IOS"}}}
|
|
147
|
+
node mobile-app/scripts/asc-api.mjs GET "/v1/bundleIds/<BUNDLE_DB_ID>/bundleIdCapabilities"
|
|
148
|
+
# enable missing capabilities the app uses (IN_APP_PURCHASE, APPLE_ID_AUTH, PUSH_NOTIFICATIONS):
|
|
149
|
+
# POST /v1/bundleIdCapabilities {"data":{"type":"bundleIdCapabilities","attributes":{"capabilityType":"APPLE_ID_AUTH"},"relationships":{"bundleId":{"data":{"type":"bundleIds","id":"<BUNDLE_DB_ID>"}}}}}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**3. Distribution certificate.** First check for an existing one (Apple caps distribution certs at ~2-3 per account):
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
node mobile-app/scripts/asc-api.mjs GET "/v1/certificates?filter[certificateType]=DISTRIBUTION"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Reuse only if the user has its private key/.p12 locally. Otherwise create a new one (with user confirmation):
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
openssl req -new -newkey rsa:2048 -nodes -keyout dist.key -out dist.csr \
|
|
162
|
+
-subj "/emailAddress=<user-email>/CN={title} Distribution/C=US"
|
|
163
|
+
node -e "const fs=require('fs');fs.writeFileSync('cert-req.json',JSON.stringify({data:{type:'certificates',attributes:{certificateType:'DISTRIBUTION',csrContent:fs.readFileSync('dist.csr','utf8')}}}))"
|
|
164
|
+
node mobile-app/scripts/asc-api.mjs POST /v1/certificates cert-req.json
|
|
165
|
+
# save body.data.id (cert id) and body.data.attributes.certificateContent (base64) -> dist.cer
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**4. App Store provisioning profile:**
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
node -e "const fs=require('fs');fs.writeFileSync('profile-req.json',JSON.stringify({data:{type:'profiles',attributes:{name:'{title} AppStore',profileType:'IOS_APP_STORE'},relationships:{bundleId:{data:{type:'bundleIds',id:'<BUNDLE_DB_ID>'}},certificates:{data:[{type:'certificates',id:'<CERT_ID>'}]},devices:{data:[]}}}}))"
|
|
172
|
+
node mobile-app/scripts/asc-api.mjs POST /v1/profiles profile-req.json
|
|
173
|
+
# save body.data.attributes.profileContent (base64) -> profile.mobileprovision
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**5. Build the .p12 (the `-legacy` flag is mandatory — Apple tooling rejects OpenSSL 3.x default ciphers):**
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
echo "<certificateContent-b64>" | base64 -d > dist.cer
|
|
180
|
+
P12PASS=$(openssl rand -hex 12)
|
|
181
|
+
openssl x509 -inform der -in dist.cer -out dist.pem 2>/dev/null || cp dist.cer dist.pem
|
|
182
|
+
openssl pkcs12 -export -in dist.pem -inkey dist.key -out dist.p12 \
|
|
183
|
+
-name "{title} Distribution" -passout pass:"$P12PASS" -legacy
|
|
184
|
+
echo "<profileContent-b64>" | base64 -d > appstore.mobileprovision
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**6. Wire into the project (gitignored paths only):**
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
mkdir -p mobile-app/credentials
|
|
191
|
+
cp dist.p12 mobile-app/credentials/dist.p12
|
|
192
|
+
cp appstore.mobileprovision mobile-app/credentials/
|
|
193
|
+
node -e "const fs=require('fs');fs.writeFileSync('mobile-app/credentials.json',JSON.stringify({ios:{provisioningProfilePath:'credentials/appstore.mobileprovision',distributionCertificate:{path:'credentials/dist.p12',password:process.argv[1]}}},null,2))" "$P12PASS"
|
|
194
|
+
git check-ignore mobile-app/credentials.json mobile-app/credentials/dist.p12 # MUST print both paths
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Setup-only mode STOPS here — report readiness (app record, cert, profile, credentials.json, eas.json) and the next command.
|
|
198
|
+
</phase>
|
|
199
|
+
|
|
200
|
+
<phase n="E" title="Local iOS build (default)">
|
|
201
|
+
Default path. This uses the local Mac/Xcode toolchain and does **not** consume Expo cloud build credits. It still uses EAS CLI for config/versioning and local credentials for signing.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
cd mobile-app
|
|
205
|
+
npx eas-cli@latest build --platform ios --profile production \
|
|
206
|
+
--local --non-interactive --output /tmp/{slug}.ipa
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
If the build fails in `expo doctor` due to patch-version mismatches, inspect the exact error. Do not blindly upgrade dependencies during a release unless the build is blocked; if blocked, run `npx expo install --check`, apply the minimal Expo SDK patch updates, rerun `npx tsc --noEmit && npm run lint`, then rebuild.
|
|
210
|
+
|
|
211
|
+
Local `autoIncrement` can skip build numbers if a canceled cloud build already reserved one. That is acceptable for App Store Connect.
|
|
212
|
+
</phase>
|
|
213
|
+
|
|
214
|
+
<phase n="E-expo" title="EAS cloud build (--expo only)">
|
|
215
|
+
Run this phase only when the user passes `--expo`, and confirm once because it consumes Expo/EAS build credits:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
cd mobile-app
|
|
219
|
+
npx eas-cli@latest build --platform ios --profile production --non-interactive --no-wait
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Poll until terminal state (FINISHED / ERRORED):
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npx eas-cli@latest build:list --platform ios --limit 1 --json --non-interactive
|
|
226
|
+
# or: npx eas-cli@latest build:view <BUILD_ID> --json
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
On FINISHED, download the artifact:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
curl -sL -o /tmp/{slug}.ipa "<artifacts.buildUrl>"
|
|
233
|
+
```
|
|
234
|
+
</phase>
|
|
235
|
+
|
|
236
|
+
<phase n="F" title="TestFlight upload">
|
|
237
|
+
**1. Internal beta group** (TestFlight needs a group; `asc publish testflight` fails without `--group`):
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
node -e "const fs=require('fs');fs.writeFileSync('group-req.json',JSON.stringify({data:{type:'betaGroups',attributes:{name:'Internal',isInternalGroup:true,hasAccessToAllBuilds:true},relationships:{app:{data:{type:'apps',id:'{asc_app_id}'}}}}}))"
|
|
241
|
+
node mobile-app/scripts/asc-api.mjs POST /v1/betaGroups group-req.json
|
|
242
|
+
# 409 "already exists" is fine — fetch the id: GET "/v1/apps/{asc_app_id}/betaGroups"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**2. Upload and wait for processing** (asc must be authenticated — `asc auth status --validate`, else `asc auth login` with the same ASC key):
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
asc publish testflight --app "{asc_app_id}" --ipa /tmp/{slug}.ipa \
|
|
249
|
+
--group "<BETA_GROUP_ID>" --wait --timeout 45m
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**3. Add testers** (internal testers must be App Store Connect team members; external emails work via groups):
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
node -e "const fs=require('fs');fs.writeFileSync('tester-req.json',JSON.stringify({data:{type:'betaTesters',attributes:{email:'<tester-email>'},relationships:{betaGroups:{data:[{type:'betaGroups',id:'<BETA_GROUP_ID>'}]}}}}))"
|
|
256
|
+
node mobile-app/scripts/asc-api.mjs POST /v1/betaTesters tester-req.json
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**4. Verify**: `asc status --app "{asc_app_id}" --output table` shows the build processed and attached to the group. Report build number, group, and tester list to the user.
|
|
260
|
+
</phase>
|
|
261
|
+
|
|
262
|
+
<failure_modes>
|
|
263
|
+
- **`.p12` rejected during signing** → it was exported without `-legacy`. Re-export: `openssl pkcs12 -in dist.p12 -nodes -out tmp.pem -passin pass:"$P12PASS" && openssl pkcs12 -export -in tmp.pem -out dist.p12 -passout pass:"$P12PASS" -legacy`.
|
|
264
|
+
- **Certificate creation returns 409 / limit reached** → list existing DISTRIBUTION certs; ask the user which to revoke (`DELETE /v1/certificates/<id>`) or whether they have its .p12 to reuse. Never revoke without confirmation — it breaks other apps signed with it.
|
|
265
|
+
- **Local build fails before Xcode archive** → verify macOS/Xcode/CocoaPods: `xcodebuild -version`, `xcode-select -p`, `command -v pod`. If unavailable and the user accepts cloud quota usage, rerun with `--expo`.
|
|
266
|
+
- **Local build fails asking for credentials** → `credentialsSource: "local"` is missing in `eas.json`, or `credentials.json` paths are wrong (they are relative to `mobile-app/`).
|
|
267
|
+
- **`--expo` cloud build fails asking for credentials** → same credential checks as local, then rerun cloud build.
|
|
268
|
+
- **`asc publish testflight` fails with "--group is required"** → create the beta group first (Phase F step 1).
|
|
269
|
+
- **Upload rejected: missing export compliance** → answer the encryption question in App Store Connect once, or add `"ITSAppUsesNonExemptEncryption": false` to `infoPlist` in `mobile-app/app.config.ts` so future builds skip it.
|
|
270
|
+
- **App opens but can't reach backend** → build was made before `eas.json` had prod URLs, or prod Convex env vars are missing (`npx convex env list --prod`). Rebuild after fixing — env is baked at build time.
|
|
271
|
+
- **User insists on EAS-managed credentials (Apple ID login)** → that flow requires interactive 2FA and cannot run with `--non-interactive`. Run `npx eas-cli@latest credentials` in a real terminal with the user present, then build. Do not attempt to script the 2FA prompt.
|
|
272
|
+
</failure_modes>
|
|
273
|
+
|
|
274
|
+
<success_metrics>
|
|
275
|
+
- `npm run check-setup` passes with a real `easProjectId`.
|
|
276
|
+
- Default local build produces `/tmp/{slug}.ipa` without any interactive prompt; `--expo` cloud builds reach FINISHED.
|
|
277
|
+
- The build shows as processed in TestFlight, attached to a beta group with at least one tester.
|
|
278
|
+
- The TestFlight app signs in and talks to the PROD Convex deployment.
|
|
279
|
+
- `git status` shows no credential files staged; nothing secret printed in the transcript.
|
|
280
|
+
</success_metrics>
|