@mmerterden/multi-agent-pipeline 15.15.0 → 15.16.0
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.
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +62 -67
- package/pipeline/lib/fetch-crashlytics.sh +36 -9
- package/pipeline/multi-agent-refs/features/url-enrichment.md +1 -1
- package/pipeline/multi-agent-refs/keychain.md +5 -1
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +12 -1
- package/pipeline/schemas/prefs.schema.json +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,19 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [15.16.0] - 2026-08-22
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **More than one Firebase project per team.** `keychainMapping.firebase` held exactly one service-account key, which is wrong for the normal case: a legacy app next to its redesign, or staging next to production, each with its own key. A crash URL from the project you did not pick failed the `project_id` check and reported it as a configuration error, which it was, but not the one the message suggested. `global.firebase.accounts[]` maps `projectId` to a keychain key, `fetch-crashlytics.sh` reads the projectId out of the console URL and picks the matching account, and the single slot stays the fallback so a one-project setup needs no config at all. `/multi-agent:setup` now loops the Firebase pass (`Add another Firebase project? [y/N]`), reading `project_id` from each decoded JSON rather than asking for it.
|
|
23
|
+
- A `project_id` mismatch now names the key it used and prints the `accounts[]` entry to add. With several projects in play, "project mismatch" alone does not say whether the URL is wrong or the mapping is incomplete.
|
|
24
|
+
|
|
25
|
+
- **Jira project keys are discovered instead of recalled.** The token is saved and the host is known by the time setup asks for a project key, so it now asks Jira: one search for issues the person assigned or reported, most-recently-updated first, and the distinct project keys become a picker. A corporate instance has thousands of projects and a typed key is a typo that routes branches and new issues at the wrong board. The free-text prompt stays as the fallback for no-VPN and fresh accounts, and the per-repo mapping offers the discovered keys rather than asking for them again.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- `setup/SKILL.md` lost its third copy of the service-ID table and its second copy of the `keychainMapping` shape; both live in `refs/keychain.md`, which the flow already cites. What stayed is the column nothing else had: where to generate each token. The Firebase host-exemption note also stopped being stated twice, two paragraphs apart.
|
|
29
|
+
- The App Store Connect tier reasoning moved into the ref that owns that flow, and `refs/keychain.md` gained the four App Store Connect standard key names so it is now the complete answer for every service setup points at.
|
|
30
|
+
- `smoke-url-enrichment.sh` follows the type label to where it now lives and adds eight assertions: five for the multi-account contract (including that the single-slot fallback survives) and three for Jira discovery and the completeness of the key-name reference.
|
|
31
|
+
|
|
19
32
|
## [15.15.0] - 2026-08-22
|
|
20
33
|
|
|
21
34
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.16.0",
|
|
4
4
|
"description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -92,32 +92,32 @@ DISCOVERED=$(~/.claude/lib/credential-store.sh list 2>/dev/null | grep -i "^${US
|
|
|
92
92
|
echo "$DISCOVERED"
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
**1b.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
| Service ID |
|
|
100
|
-
|
|
101
|
-
| `jira` |
|
|
102
|
-
| `bitbucket_token` |
|
|
103
|
-
| `bitbucket_user` |
|
|
104
|
-
| `github` |
|
|
105
|
-
| `confluence` |
|
|
106
|
-
| `figma` |
|
|
107
|
-
| `figma_mcp` |
|
|
108
|
-
| `fortify` |
|
|
109
|
-
| `graylog` |
|
|
110
|
-
| `graylog_test` |
|
|
111
|
-
| `firebase` |
|
|
112
|
-
| `jenkins` |
|
|
113
|
-
| `appstore_connect_key_id` |
|
|
114
|
-
| `appstore_connect_issuer_id` |
|
|
115
|
-
| `appstore_connect_apple_id` |
|
|
116
|
-
| `appstore_connect_password_item` |
|
|
95
|
+
**1b. Search keywords (per service):**
|
|
96
|
+
|
|
97
|
+
Standard key names - the ones to use when creating a NEW key - live in `$HOME/.claude/multi-agent-refs/keychain.md` "Standard key names"; read the name for a service from there. Existing keys under other names are perfectly valid, which is what these keywords are for.
|
|
98
|
+
|
|
99
|
+
| Service ID | Search Keywords |
|
|
100
|
+
|------------|-----------------|
|
|
101
|
+
| `jira` | `jira` |
|
|
102
|
+
| `bitbucket_token` | `bitbucket` + (`token` or `access` or `pass`) |
|
|
103
|
+
| `bitbucket_user` | `bitbucket` + (`user` or `name`) |
|
|
104
|
+
| `github` | `github` or `gh` (exclude `ssh`) |
|
|
105
|
+
| `confluence` | `confluence` |
|
|
106
|
+
| `figma` | `figma` + (`token` or `pat` or `access`) + NOT `mcp` |
|
|
107
|
+
| `figma_mcp` | `figma` + `mcp` |
|
|
108
|
+
| `fortify` | `fortify` |
|
|
109
|
+
| `graylog` | `graylog` |
|
|
110
|
+
| `graylog_test` | `graylog` + `test` |
|
|
111
|
+
| `firebase` | `firebase` (any variant: `sa`, `service`, `account`, `access`, `json`) |
|
|
112
|
+
| `jenkins` | `jenkins` |
|
|
113
|
+
| `appstore_connect_key_id` | (`appstore` or `asc` or `app_store`) + (`key` or `keyid`) |
|
|
114
|
+
| `appstore_connect_issuer_id` | (`appstore` or `asc` or `app_store`) + `issuer` |
|
|
115
|
+
| `appstore_connect_apple_id` | (`appstore` or `asc`) + (`apple` or `account` or `user`) |
|
|
116
|
+
| `appstore_connect_password_item` | (`appstore` or `asc` or `altool`) + (`password` or `app_specific`) |
|
|
117
117
|
|
|
118
118
|
> Multi-agent setup onboards **both** Figma tokens because the pipeline's 3-tier Figma access chain (see `$HOME/.claude/rules/figma-pipeline.md`) uses them in priority order: Tier 1 = MCP (`figma_mcp`), Tier 2 = REST PAT (`figma`), Tier 3 = user screenshot fallback. Missing either token forces a downshift; missing both leaves Tier 3 as the only path. The Tier 2 logical key is `figma`, not `figma_pat`: `figma_pat` is the pre-v13.6 name that `migrate-prefs.mjs` consolidates away, and writing a new mapping under it means the next migration deletes it.
|
|
119
119
|
|
|
120
|
-
> The four App Store Connect entries are **iOS-only and optional
|
|
120
|
+
> The four App Store Connect entries are **iOS-only and optional** - skipping them all reports Gate 2 of `/multi-agent:store-ready` as `SKIPPED`, never as a pass. Two hold identifiers rather than secrets and one holds a keychain ITEM NAME, yet all four go through the mapping layer so every credential is read the same way. Tiers, the role constraint that decides which to offer first, and the onboarding mechanics: Step 3b and `$HOME/.claude/multi-agent-refs/setup/app-store-connect.md`.
|
|
121
121
|
|
|
122
122
|
**1c. Resolution logic (per service):**
|
|
123
123
|
|
|
@@ -184,27 +184,14 @@ Keychain Token Discovery (${USER}):
|
|
|
184
184
|
|
|
185
185
|
Non-standard key names are perfectly valid - the mapping handles resolution. No rename or migration is offered.
|
|
186
186
|
|
|
187
|
-
Save the resolved mapping to
|
|
187
|
+
Save the resolved mapping to `prefs.global.keychainMapping` - every service ID gets an entry, the discovered key name verbatim or `null` when nothing was found:
|
|
188
|
+
|
|
188
189
|
```json
|
|
189
|
-
{
|
|
190
|
-
"global": {
|
|
191
|
-
"keychainMapping": {
|
|
192
|
-
"jira": "${USER}_Jira_Access_Token",
|
|
193
|
-
"bitbucket_token": "${USER}_Bitbucket_Access_Token",
|
|
194
|
-
"bitbucket_user": "${USER}_Bitbucket_Username",
|
|
195
|
-
"github": "${USER}_Github",
|
|
196
|
-
"confluence": null,
|
|
197
|
-
"figma": "${USER}_Figma_Access_Token",
|
|
198
|
-
"figma_mcp": "${USER}_Figma_Mcp_Access_Token",
|
|
199
|
-
"fortify": null,
|
|
200
|
-
"graylog": null,
|
|
201
|
-
"firebase": "${USER}_Firebase_Access_Json",
|
|
202
|
-
"jenkins": null
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
190
|
+
{ "jira": "${USER}_Jira_Access_Token", "github": "${USER}_Github", "confluence": null, "...": "..." }
|
|
206
191
|
```
|
|
207
192
|
|
|
193
|
+
Full key list and shape: `$HOME/.claude/multi-agent-refs/keychain.md`.
|
|
194
|
+
|
|
208
195
|
`null` = not mapped (missing or skipped). Pipeline phases read this mapping to retrieve tokens dynamically - never hardcoded key names.
|
|
209
196
|
|
|
210
197
|
### Step 2.7 - Operational reporting token (optional, opt-in)
|
|
@@ -370,6 +357,12 @@ pbcopy < /dev/null # clear clipboard
|
|
|
370
357
|
|
|
371
358
|
Linux substitutes: `xclip -selection clipboard -o` or `wl-paste` for `pbpaste`; matching clear command for `pbcopy < /dev/null`. The shell driver detects platform and routes accordingly (see `~/.claude/lib/credential-store.sh`).
|
|
372
359
|
|
|
360
|
+
**Firebase repeats per project.** Teams own several Firebase projects (legacy plus redesign, staging plus prod), each with its own key, and a crash URL from the second one used to fail the `project_id` check as if it were misconfigured. After each saved key, read `project_id` from the decoded JSON (never ask) and offer another round: `Saved: <project_id>. Add another Firebase project? [y/N]`.
|
|
361
|
+
|
|
362
|
+
- Every key appends `{projectId, keychainKey, label?}` to `prefs.global.firebase.accounts`; the first one also fills `keychainMapping.firebase`, so a one-project setup is unchanged.
|
|
363
|
+
- Keys after the first are named `${USER}_Firebase_Access_Json_<projectId>` so they cannot collide.
|
|
364
|
+
- At run time `fetch-crashlytics.sh` picks the account matching the console URL's projectId, falling back to `keychainMapping.firebase`.
|
|
365
|
+
|
|
373
366
|
For GitHub (special case):
|
|
374
367
|
```
|
|
375
368
|
! gh auth login
|
|
@@ -409,8 +402,7 @@ Update `serviceStatus.{service} = { ok: true, checkedAt: <now> }`.
|
|
|
409
402
|
**→ Next: if `{service}` is `jira` / `confluence` / `bitbucket` / `fortify` / `graylog` AND the corresponding
|
|
410
403
|
`prefs.global.hosts.{service}` is unset, run Step 3.5 (Host Prompt) inline before
|
|
411
404
|
the Auto-routing rule below. required - the pipeline cannot build API URLs without
|
|
412
|
-
the host. Firebase is
|
|
413
|
-
`firebasecrashlytics.googleapis.com`); the SA JSON itself carries `project_id`.**
|
|
405
|
+
the host. Firebase is exempt (see Step 3.5).**
|
|
414
406
|
|
|
415
407
|
**Auto-routing rule** (optional, on first token per platform):
|
|
416
408
|
|
|
@@ -431,7 +423,7 @@ Runs inside Step 3 alongside the other missing credentials. The four `appstore_c
|
|
|
431
423
|
|
|
432
424
|
### Step 3.5 - Host Prompt (embedded in Token Save Flow)
|
|
433
425
|
|
|
434
|
-
**Not a standalone step** - runs inline at the end of the Token Save Flow whenever the saved token belongs to a **hosted service** (Jira, Confluence, Bitbucket, Fortify, Graylog) AND the host is not yet in `prefs.global.hosts`. Firebase tokens skip this step - Crashlytics is always on Google's fixed domains and `project_id` is embedded in the
|
|
426
|
+
**Not a standalone step** - runs inline at the end of the Token Save Flow whenever the saved token belongs to a **hosted service** (Jira, Confluence, Bitbucket, Fortify, Graylog) AND the host is not yet in `prefs.global.hosts`. Firebase tokens skip this step - Crashlytics is always on Google's fixed domains and `project_id` is embedded in the service-account JSON.
|
|
435
427
|
|
|
436
428
|
Right after Step D (identity mapping confirmed), ask:
|
|
437
429
|
|
|
@@ -443,27 +435,28 @@ Service host - needed to build API URLs for this token.
|
|
|
443
435
|
|
|
444
436
|
Save to `prefs.global.hosts.{service}`. For Fortify, also ask `Fortify project version ids (optional, comma-separated - e.g. 1234,5678)` → `prefs.global.fortify.versionIds`. A ticket naming only an instance id carries no version, so the lookup silently no-ops without these; URL-referenced findings resolve either way.
|
|
445
437
|
|
|
446
|
-
For Jira
|
|
438
|
+
For Jira, **discover the project keys instead of asking the user to recall them.** The token is already saved and the host is already known, so ask Jira which projects this person actually works in - a corporate instance has thousands of projects, and a typed key is a typo waiting to route branches and new issues at the wrong board:
|
|
447
439
|
|
|
448
|
-
```
|
|
449
|
-
|
|
440
|
+
```bash
|
|
441
|
+
curl -s -H "Authorization: Bearer $TOKEN" \
|
|
442
|
+
"https://{JIRA_HOST}/rest/api/2/search?jql=assignee%3DcurrentUser()%20OR%20reporter%3DcurrentUser()%20ORDER%20BY%20updated%20DESC&fields=project&maxResults=100" \
|
|
443
|
+
| jq -r '[.issues[].fields.project | "\(.key)\t\(.name)"] | unique | .[]'
|
|
450
444
|
```
|
|
451
445
|
|
|
452
|
-
|
|
446
|
+
Present the distinct keys, most-recently-worked first, as a picker; fall back to the free-text prompt `Default Jira project key (e.g. PROJ)` when the call fails or returns nothing (no VPN, fresh account). The picked key → `prefs.global.defaultJiraKey`. From the Jira host, derive `corpDomain` (`jira.example.com` → `example.com`) and pre-fill it so the Confluence / Bitbucket prompts only need the subdomain.
|
|
453
447
|
|
|
454
|
-
`defaultJiraKey`
|
|
448
|
+
`defaultJiraKey` is a single global fallback, but one Jira host usually serves several repos with DIFFERENT keys, so the pass continues with a per-repo mapping. Show the repos, and let the key come from the discovered list rather than being typed again:
|
|
455
449
|
|
|
456
450
|
```
|
|
457
451
|
Per-repo Jira keys (optional) - repos whose project key differs from {defaultJiraKey}:
|
|
458
452
|
|
|
459
453
|
[ ] 1. my-ios-app (current: PROJA)
|
|
460
454
|
[ ] 2. my-other-app (current: - )
|
|
461
|
-
...
|
|
462
455
|
|
|
463
|
-
Toggle repos, then
|
|
456
|
+
Toggle repos, then pick a project for each - discovered: PROJA, PROJB, PROJC. Enter to skip.
|
|
464
457
|
```
|
|
465
458
|
|
|
466
|
-
- The repo list comes from `prefs.projects` + `recentProjects` (when Step 5 already ran); a free-text repo path is accepted for repos not discovered yet.
|
|
459
|
+
- The repo list comes from `prefs.projects` + `recentProjects` (when Step 5 already ran); a free-text repo path is accepted for repos not discovered yet, and a free-text key for a project the search did not surface.
|
|
467
460
|
- Each answer is PREPENDED to `prefs.projects[{slug}].jiraProjectKeys` (deduped, max 10 per schema). Example: `my-ios-app` → `PROJA`, `my-other-app` → `PROJB` - two repos on the same Jira host, two different project keys.
|
|
468
461
|
- Resolution order everywhere a Jira key is needed (placeholder replacement, branch names, new-issue creation): `prefs.projects[{slug}].jiraProjectKeys[0]` first, `global.defaultJiraKey` as fallback.
|
|
469
462
|
- **Re-run / update**: `/multi-agent:setup jira-keys` re-opens only this mapping without touching tokens or hosts.
|
|
@@ -709,21 +702,23 @@ If no backend is available, setup falls back to a plain-text prompt + a warning
|
|
|
709
702
|
|
|
710
703
|
Standard key names (used when creating NEW tokens). Discovery may find existing keys under different names - that's fine, the mapping in preferences handles it.
|
|
711
704
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
|
715
|
-
|
|
716
|
-
| `
|
|
717
|
-
| `
|
|
718
|
-
| `
|
|
719
|
-
| `
|
|
720
|
-
| `
|
|
721
|
-
| `
|
|
722
|
-
| `
|
|
723
|
-
| `
|
|
724
|
-
| `
|
|
725
|
-
| `
|
|
726
|
-
|
|
|
705
|
+
Standard key names and token types: `$HOME/.claude/multi-agent-refs/keychain.md` "Standard key names". Where to generate each one:
|
|
706
|
+
|
|
707
|
+
| Service ID | Where to Get |
|
|
708
|
+
|------------|---------------|
|
|
709
|
+
| `jira` | Jira -> Profile -> Personal Access Tokens (VPN required) |
|
|
710
|
+
| `bitbucket_token` | Bitbucket -> Personal settings -> App passwords |
|
|
711
|
+
| `bitbucket_user` | Bitbucket profile username (plain text, not a PAT) |
|
|
712
|
+
| `github` | GitHub Settings -> Tokens (scopes: repo, read:org, project) |
|
|
713
|
+
| `confluence` | Confluence -> Profile -> Personal Access Tokens (VPN required) |
|
|
714
|
+
| `figma` | Figma Developer Settings (max 90 days). Tier 2 / REST. Prompt copy: "Figma Personal Access Token (for Tier 2)". |
|
|
715
|
+
| `figma_mcp` | Automatic via Claude Code Figma MCP remote auth. Tier 1 / MCP. Prompt copy: "Figma MCP token (for Tier 1)". |
|
|
716
|
+
| `fortify` | Fortify SSC -> Token Management (VPN required) |
|
|
717
|
+
| `graylog` | Graylog -> System -> Users and Teams -> Edit Tokens (VPN required) |
|
|
718
|
+
| `graylog_test` | Same page on the TEST instance; optional |
|
|
719
|
+
| `firebase` | Firebase Console -> Project settings -> Service accounts -> Generate new private key. One key per Firebase project; `project_id` is parsed from the decoded JSON, never asked. |
|
|
720
|
+
| `jenkins` | Jenkins -> User -> Configure -> API Token |
|
|
721
|
+
| Git Identities | Stored in `$HOME/.claude/multi-agent-preferences.json`, not Keychain |
|
|
727
722
|
|
|
728
723
|
### Inline Token Save Pattern
|
|
729
724
|
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
#
|
|
14
14
|
# Optional env:
|
|
15
15
|
# FIREBASE_TIMEOUT_SECONDS default 20
|
|
16
|
-
# FIREBASE_TOKEN_KEY
|
|
17
|
-
#
|
|
16
|
+
# FIREBASE_TOKEN_KEY forces one keychain key, skipping account lookup
|
|
17
|
+
#
|
|
18
|
+
# Credential resolution, in order:
|
|
19
|
+
# 1. FIREBASE_TOKEN_KEY (env override)
|
|
20
|
+
# 2. prefs.global.firebase.accounts[] entry whose projectId matches the URL
|
|
21
|
+
# 3. prefs.global.keychainMapping.firebase (single-project setups)
|
|
22
|
+
# 4. "${USER}_Firebase_Access_Json"
|
|
18
23
|
#
|
|
19
24
|
# Output (stdout, single JSON object):
|
|
20
25
|
# {
|
|
@@ -116,18 +121,35 @@ if [ -z "$PROJECT_ID" ] || [ -z "$PLATFORM" ] || [ -z "$BUNDLE" ] || [ -z "$ISSU
|
|
|
116
121
|
exit 4
|
|
117
122
|
fi
|
|
118
123
|
|
|
119
|
-
# Resolve Firebase SA JSON via keychain mapping (matches Phase 0 contract).
|
|
124
|
+
# Resolve the Firebase SA JSON via keychain mapping (matches Phase 0 contract).
|
|
125
|
+
#
|
|
126
|
+
# One team routinely owns several Firebase projects - a legacy app and its
|
|
127
|
+
# redesign, or staging next to production - and each has its own service-account
|
|
128
|
+
# key. A single `keychainMapping.firebase` slot forced a choice, and a crash URL
|
|
129
|
+
# from the other project then failed the project_id check as if it were a
|
|
130
|
+
# configuration error. `global.firebase.accounts[]` maps projectId -> keychain
|
|
131
|
+
# key, and the URL's projectId picks the key. The single slot stays the fallback,
|
|
132
|
+
# so a one-project setup needs no config at all.
|
|
120
133
|
PREFS="$HOME/.claude/multi-agent-preferences.json"
|
|
121
134
|
TOKEN_KEY="${FIREBASE_TOKEN_KEY:-}"
|
|
135
|
+
FIREBASE_ACCOUNT_LABEL=""
|
|
122
136
|
if [ -z "$TOKEN_KEY" ] && [ -f "$PREFS" ]; then
|
|
123
|
-
|
|
124
|
-
import json
|
|
137
|
+
ACCOUNT_HIT=$(WANT_PROJECT="$PROJECT_ID" python3 -c "
|
|
138
|
+
import json, os
|
|
139
|
+
want = os.environ.get('WANT_PROJECT') or ''
|
|
125
140
|
try:
|
|
126
|
-
|
|
127
|
-
print(p.get('global', {}).get('keychainMapping', {}).get('firebase') or '')
|
|
141
|
+
g = json.load(open('$PREFS')).get('global', {})
|
|
128
142
|
except Exception:
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
g = {}
|
|
144
|
+
for a in (g.get('firebase', {}) or {}).get('accounts') or []:
|
|
145
|
+
if isinstance(a, dict) and str(a.get('projectId') or '') == want and a.get('keychainKey'):
|
|
146
|
+
print('%s\t%s' % (a['keychainKey'], a.get('label') or a['projectId']))
|
|
147
|
+
break
|
|
148
|
+
else:
|
|
149
|
+
print('%s\t' % ((g.get('keychainMapping', {}) or {}).get('firebase') or ''))
|
|
150
|
+
" 2>/dev/null || printf '\t')
|
|
151
|
+
TOKEN_KEY=$(printf '%s' "$ACCOUNT_HIT" | cut -f1)
|
|
152
|
+
FIREBASE_ACCOUNT_LABEL=$(printf '%s' "$ACCOUNT_HIT" | cut -f2)
|
|
131
153
|
fi
|
|
132
154
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Firebase_Access_Json"
|
|
133
155
|
|
|
@@ -199,7 +221,12 @@ except Exception:
|
|
|
199
221
|
print('')
|
|
200
222
|
")
|
|
201
223
|
if [ -n "$SA_PROJECT" ] && [ "$SA_PROJECT" != "$PROJECT_ID" ]; then
|
|
224
|
+
# Still a hard error, but say which key was used and how to map the right one:
|
|
225
|
+
# with several Firebase projects in play, "project mismatch" alone does not
|
|
226
|
+
# tell you whether the URL is wrong or the mapping is missing an account.
|
|
202
227
|
echo "ERR: SA project_id ($SA_PROJECT) != URL project ($PROJECT_ID)" >&2
|
|
228
|
+
echo " key used: $TOKEN_KEY${FIREBASE_ACCOUNT_LABEL:+ (account: $FIREBASE_ACCOUNT_LABEL)}" >&2
|
|
229
|
+
echo " map this project: prefs.global.firebase.accounts[] += {\"projectId\":\"$PROJECT_ID\",\"keychainKey\":\"<keychain-key>\"}" >&2
|
|
203
230
|
exit 5
|
|
204
231
|
fi
|
|
205
232
|
|
|
@@ -45,7 +45,7 @@ The catalogue is the single source of truth for downstream phases - they read
|
|
|
45
45
|
URL pattern (caught by the extractor): `console.firebase.google.com/(u/[0-9]+/)?project/<projectId>/crashlytics/app/(ios|android)(:|%3A)<bundleOrPackage>/issues/<issueId>(/sessions/<sessionId>)?`
|
|
46
46
|
|
|
47
47
|
- Host is fixed (`console.firebase.google.com`); no `hosts.firebase` pref needed.
|
|
48
|
-
- Token resolution: `prefs.global.keychainMapping.firebase`
|
|
48
|
+
- Token resolution, in order: env `FIREBASE_TOKEN_KEY`; the `prefs.global.firebase.accounts[]` entry whose `projectId` equals the URL's project; `prefs.global.keychainMapping.firebase` (single-project setups). Then `~/.claude/lib/credential-store.sh get "<key>" | base64 -d` → Service Account JSON. `project_id` read from the decoded JSON - verify it matches the `<projectId>` from the URL; mismatch → the fetcher exits `5` naming the key it used and the `accounts[]` entry to add, and enrichment is skipped. Several Firebase projects per team is the normal case (legacy plus redesign, staging plus prod), which is what `accounts[]` exists for.
|
|
49
49
|
- Exchange the SA JSON for a short-lived GCP access token (scope `https://www.googleapis.com/auth/firebase https://www.googleapis.com/auth/cloud-platform`) using `google-auth` (Python) or an inline JWT exchange (OpenSSL sign → `https://oauth2.googleapis.com/token`). Cache in-memory for the pipeline run only - never persist.
|
|
50
50
|
- Resolve the opaque `appId` first: `GET https://firebase.googleapis.com/v1beta1/projects/<projectId>/(iosApps|androidApps)?pageSize=200` and match `bundleId` (iOS) / `packageName` (Android) against the bundle from the URL. The console URL only carries the bundle, and the Crashlytics API only accepts the appId (`1:1234567890:ios:abcdef`). No match in a multi-app project → exit `3`, reason `app-not-found`; never guess at another app.
|
|
51
51
|
- Fetch crash detail with two calls - v1alpha has no get-issue-by-id endpoint: `GET .../v1alpha/projects/<projectId>/apps/<appId>/reports/topIssues?pageSize=200` for the summary and metrics (filter to the issue id), and `GET .../apps/<appId>/events?filter.issue.id=<issueId>&pageSize=1` for the newest event, which carries the stack frames, device, OS and breadcrumbs.
|
|
@@ -129,8 +129,12 @@ The shell driver auto-delegates to `~/.claude/scripts/keychain.py` on macOS / Li
|
|
|
129
129
|
| `fortify` | Fortify | `${USER}_Fortify_Access_Token` | API Token |
|
|
130
130
|
| `graylog` | Graylog (prod) | `${USER}_Graylog_Access_Token` | API Token |
|
|
131
131
|
| `graylog_test` | Graylog (test) | `${USER}_Graylog_Test_Access_Token` | API Token, optional - unset falls back to `graylog` |
|
|
132
|
-
| `firebase` | Firebase | `${USER}_Firebase_Access_Json` | JSON (base64)
|
|
132
|
+
| `firebase` | Firebase | `${USER}_Firebase_Access_Json` | JSON (base64). One key per Firebase project; extras are named `..._Json_<projectId>` and listed in `global.firebase.accounts[]` |
|
|
133
133
|
| `jenkins` | Jenkins CI | `${USER}_Jenkins_Access_Token` | API Token |
|
|
134
|
+
| `appstore_connect_key_id` | App Store Connect | `${USER}_AppStoreConnect_Key_Id` | Identifier, not a secret |
|
|
135
|
+
| `appstore_connect_issuer_id` | App Store Connect | `${USER}_AppStoreConnect_Issuer_Id` | Identifier, not a secret |
|
|
136
|
+
| `appstore_connect_apple_id` | App Store Connect | `${USER}_AppStoreConnect_Apple_Id` | Email address |
|
|
137
|
+
| `appstore_connect_password_item` | App Store Connect | `${USER}_AppStoreConnect_Password_Item` | Keychain ITEM NAME, not a password |
|
|
134
138
|
| - | Git Identity | Stored in preferences JSON | Not Keychain |
|
|
135
139
|
|
|
136
140
|
**Key name mapping lives in preferences:**
|
|
@@ -17,9 +17,20 @@ what they hold is not a pasteable secret:
|
|
|
17
17
|
| `appstore_connect_apple_id` | an email address | same |
|
|
18
18
|
| `appstore_connect_password_item` | a keychain ITEM NAME | the password lives in Apple's own keychain item, referenced as `-p @keychain:<item>` and never read by the pipeline |
|
|
19
19
|
|
|
20
|
+
All four entries are **iOS-only and optional**: skip them all and the pipeline
|
|
21
|
+
still works, it just reports Gate 2 of `/multi-agent:store-ready` as `SKIPPED`,
|
|
22
|
+
never as a pass.
|
|
23
|
+
|
|
24
|
+
The tiers mirror the Figma 3-tier shape: Tier 1 = API key (`appstore_connect_key_id`
|
|
25
|
+
+ `appstore_connect_issuer_id`), Tier 2 = Apple ID + app-specific password
|
|
26
|
+
(`appstore_connect_apple_id` + `appstore_connect_password_item`), Tier 3 = nothing
|
|
27
|
+
configured.
|
|
28
|
+
|
|
20
29
|
Ask which tier to configure (picker): **API key** / **Apple ID + app-specific
|
|
21
30
|
password** / **Skip**. Lead with the second when the user says they cannot create
|
|
22
|
-
an API key
|
|
31
|
+
an API key: creating one needs an Admin or App Manager role in App Store Connect,
|
|
32
|
+
while an app-specific password is generated by the account holder at
|
|
33
|
+
`appleid.apple.com` with no team permission at all.
|
|
23
34
|
|
|
24
35
|
**API key.** The private key is a FILE and is never copied into the credential
|
|
25
36
|
store. It must sit in a directory `altool` already searches:
|
|
@@ -590,6 +590,36 @@
|
|
|
590
590
|
}
|
|
591
591
|
}
|
|
592
592
|
},
|
|
593
|
+
"firebase": {
|
|
594
|
+
"type": "object",
|
|
595
|
+
"additionalProperties": false,
|
|
596
|
+
"description": "v15.16+ - Firebase service-account keys. One team routinely owns several Firebase projects (a legacy app and its redesign, staging next to production), each with its own key, and the single keychainMapping.firebase slot forced a choice: a crash URL from the other project then failed the project_id check as if it were misconfigured. Leave this out entirely for a one-project setup - the single slot stays the fallback.",
|
|
597
|
+
"properties": {
|
|
598
|
+
"accounts": {
|
|
599
|
+
"type": "array",
|
|
600
|
+
"description": "Maps a Firebase projectId to the keychain key holding its service-account JSON. fetch-crashlytics.sh reads the projectId out of the console URL and picks the matching entry; no match falls back to keychainMapping.firebase.",
|
|
601
|
+
"items": {
|
|
602
|
+
"type": "object",
|
|
603
|
+
"additionalProperties": false,
|
|
604
|
+
"required": ["projectId", "keychainKey"],
|
|
605
|
+
"properties": {
|
|
606
|
+
"projectId": {
|
|
607
|
+
"type": "string",
|
|
608
|
+
"description": "Firebase project id exactly as it appears in a console URL, e.g. my-app-prod-1234."
|
|
609
|
+
},
|
|
610
|
+
"keychainKey": {
|
|
611
|
+
"type": "string",
|
|
612
|
+
"description": "Keychain key holding this project's service-account JSON (base64 or raw)."
|
|
613
|
+
},
|
|
614
|
+
"label": {
|
|
615
|
+
"type": "string",
|
|
616
|
+
"description": "Human label used in error output, e.g. \"redesign prod\". Defaults to the projectId."
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
},
|
|
593
623
|
"fortify": {
|
|
594
624
|
"type": "object",
|
|
595
625
|
"additionalProperties": false,
|