@mmerterden/multi-agent-pipeline 14.1.1 → 14.2.2
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 +162 -0
- package/README.md +10 -6
- package/README.tr.md +143 -0
- package/docs/architecture.md +23 -8
- package/docs/ecosystem.md +237 -0
- package/install/_plugin-skills.mjs +16 -2
- package/install/codex.mjs +9 -4
- package/install/templates/copilot-instructions.md +12 -9
- package/package.json +1 -1
- package/pipeline/commands/deploy.md +4 -1
- package/pipeline/commands/multi-agent/SKILL.md +8 -5
- package/pipeline/commands/multi-agent/analysis/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -4
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
- package/pipeline/commands/multi-agent/help/SKILL.md +49 -11
- package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/ship/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +340 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +7 -6
- package/pipeline/commands/multi-agent/test/SKILL.md +18 -8
- package/pipeline/commands/multi-agent/test-accessibility/SKILL.md +33 -0
- package/pipeline/commands/multi-agent/test-dark-mode/SKILL.md +33 -0
- package/pipeline/commands/multi-agent/test-dynamic-type/SKILL.md +33 -0
- package/pipeline/commands/multi-agent/test-screenshots/SKILL.md +41 -0
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +28 -201
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/commands/sim-test.md +45 -36
- package/pipeline/lib/extract-conventions.sh +44 -15
- package/pipeline/lib/fetch-figma-annotations.sh +8 -1
- package/pipeline/lib/fetch-fortify.sh +23 -8
- package/pipeline/lib/figma-screenshot.sh +11 -1
- package/pipeline/lib/issue-fetcher.sh +76 -9
- package/pipeline/lib/md2confluence-v3.py +16 -2
- package/pipeline/lib/plan-todos.sh +5 -2
- package/pipeline/lib/post-pr-review.sh +8 -6
- package/pipeline/lib/shadow-git.sh +50 -9
- package/pipeline/lib/submodule-detector.sh +8 -1
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
- package/pipeline/multi-agent-refs/channels/jira.md +13 -2
- package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
- package/pipeline/multi-agent-refs/channels/pr.md +20 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +6 -5
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +2 -2
- package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
- package/pipeline/multi-agent-refs/knowledge.md +1 -1
- package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
- package/pipeline/multi-agent-refs/phases/modes.md +20 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
- package/pipeline/multi-agent-refs/phases.md +6 -0
- package/pipeline/multi-agent-refs/rules.md +2 -0
- package/pipeline/schemas/prefs.schema.json +2 -2
- package/pipeline/scripts/audit-log-rotate.sh +10 -0
- package/pipeline/scripts/build-stack-plugins.mjs +8 -1
- package/pipeline/scripts/check-derived-drift.mjs +13 -1
- package/pipeline/scripts/diff-explain.mjs +41 -3
- package/pipeline/scripts/diff-risk-score.mjs +72 -8
- package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
- package/pipeline/scripts/learning-curve.mjs +8 -2
- package/pipeline/scripts/output-quality-check.sh +15 -4
- package/pipeline/scripts/phase-tracker.sh +21 -8
- package/pipeline/scripts/pre-commit-check.sh +69 -22
- package/pipeline/scripts/render-agent-log-cost.sh +8 -3
- package/pipeline/scripts/render-cost-summary.sh +42 -22
- package/pipeline/scripts/render-work-summary.sh +47 -13
- package/pipeline/scripts/review-scope.mjs +1 -1
- package/pipeline/scripts/run-aggregator.mjs +38 -14
- package/pipeline/scripts/smoke-schema-validation.sh +5 -1
- package/pipeline/scripts/test-gap-scan.mjs +45 -6
- package/pipeline/scripts/uninstall.mjs +39 -4
- package/pipeline/scripts/update-issue-progress.sh +12 -16
- package/pipeline/scripts/worktree-finalize.sh +23 -2
- package/pipeline/skills/.skills-index.json +58 -4
- package/pipeline/skills/shared/README.md +12 -7
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +13 -17
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +50 -12
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-purge/SKILL.md +18 -3
- package/pipeline/skills/shared/core/multi-agent-ship/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +50 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +6 -5
- package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +18 -8
- package/pipeline/skills/shared/core/multi-agent-test-accessibility/SKILL.md +37 -0
- package/pipeline/skills/shared/core/multi-agent-test-dark-mode/SKILL.md +37 -0
- package/pipeline/skills/shared/core/multi-agent-test-dynamic-type/SKILL.md +37 -0
- package/pipeline/skills/shared/core/multi-agent-test-screenshots/SKILL.md +44 -0
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +29 -101
- package/pipeline/skills/shared/external/firebase/SKILL.md +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +144 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +175 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
- package/pipeline/skills/skills-index.md +10 -4
|
@@ -1,219 +1,46 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "
|
|
3
|
-
description-tr: "
|
|
2
|
+
description: "iOS-pinned alias of store-ready: same three gates on an iOS build, one implementation. Validates a TestFlight / App Store package, never uploads. Use when an iOS build is about to go to TestFlight, or a submission was rejected and you need why."
|
|
3
|
+
description-tr: "store-ready komutunun iOS'a sabitlenmiş alias'ı: iOS build'inde aynı üç kapı, tek implementasyon. TestFlight / App Store paketini doğrular, asla yüklemez."
|
|
4
4
|
argument-hint: "[repo] - empty = pick from prefs; repo name or path; --ipa=<path>; --archive=<path>; --resume"
|
|
5
5
|
allowed-tools: Agent, Bash, Read, Write, Edit, Glob, Grep, TaskCreate, TaskUpdate, AskUserQuestion, Skill, mcp__dev-toolkit__ios_app_store_audit, mcp__dev-toolkit__ios_export_ipa, mcp__dev-toolkit__ios_testflight_validate, mcp__dev-toolkit__ios_xcodebuild, mcp__dev-toolkit__ios_xcresult
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# /multi-agent:testflight-validation -
|
|
8
|
+
# /multi-agent:testflight-validation - iOS alias for store-ready
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
This command is an alias. The implementation it used to carry was merged into
|
|
11
|
+
`/multi-agent:store-ready`, which runs the same three gates on iOS and adds the
|
|
12
|
+
Android side, so there is one flow to maintain instead of two that had already
|
|
13
|
+
started to drift.
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
The name is kept because it is the one people reach for when the target is
|
|
16
|
+
TestFlight, and removing a command is a breaking change to the slash-command
|
|
17
|
+
surface.
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
A validation run must not be able to ship a build by accident.
|
|
19
|
+
## Dispatcher
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Each gate sees something the others structurally cannot. Reporting one of them as
|
|
21
|
-
"the check" is how a build passes locally and gets rejected anyway.
|
|
22
|
-
|
|
23
|
-
| Gate | What runs | Needs | Sees | Blind to |
|
|
24
|
-
|---|---|---|---|---|
|
|
25
|
-
| **1. Static** | `ios_app_store_audit` (18 rules, real ITMS codes) | an `.xcarchive` | privacy manifest, required-reason API, Info.plist, code signing, entitlements, embedded SDK, IPv6, debug-tool leak, binary size | anything that depends on the App Store Connect account |
|
|
26
|
-
| **2. Authoritative** | `ios_testflight_validate` → `altool --validate-app` | an `.ipa` + credentials | unregistered bundle ID, profile that does not match the app record, **a version+build pair already used**, entitlements not provisioned for the App ID | the Review Guidelines - Apple's validator does not read them |
|
|
27
|
-
| **3. Guideline** | `app-store-review` skill + repo evidence | repo checkout | ATT flow, privacy policy, account deletion, IAP rules, purpose-string wording, permission justification | anything not visible in source |
|
|
28
|
-
|
|
29
|
-
Gate 2 is the only one that asks Apple, and Gate 3 is the only one that covers the
|
|
30
|
-
rejections a human reviewer writes. Most "we passed validation and still got
|
|
31
|
-
rejected" cases are Gate 3 findings.
|
|
32
|
-
|
|
33
|
-
## Step 0 - parse input
|
|
34
|
-
|
|
35
|
-
| Input | Meaning |
|
|
36
|
-
|---|---|
|
|
37
|
-
| (empty) | ask which repo (Step 1) |
|
|
38
|
-
| `my-ios-app` or a path | that repo |
|
|
39
|
-
| `--ipa=<path>` | Mode B with an `.ipa`; Gate 1 cannot run (see Step 3) |
|
|
40
|
-
| `--archive=<path>` | Mode B with an `.xcarchive`; all three gates run |
|
|
41
|
-
| `--resume` | continue the last run from its state file |
|
|
42
|
-
|
|
43
|
-
State lives at `$HOME/.claude/logs/multi-agent/<task_id>/agent-state.json` with
|
|
44
|
-
`taskId = TFV-<repo>-<yyyymmddHHMM>`. Register phases with the tracker
|
|
45
|
-
(`$HOME/.claude/multi-agent-refs/tracker-contract.md`) so `:resume` and `:status`
|
|
46
|
-
work like any other run.
|
|
47
|
-
|
|
48
|
-
## Step 1 - pickers (native, always)
|
|
49
|
-
|
|
50
|
-
Use `AskUserQuestion` for every step - never a numbered text menu. Questions and
|
|
51
|
-
descriptions render in `prefs.global.outputLanguage`; `label` and `header` stay
|
|
52
|
-
English, per `$HOME/.claude/multi-agent-refs/picker-contract.md`. Print the
|
|
53
|
-
`Step <i>/<n>: <what this decides>` breadcrumb for each.
|
|
54
|
-
|
|
55
|
-
1. **Repo** - from `prefs.projects` where the stack is iOS. A single match
|
|
56
|
-
auto-resolves (say so in the breadcrumb, do not silently skip the step).
|
|
57
|
-
2. **Branch** - the branch to validate. Resolution order:
|
|
58
|
-
- `git fetch --prune` first, capturing **stderr**. **If the fetch fails, do not
|
|
59
|
-
silently fall back to a cached ref**, and **classify before naming a cause** -
|
|
60
|
-
the same rule as the `/multi-agent:dev` remote gate:
|
|
61
|
-
|
|
62
|
-
| stderr contains | Cause | Remedy |
|
|
63
|
-
|---|---|---|
|
|
64
|
-
| `could not read Password`, `Authentication failed`, `403` | credential | store the PAT in the credential helper or switch the remote to SSH. **A VPN cannot fix this**, and the base ref being stale is unrelated to what broke - do not offer the cached-ref fallback. |
|
|
65
|
-
| `Could not resolve host`, `Operation timed out`, `Connection refused` | network | retry / continue on the cached ref with an explicit warning / switch remote / abort, per `$HOME/.claude/multi-agent-refs/rules.md` |
|
|
66
|
-
| `Repository not found`, `404` | wrong remote | show `git remote -v` and ask |
|
|
67
|
-
|
|
68
|
-
Always print the observed stderr line next to the classification. Asserting
|
|
69
|
-
`unreachable (VPN/DNS)` for a missing-credential error that returns in under a
|
|
70
|
-
second sends the user to fix something that was never broken.
|
|
71
|
-
- Offer the current branch, the default branch, and any `release/*` /
|
|
72
|
-
`tkdevelop/*` heads.
|
|
73
|
-
3. **Mode** - how the build is obtained:
|
|
74
|
-
- `Supply a build` (Mode B, default) - fastest, no signing needed in-run.
|
|
75
|
-
- `Archive from this branch` (Mode A) - needs a distribution certificate and
|
|
76
|
-
profile in the keychain, and takes as long as a release archive.
|
|
77
|
-
|
|
78
|
-
## Step 2 - pre-flight, before anything expensive
|
|
79
|
-
|
|
80
|
-
Report every line; a missing prerequisite is a halt, not a warning.
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
xcrun --find altool >/dev/null 2>&1 || echo "MISSING: altool (install Xcode)"
|
|
84
|
-
xcodebuild -version | head -1
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Then resolve credentials, and **state which tier is active in the report**:
|
|
88
|
-
|
|
89
|
-
| Tier | Source | Effect |
|
|
90
|
-
|---|---|---|
|
|
91
|
-
| 1 | ASC API key - key id + issuer id from the keychain via `prefs.global.keychainMapping`, `.p8` at `~/.appstoreconnect/private_keys/AuthKey_<keyId>.p8` | Gate 2 runs |
|
|
92
|
-
| 2 | Apple ID + app-specific password, referenced as a keychain item | Gate 2 runs |
|
|
93
|
-
| 3 | neither | **Gate 2 reports `SKIPPED`, and the run says so in the verdict line** |
|
|
94
|
-
|
|
95
|
-
Credentials come from `/multi-agent:setup`; never prompt for a secret value in
|
|
96
|
-
chat. If nothing is configured, tell the user which of the two tiers they can set
|
|
97
|
-
up and that tier 2 needs no elevated App Store Connect role.
|
|
98
|
-
|
|
99
|
-
Multi-provider accounts need `--provider-public-id`. When it is not in prefs, run
|
|
100
|
-
`ios_testflight_validate({list_providers: true})` once and ask which provider.
|
|
101
|
-
|
|
102
|
-
## Step 3 - obtain the build
|
|
103
|
-
|
|
104
|
-
### Mode B - a build you supply
|
|
105
|
-
|
|
106
|
-
- `.xcarchive` → Gate 1 runs on it. To reach Gate 2 the archive must be exported,
|
|
107
|
-
so run `ios_export_ipa` (see Mode A step 3 for the signing inputs).
|
|
108
|
-
- `.ipa` only → **Gate 1 is reported `SKIPPED (needs .xcarchive)`.** The static
|
|
109
|
-
audit reads archive structure that an `.ipa` does not carry. Do not present a
|
|
110
|
-
two-gate run as a full pass; say which gate did not run and why, and offer to
|
|
111
|
-
re-run with the archive.
|
|
112
|
-
|
|
113
|
-
### Mode A - archive from the branch
|
|
114
|
-
|
|
115
|
-
1. Worktree at `{projectRoot}/{worktreeBasePath}/{taskId}` on the chosen branch.
|
|
116
|
-
**Never under `$HOME`**, never a direct checkout of the main working tree.
|
|
117
|
-
2. Resolve the scheme and workspace/project from prefs; ask if ambiguous.
|
|
118
|
-
3. Archive:
|
|
119
|
-
`ios_xcodebuild({workspace|project, scheme, action: "archive", configuration: "Release", destination: "generic/platform=iOS"})`
|
|
120
|
-
Note the destination: the simulator default would produce an archive that
|
|
121
|
-
cannot be exported for distribution.
|
|
122
|
-
4. Export:
|
|
123
|
-
`ios_export_ipa({archive_path, output_dir, method: "app-store-connect", team_id, provisioning_profiles?, signing_style?})`
|
|
124
|
-
Leave `allow_provisioning_updates` off unless the user asks: it lets xcodebuild
|
|
125
|
-
create or modify profiles in the developer account, which a validation run has
|
|
126
|
-
no business doing.
|
|
127
|
-
5. A failed export halts with the parsed errors. The usual causes are a missing
|
|
128
|
-
distribution certificate, a profile that does not match the bundle ID, or
|
|
129
|
-
`signing_style: "manual"` with no `provisioning_profiles` map.
|
|
130
|
-
|
|
131
|
-
## Step 4 - Gate 1, static audit
|
|
132
|
-
|
|
133
|
-
`ios_app_store_audit({archive_path, rules: "all"})`.
|
|
134
|
-
|
|
135
|
-
`error` findings are blocking; `warning` is advisory. Group the output by severity
|
|
136
|
-
and keep each finding's ITMS code - Gate 2 may return the same code, and seeing
|
|
137
|
-
it in both places tells the user it is real rather than a heuristic.
|
|
138
|
-
|
|
139
|
-
## Step 5 - Gate 2, Apple's own validation
|
|
140
|
-
|
|
141
|
-
`ios_testflight_validate({ipa_path, platform: "ios", <credential args>})`.
|
|
142
|
-
|
|
143
|
-
Render the verdict exactly as returned:
|
|
144
|
-
|
|
145
|
-
- `PASS` - Apple accepted the binary for delivery.
|
|
146
|
-
- `FAIL` - list each issue with its ITMS code, the mapped guideline, and the hint.
|
|
147
|
-
- `SKIPPED` - print the reason. **Never render this as a pass.** The verdict line
|
|
148
|
-
for the whole run must read `2 of 3 gates cleared, 1 skipped`, not `passed`.
|
|
149
|
-
|
|
150
|
-
Gate 2 is the only gate that catches a build number already used - the most
|
|
151
|
-
common wasted upload - so when it fails on that, say so plainly and name the next
|
|
152
|
-
free build number.
|
|
153
|
-
|
|
154
|
-
## Step 6 - Gate 3, guideline review
|
|
155
|
-
|
|
156
|
-
Load the `app-store-review` skill and review the repo against it. This is the gate
|
|
157
|
-
that catches what a human reviewer rejects, so it reads source, not the binary:
|
|
158
|
-
|
|
159
|
-
| Area | Evidence to gather |
|
|
160
|
-
|---|---|
|
|
161
|
-
| Purpose strings | every `NS*UsageDescription` in Info.plist - present, specific, user-facing, and matching what the code actually does with the data |
|
|
162
|
-
| Privacy manifest | `PrivacyInfo.xcprivacy` exists, declares required-reason APIs, and matches the SDKs actually linked |
|
|
163
|
-
| Tracking | if any tracking API or SDK is present, an ATT prompt exists and runs before collection |
|
|
164
|
-
| Account deletion | if the app creates accounts, an in-app deletion path exists (guideline 5.1.1(v)) |
|
|
165
|
-
| Privacy policy | reachable in-app and in the metadata |
|
|
166
|
-
| IAP | anything unlocking features goes through StoreKit, with no external purchase path |
|
|
167
|
-
| Sign in with Apple | present when a third-party social login is offered |
|
|
168
|
-
|
|
169
|
-
For each: `pass` / `fail` / `not-applicable` with the evidence path that justifies
|
|
170
|
-
it. `not-applicable` needs a reason - an unexamined area is not a pass.
|
|
171
|
-
|
|
172
|
-
## Step 7 - report
|
|
173
|
-
|
|
174
|
-
Write to `~/TestFlightChecks/<repo>-<branch>-<timestamp>/report.md` and print a
|
|
175
|
-
summary. Structure:
|
|
21
|
+
Read this file and apply its instructions:
|
|
176
22
|
|
|
177
23
|
```
|
|
178
|
-
|
|
179
|
-
Build: <ipa or archive path> · <bundle id> <version> (<build>)
|
|
180
|
-
Auth: tier <1|2|none> · <method>
|
|
181
|
-
|
|
182
|
-
Gate 1 static audit PASS | FAIL (<n> blocking, <n> advisory) | SKIPPED (<reason>)
|
|
183
|
-
Gate 2 Apple validation PASS | FAIL (<n> issues) | SKIPPED (<reason>)
|
|
184
|
-
Gate 3 guideline review PASS | FAIL (<n> findings) | <n> not-applicable
|
|
185
|
-
|
|
186
|
-
Blocking - fix before uploading
|
|
187
|
-
[ITMS-90683] Info.plist: NSCameraUsageDescription missing
|
|
188
|
-
guideline 5.1.1 Data Collection and Storage
|
|
189
|
-
<hint>
|
|
190
|
-
<file:line>
|
|
191
|
-
|
|
192
|
-
Advisory
|
|
193
|
-
...
|
|
194
|
-
|
|
195
|
-
Not run
|
|
196
|
-
Gate 1: needs an .xcarchive; only an .ipa was supplied
|
|
24
|
+
$HOME/.claude/commands/multi-agent/store-ready/SKILL.md
|
|
197
25
|
```
|
|
198
26
|
|
|
199
|
-
|
|
27
|
+
Run it with **platform pinned to `ios`** and skip its platform-detection step. Pass
|
|
28
|
+
`$ARGUMENTS` through verbatim: `--ipa=`, `--archive=`, a repo name or path, and
|
|
29
|
+
`--resume` all mean exactly what they mean there.
|
|
200
30
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
-
|
|
204
|
-
cannot act on is noise.
|
|
205
|
-
- No AI or assistant attribution anywhere, per
|
|
206
|
-
`$HOME/.claude/rules/git-conventions.md`.
|
|
207
|
-
- Real newlines, no HTML entities, per
|
|
208
|
-
`$HOME/.claude/multi-agent-refs/rules.md "External System Outputs"`.
|
|
31
|
+
`--aab=` / `--apk=` are Android inputs and are not valid here. If one is supplied,
|
|
32
|
+
do not silently switch platform - say the Android inputs belong to
|
|
33
|
+
`/multi-agent:store-ready`, and stop.
|
|
209
34
|
|
|
210
|
-
##
|
|
35
|
+
## What you get
|
|
211
36
|
|
|
212
|
-
|
|
37
|
+
Unchanged from before the merge, on the iOS path:
|
|
213
38
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
39
|
+
| Gate | What runs | Needs |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| **1. Static** | `ios_app_store_audit` (18 rules, real ITMS codes) | an `.xcarchive` |
|
|
42
|
+
| **2. Authoritative** | `ios_testflight_validate` → `altool --validate-app` | an `.ipa` + credentials |
|
|
43
|
+
| **3. Policy** | `app-store-review` skill vs repo source | repo checkout |
|
|
218
44
|
|
|
219
|
-
|
|
45
|
+
A skipped gate is never folded into the pass count, and the run never uploads. The
|
|
46
|
+
report lands in `~/StoreChecks/ios-<repo>-<branch>-<timestamp>/report.md`.
|
|
@@ -101,7 +101,7 @@ Update the pipeline in one command. Existing preferences are preserved; only ski
|
|
|
101
101
|
```
|
|
102
102
|
Current: v3.6.0
|
|
103
103
|
-> git pull origin main
|
|
104
|
-
-> node install.js --all (
|
|
104
|
+
-> node install.js --all (49 commands, 50 scripts, 202 skills)
|
|
105
105
|
-> migrate-prefs.mjs (0 changes - already v2.1.0)
|
|
106
106
|
|
|
107
107
|
✓ Updated: v3.6.0 → v3.7.0
|
|
@@ -35,12 +35,36 @@ For Android: use `mcp__dev-toolkit__android_*` tools
|
|
|
35
35
|
/multi-agent test "accessibility" -> accessibility audit (visual + MCP audit tool)
|
|
36
36
|
/multi-agent test "dynamic type" -> large text test
|
|
37
37
|
/multi-agent test "screenshot tr" -> locale screenshots
|
|
38
|
-
/multi-agent test "store-ready" ->
|
|
39
|
-
/multi-agent test "biometric" -> Face ID / Touch ID flow test
|
|
40
|
-
/multi-agent test "performance" -> launch time + scroll performance
|
|
38
|
+
/multi-agent test "store-ready" -> hands off to /multi-agent:store-ready (package validation)
|
|
41
39
|
/sim-test -> standalone (same thing)
|
|
42
40
|
```
|
|
43
41
|
|
|
42
|
+
Not offered, deliberately - `"biometric"` and `"performance"` were listed here with
|
|
43
|
+
no implementation section, so reaching one fell through to the general sweep and got
|
|
44
|
+
reported as the scenario asked for. Neither can be implemented symmetrically today:
|
|
45
|
+
biometric has an iOS tool (`ios_biometric`) and no Android counterpart, and launch
|
|
46
|
+
timing has `android_launch_time` and no iOS counterpart. Platform is auto-detected,
|
|
47
|
+
so either one would work on one platform and silently do nothing on the other. They
|
|
48
|
+
come back when the missing side exists, not before - do not re-add the rows to make
|
|
49
|
+
the list look complete.
|
|
50
|
+
|
|
51
|
+
Four scenarios also have a fixed-scenario command that pins the tag, so it does not
|
|
52
|
+
have to be typed or quoted. They arrive here with the scenario already resolved -
|
|
53
|
+
treat them as identical to the quoted form:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
/multi-agent:test-dark-mode -> scenario "dark mode"
|
|
57
|
+
/multi-agent:test-accessibility -> scenario "accessibility"
|
|
58
|
+
/multi-agent:test-dynamic-type -> scenario "dynamic type"
|
|
59
|
+
/multi-agent:test-screenshots [locale] -> scenario "screenshot <locale>", locale defaults to tr
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`store-ready` is not one of them, and does not belong to this file at all: it
|
|
63
|
+
validates a built package on either platform and lives at
|
|
64
|
+
`/multi-agent:store-ready`. The `"store-ready"` tag is kept as a hand-off so an
|
|
65
|
+
existing invocation still lands somewhere correct. `/multi-agent:testflight-validation`
|
|
66
|
+
is the iOS-pinned alias of that same command.
|
|
67
|
+
|
|
44
68
|
## Flow
|
|
45
69
|
|
|
46
70
|
### Step 1 - Device & App Discovery
|
|
@@ -121,43 +145,28 @@ Call: ios_set_locale(language: "<lang>", bundle_id: "...")
|
|
|
121
145
|
|
|
122
146
|
**"store-ready":**
|
|
123
147
|
|
|
124
|
-
|
|
148
|
+
Not implemented here. `store-ready` validates a built **package**, which is a
|
|
149
|
+
different job from driving a running app, and it is owned by one command on both
|
|
150
|
+
platforms:
|
|
125
151
|
|
|
126
152
|
```
|
|
127
|
-
|
|
128
|
-
cwd contains .xcodeproj OR Package.swift -> iOS
|
|
129
|
-
cwd contains build.gradle OR build.gradle.kts -> Android
|
|
130
|
-
Otherwise -> error: "store-ready needs an iOS or Android project"
|
|
131
|
-
|
|
132
|
-
Step A - run the standard visual + accessibility sweep (light + dark + large text).
|
|
133
|
-
|
|
134
|
-
Step B - dispatch compliance skill:
|
|
135
|
-
iOS:
|
|
136
|
-
Load $HOME/.claude/skills/apple-archive-compliance/SKILL.md
|
|
137
|
-
Prereq: @mmerterden/dev-toolkit-mcp ≥ v2.9.0 (provides ios_app_store_audit MCP tool)
|
|
138
|
-
- installable via npm; standalone ArchiveGuard binary deprecated in v8.4.0
|
|
139
|
-
Artifact: pick newest .xcarchive under ~/Library/Developer/Xcode/Archives/**
|
|
140
|
-
OR accept explicit path argument after "store-ready"
|
|
141
|
-
Invoke: mcp__dev-toolkit__ios_app_store_audit({ archive_path: <path>, rules: "all" })
|
|
142
|
-
OR (Node fallback): node -e "import('@mmerterden/dev-toolkit-mcp/tools/ios-app-store-audit/index.js').then(m => m.runAudit({ archivePath: '<path>', rules: 'all' }).then(r => console.log(JSON.stringify(r))))"
|
|
143
|
-
Humanize via --lang en (promptLanguage is locked to "en"; pass --lang=tr explicitly to opt into Turkish)
|
|
144
|
-
Android:
|
|
145
|
-
Load $HOME/.claude/skills/google-play-compliance/SKILL.md
|
|
146
|
-
Prereq: bundletool, aapt2, apksigner in PATH (Android SDK build-tools)
|
|
147
|
-
Artifact: newest .aab under **/build/outputs/bundle/**/*.aab
|
|
148
|
-
OR explicit path argument
|
|
149
|
-
Invoke: bundletool validate + dump manifest, aapt2 dump badging,
|
|
150
|
-
apksigner verify, ABI/native scan
|
|
151
|
-
Merge findings with UI-hunter findings -> single report, severity-grouped.
|
|
152
|
-
|
|
153
|
-
Step C - humanize via `--lang en` by default (promptLanguage is locked to "en"; pass --lang=tr explicitly to opt into Turkish), group by severity
|
|
154
|
-
(error = blocker, warning = risk, info = hygiene), link each finding to the
|
|
155
|
-
Apple ITMS / Play policy reference supplied by the skill catalog.
|
|
156
|
-
|
|
157
|
-
Step D - offer `/multi-agent:channels` follow-up so findings can land in
|
|
158
|
-
Jira / Confluence / Wiki / PR body via the normal Phase 7 machinery.
|
|
153
|
+
$HOME/.claude/commands/multi-agent/store-ready/SKILL.md
|
|
159
154
|
```
|
|
160
155
|
|
|
156
|
+
Read that file and follow it. Pass through any artifact path given after
|
|
157
|
+
`store-ready` as its `--archive=` / `--ipa=` / `--aab=` / `--apk=` input.
|
|
158
|
+
|
|
159
|
+
Its Step A is the visual + accessibility sweep in this file - it calls back here
|
|
160
|
+
for the running-app half, then runs three gates per platform that nothing in this
|
|
161
|
+
file can do: the static package audit, the store's own validator, and a policy
|
|
162
|
+
review against repo source. It merges both halves into one severity-grouped report
|
|
163
|
+
and offers the `/multi-agent:channels` follow-up.
|
|
164
|
+
|
|
165
|
+
The archive-compliance audit used to be duplicated here, invoking
|
|
166
|
+
`ios_app_store_audit` with exactly the arguments the store-ready command's Gate 1
|
|
167
|
+
uses. Two copies of one call is how the iOS path grew a second door with no Gate 2,
|
|
168
|
+
no Gate 3 and no Android parity, so the copy is gone rather than kept in sync.
|
|
169
|
+
|
|
161
170
|
**No argument (full test):**
|
|
162
171
|
|
|
163
172
|
- Run light mode -> all screens
|
|
@@ -140,9 +140,31 @@ run_find() {
|
|
|
140
140
|
if [ "${#roots[@]}" -eq 0 ]; then
|
|
141
141
|
return 0
|
|
142
142
|
fi
|
|
143
|
+
# No explicit -print anywhere in this expression means find falls back to
|
|
144
|
+
# its own implicit print on the WHOLE expression - which fires for every
|
|
145
|
+
# true branch of the -prune OR-chain too, including the pruned directories
|
|
146
|
+
# themselves (their contents are still correctly skipped; the dir path
|
|
147
|
+
# itself was not). An explicit -print, implicit-AND'd onto the caller's own
|
|
148
|
+
# criteria as the OR-chain's last branch, only fires on that branch.
|
|
143
149
|
find "${roots[@]}" \
|
|
144
150
|
"${SKIP_PRUNE_ARGS[@]}" \
|
|
145
|
-
"$@" 2>/dev/null || true
|
|
151
|
+
"$@" -print 2>/dev/null || true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Count how many of the newline-separated paths in $1 contain a line matching
|
|
155
|
+
# grep pattern $2. `xargs grep -l` here used to word-split on whitespace by
|
|
156
|
+
# default, so any path with a space in it was silently mis-scanned as several
|
|
157
|
+
# nonexistent partial paths (a false-negative on real matches, not an error -
|
|
158
|
+
# stderr was already redirected to /dev/null). Reading line-by-line handles
|
|
159
|
+
# that correctly since each line is one whole path regardless of embedded
|
|
160
|
+
# spaces.
|
|
161
|
+
count_grep_hits() {
|
|
162
|
+
local list="$1" pattern="$2" n=0 f
|
|
163
|
+
while IFS= read -r f; do
|
|
164
|
+
[ -z "$f" ] && continue
|
|
165
|
+
grep -lq "$pattern" "$f" 2>/dev/null && n=$((n + 1))
|
|
166
|
+
done <<< "$list"
|
|
167
|
+
printf '%s' "$n"
|
|
146
168
|
}
|
|
147
169
|
|
|
148
170
|
# ---- confidence classifier ----------------------------------------------------
|
|
@@ -849,8 +871,15 @@ bucket_localization_key() {
|
|
|
849
871
|
case "$PLATFORM" in
|
|
850
872
|
ios)
|
|
851
873
|
local strings_files xcstrings_files
|
|
852
|
-
|
|
853
|
-
|
|
874
|
+
# An earlier version ANDed -name onto the FRONT of the prune chain
|
|
875
|
+
# (`-name X SKIP_PRUNE_ARGS...`), which requires every -prune branch to
|
|
876
|
+
# ALSO be named "Localizable.strings" - a directory prune can never
|
|
877
|
+
# satisfy that, so no prune ever fired and the search walked into
|
|
878
|
+
# Pods/.build/node_modules looking for vendored copies. Prunes must
|
|
879
|
+
# stay their own independent OR-chain, ahead of (not ANDed with) the
|
|
880
|
+
# real search criteria. The xcstrings search had no prune chain at all.
|
|
881
|
+
strings_files=$(find "$REPO_PATH" "${SKIP_PRUNE_ARGS[@]}" -name 'Localizable.strings' -type f -print 2>/dev/null | head -5 || true)
|
|
882
|
+
xcstrings_files=$(find "$REPO_PATH" "${SKIP_PRUNE_ARGS[@]}" -name 'Localizable.xcstrings' -type f -print 2>/dev/null | head -3 || true)
|
|
854
883
|
if [ -n "$strings_files" ]; then
|
|
855
884
|
keys=$(printf '%s\n' "$strings_files" | while IFS= read -r f; do
|
|
856
885
|
[ -n "$f" ] && grep -hEo '^"[^"]+"' "$f" 2>/dev/null | tr -d '"'
|
|
@@ -918,9 +947,9 @@ bucket_di_registration() {
|
|
|
918
947
|
fi
|
|
919
948
|
files=$(run_find -type f -name '*.swift' 2>/dev/null || true)
|
|
920
949
|
if [ -n "$files" ]; then
|
|
921
|
-
resolver_count=$(
|
|
922
|
-
factory_count=$(
|
|
923
|
-
swinject_count=$(
|
|
950
|
+
resolver_count=$(count_grep_hits "$files" 'Resolver.register')
|
|
951
|
+
factory_count=$(count_grep_hits "$files" '@Injected\|Container.shared')
|
|
952
|
+
swinject_count=$(count_grep_hits "$files" 'Swinject\|Container()')
|
|
924
953
|
fi
|
|
925
954
|
manual_count=${manual_count:-0}; resolver_count=${resolver_count:-0}; factory_count=${factory_count:-0}; swinject_count=${swinject_count:-0}
|
|
926
955
|
|
|
@@ -941,9 +970,9 @@ bucket_di_registration() {
|
|
|
941
970
|
files=$(run_find -type f -name '*.kt' 2>/dev/null || true)
|
|
942
971
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
943
972
|
local hilt_count koin_count dagger_count
|
|
944
|
-
hilt_count=$(
|
|
945
|
-
koin_count=$(
|
|
946
|
-
dagger_count=$(
|
|
973
|
+
hilt_count=$(count_grep_hits "$files" '@Module\|@InstallIn\|@HiltViewModel')
|
|
974
|
+
koin_count=$(count_grep_hits "$files" 'koinViewModel\|module {')
|
|
975
|
+
dagger_count=$(count_grep_hits "$files" '@Component\|@Subcomponent')
|
|
947
976
|
hilt_count=${hilt_count:-0}; koin_count=${koin_count:-0}; dagger_count=${dagger_count:-0}
|
|
948
977
|
if [ "$hilt_count" -ge "$koin_count" ] && [ "$hilt_count" -ge "$dagger_count" ] && [ "$hilt_count" -gt 0 ]; then
|
|
949
978
|
emit_bucket "Hilt" "@Module @InstallIn(SingletonComponent::class)" "$(confidence_for "$hilt_count")" "[]" '["Koin","Dagger"]'
|
|
@@ -959,9 +988,9 @@ bucket_di_registration() {
|
|
|
959
988
|
files=$(run_find -type f -name '*.py' 2>/dev/null || true)
|
|
960
989
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
961
990
|
local depends_count dishka_count di_count
|
|
962
|
-
depends_count=$(
|
|
963
|
-
dishka_count=$(
|
|
964
|
-
di_count=$(
|
|
991
|
+
depends_count=$(count_grep_hits "$files" 'Depends(')
|
|
992
|
+
dishka_count=$(count_grep_hits "$files" 'dishka')
|
|
993
|
+
di_count=$(count_grep_hits "$files" 'dependency_injector')
|
|
965
994
|
depends_count=${depends_count:-0}; dishka_count=${dishka_count:-0}; di_count=${di_count:-0}
|
|
966
995
|
if [ "$depends_count" -ge "$dishka_count" ] && [ "$depends_count" -ge "$di_count" ] && [ "$depends_count" -gt 0 ]; then
|
|
967
996
|
emit_bucket "FastAPI Depends" "def handler(svc: FooService = Depends(get_service))" "$(confidence_for "$depends_count")" "[]" '["dishka","dependency_injector"]'
|
|
@@ -977,9 +1006,9 @@ bucket_di_registration() {
|
|
|
977
1006
|
files=$(run_find -type f \( -name '*.tsx' -o -name '*.ts' \) 2>/dev/null || true)
|
|
978
1007
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
979
1008
|
local context_count zustand_count hook_count
|
|
980
|
-
context_count=$(
|
|
981
|
-
zustand_count=$(
|
|
982
|
-
hook_count=$(
|
|
1009
|
+
context_count=$(count_grep_hits "$files" 'createContext\|\.Provider')
|
|
1010
|
+
zustand_count=$(count_grep_hits "$files" "from 'zustand'\\|from \"zustand\"")
|
|
1011
|
+
hook_count=$(count_grep_hits "$files" 'export function use[A-Z]')
|
|
983
1012
|
context_count=${context_count:-0}; zustand_count=${zustand_count:-0}; hook_count=${hook_count:-0}
|
|
984
1013
|
if [ "$context_count" -ge "$zustand_count" ] && [ "$context_count" -ge "$hook_count" ] && [ "$context_count" -gt 0 ]; then
|
|
985
1014
|
emit_bucket "React Context Provider" "const FooContext = createContext<Foo>(...)" "$(confidence_for "$context_count")" "[]" '["zustand","custom-hook-factory"]'
|
|
@@ -124,10 +124,17 @@ trap 'rm -rf "$TMPDIR_A"' EXIT INT TERM
|
|
|
124
124
|
# curl one /nodes batch (token via -K config so it never lands in argv).
|
|
125
125
|
curl_nodes() {
|
|
126
126
|
local out="$1" ids="$2" code
|
|
127
|
+
# curl already writes "000" via -w on a total connection failure (and
|
|
128
|
+
# still exits non-zero for it, which the inner `|| echo "000"` used to
|
|
129
|
+
# catch too - so `code` became the literal string "000000", never
|
|
130
|
+
# matching a plain "000" comparison anywhere downstream). `|| true` keeps
|
|
131
|
+
# a failing curl from tripping any caller's `set -e` without re-printing
|
|
132
|
+
# into $code; `:-000` only fills in when curl fails before -w ever runs.
|
|
127
133
|
code=$(curl -sS -o "$out" -w '%{http_code}' \
|
|
128
134
|
--max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
|
|
129
135
|
-K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
|
|
130
|
-
"$FIGMA_API/files/$FILE_KEY/nodes?ids=$ids" ||
|
|
136
|
+
"$FIGMA_API/files/$FILE_KEY/nodes?ids=$ids") || true
|
|
137
|
+
code="${code:-000}"
|
|
131
138
|
printf '%s' "$code"
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -202,18 +202,33 @@ fortify_get() {
|
|
|
202
202
|
return 1
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
case
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
# Check fortify_get's actual exit status, not a string pattern over its
|
|
206
|
+
# output - the old `case ... HTTP_AUTH_FAILED|*HTTP_4*)` only recognized
|
|
207
|
+
# auth failures and 4xx. A dropped connection, timeout, or 5xx made
|
|
208
|
+
# fortify_get return "...\nHTTP_000"/"...\nHTTP_5xx", which matched neither
|
|
209
|
+
# arm, so the case fell through, VERSION_JSON stayed unparseable garbage,
|
|
210
|
+
# and the Python block below silently treated that as "0 issues found" -
|
|
211
|
+
# the security gate passing precisely when it couldn't check anything.
|
|
212
|
+
if ! VERSION_JSON=$(fortify_get "projectVersions/$VERSION_ID"); then
|
|
213
|
+
echo "ERR: Fortify fetch failed for projectVersions/$VERSION_ID: ${VERSION_JSON:-no response}" >&2
|
|
214
|
+
exit 3
|
|
215
|
+
fi
|
|
211
216
|
|
|
212
217
|
ISSUES_JSON=""
|
|
213
218
|
if [ -n "$ISSUE_ID" ]; then
|
|
214
|
-
ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?q=issueInstanceId:\"$ISSUE_ID\"&limit=20"
|
|
219
|
+
if ! ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?q=issueInstanceId:\"$ISSUE_ID\"&limit=20"); then
|
|
220
|
+
echo "ERR: Fortify issues fetch failed for issue $ISSUE_ID: ${ISSUES_JSON:-no response}" >&2
|
|
221
|
+
exit 3
|
|
222
|
+
fi
|
|
215
223
|
else
|
|
216
|
-
|
|
224
|
+
# filter=ANALYSIS!=NotAnIssue,ANALYSIS!=FalsePositive (%21%3D is `!=`) is
|
|
225
|
+
# meant to EXCLUDE suppressed findings. It was encoded as %3D%3D (`==`),
|
|
226
|
+
# which instead selected only suppressed findings - the opposite of the
|
|
227
|
+
# gate's intent whenever SSC honors the filter.
|
|
228
|
+
if ! ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?limit=200&filter=ANALYSIS%21%3DNotAnIssue,ANALYSIS%21%3DFalsePositive&filterset=Critical%2CHigh"); then
|
|
229
|
+
echo "ERR: Fortify issues fetch failed for version $VERSION_ID: ${ISSUES_JSON:-no response}" >&2
|
|
230
|
+
exit 3
|
|
231
|
+
fi
|
|
217
232
|
fi
|
|
218
233
|
|
|
219
234
|
# Issue details are fetched per-issue lazily - only for the first 10 to keep
|
|
@@ -180,10 +180,20 @@ curl_figma() {
|
|
|
180
180
|
for attempt in 1 2 3; do
|
|
181
181
|
# Token header goes through a curl config via process substitution so it
|
|
182
182
|
# never appears in argv (argv is visible to ps).
|
|
183
|
+
# curl already writes "000" via -w on a total connection failure (and
|
|
184
|
+
# still exits non-zero for it, which the inner `|| echo "000"` used to
|
|
185
|
+
# catch too - so `code` became the literal string "000000", matching
|
|
186
|
+
# neither the `000)` retry branch below nor handle_http_code's "000)
|
|
187
|
+
# network timeout" case, just the generic catch-all). `|| true` on its
|
|
188
|
+
# own line keeps `set -e` from aborting on curl's exit status without
|
|
189
|
+
# re-printing anything into $code; the `:-000` fallback only fires for
|
|
190
|
+
# the rarer case where curl fails before -w ever runs and $code is
|
|
191
|
+
# genuinely empty.
|
|
183
192
|
code=$(curl -sS -o "$out" -w '%{http_code}' \
|
|
184
193
|
--max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
|
|
185
194
|
-K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
|
|
186
|
-
"$url" ||
|
|
195
|
+
"$url") || true
|
|
196
|
+
code="${code:-000}"
|
|
187
197
|
case "$code" in
|
|
188
198
|
2*) printf '%s' "$code"; return 0 ;;
|
|
189
199
|
401|403) printf '%s' "$code"; return 0 ;;
|