@mmerterden/multi-agent-pipeline 11.3.2 → 11.5.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 +141 -0
- package/README.md +1 -0
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +113 -11
- package/install/copilot.mjs +75 -6
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/review/SKILL.md +36 -4
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +22 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +22 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- package/pipeline/lib/account-resolver.sh +61 -23
- package/pipeline/lib/channels-multi-repo.sh +7 -2
- package/pipeline/lib/count-lib.sh +27 -0
- package/pipeline/lib/credential-store.sh +23 -6
- package/pipeline/lib/extract-conventions.sh +27 -21
- package/pipeline/lib/fetch-confluence.sh +25 -13
- package/pipeline/lib/fetch-crashlytics.sh +30 -8
- package/pipeline/lib/fetch-fortify.sh +11 -2
- package/pipeline/lib/fetch-swagger.sh +18 -7
- package/pipeline/lib/figma-mcp-refresh.sh +26 -11
- package/pipeline/lib/figma-screenshot.sh +11 -2
- package/pipeline/lib/issue-fetcher.sh +31 -6
- package/pipeline/lib/multi-repo-pipeline.sh +84 -20
- package/pipeline/lib/plan-todos.sh +12 -3
- package/pipeline/lib/post-pr-review.sh +5 -3
- package/pipeline/lib/repo-cache.sh +53 -9
- package/pipeline/lib/review-watch.sh +26 -6
- package/pipeline/lib/shadow-git.sh +45 -4
- package/pipeline/lib/vercel-deploy.sh +10 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
- package/pipeline/multi-agent-refs/readiness-review.md +65 -0
- package/pipeline/scripts/audit-log-rotate.sh +38 -10
- package/pipeline/scripts/check-md-links.mjs +34 -9
- package/pipeline/scripts/diff-risk-score.mjs +4 -1
- package/pipeline/scripts/evidence-gate.mjs +10 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +7 -7
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- package/pipeline/scripts/migrate-prefs.mjs +26 -7
- package/pipeline/scripts/phase-tracker.sh +71 -0
- package/pipeline/scripts/pre-commit-check.sh +39 -0
- package/pipeline/scripts/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-generate-issue.sh +3 -3
- package/pipeline/scripts/smoke-install-layout.sh +11 -3
- package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
- package/pipeline/scripts/smoke-pack-contents.sh +140 -0
- package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
- package/pipeline/scripts/smoke-review-readiness.sh +91 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/smoke-wrapper-preservation.sh +68 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +105 -36
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
- package/pipeline/skills/.skill-manifest.json +13 -5
- package/pipeline/skills/.skills-index.json +20 -2
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +6 -2
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -10
- package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +25 -0
- package/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md +25 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/skills-index.md +4 -2
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-fetchers-offline.sh - offline invocation tests for the pipeline/lib
|
|
4
|
+
# fetchers. No network, no real tokens.
|
|
5
|
+
#
|
|
6
|
+
# Asserts, per fetcher:
|
|
7
|
+
# 1. usage / arg-validation paths exit with the documented code
|
|
8
|
+
# 2. URL/host parsing succeeds for a representative URL. This is the
|
|
9
|
+
# permanent regression guard for the env-prefix bug where
|
|
10
|
+
# `VAR=x PARSED=$(python3 ...)` never exported VAR to the child, so
|
|
11
|
+
# fetch-confluence always failed with "could not determine Confluence
|
|
12
|
+
# host" before reaching the credential stage.
|
|
13
|
+
# 3. with stub credentials (fake credential-store in a temp HOME, fake curl
|
|
14
|
+
# on PATH) the failure moves past parsing to the documented
|
|
15
|
+
# credential/network exit code
|
|
16
|
+
# 4. static source checks: no bash-4-only constructs (macOS /bin/bash is
|
|
17
|
+
# 3.2) and no secret-bearing argv patterns (curl -H auth headers,
|
|
18
|
+
# --data-urlencode secrets, security add-generic-password -w on argv)
|
|
19
|
+
#
|
|
20
|
+
# Fetchers run under /bin/bash when available so bash-4-isms fail loudly on
|
|
21
|
+
# macOS instead of passing under a newer Homebrew bash.
|
|
22
|
+
#
|
|
23
|
+
# Exit codes: 0=pass, 1=failure
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
28
|
+
LIB="$REPO_ROOT/pipeline/lib"
|
|
29
|
+
SCRIPTS="$REPO_ROOT/pipeline/scripts"
|
|
30
|
+
|
|
31
|
+
# Prefer the system bash (3.2 on macOS) so bash-4-isms regress loudly here.
|
|
32
|
+
SH="/bin/bash"
|
|
33
|
+
[ -x "$SH" ] || SH="$(command -v bash)"
|
|
34
|
+
|
|
35
|
+
PASS=0
|
|
36
|
+
FAIL=0
|
|
37
|
+
note() { echo " -> $*"; }
|
|
38
|
+
pass() { echo " ok $*"; PASS=$((PASS + 1)); }
|
|
39
|
+
fail() { echo " FAIL $*"; FAIL=$((FAIL + 1)); }
|
|
40
|
+
|
|
41
|
+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/smoke-fetchers.XXXXXX")
|
|
42
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
43
|
+
|
|
44
|
+
OUT="$WORK/out.log"
|
|
45
|
+
ERR="$WORK/err.log"
|
|
46
|
+
|
|
47
|
+
# expect_rc <expected-rc> <label> <cmd...>
|
|
48
|
+
# Runs cmd, captures stdout/stderr to $OUT/$ERR, asserts the exit code.
|
|
49
|
+
expect_rc() {
|
|
50
|
+
local expected="$1" label="$2"
|
|
51
|
+
shift 2
|
|
52
|
+
local rc=0
|
|
53
|
+
"$@" >"$OUT" 2>"$ERR" || rc=$?
|
|
54
|
+
if [ "$rc" -eq "$expected" ]; then
|
|
55
|
+
pass "$label (rc=$rc)"
|
|
56
|
+
else
|
|
57
|
+
fail "$label (rc=$rc, expected $expected)"
|
|
58
|
+
tail -5 "$ERR" | sed 's/^/ /'
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
assert_in_file() {
|
|
63
|
+
local needle="$1" file="$2" label="$3"
|
|
64
|
+
if grep -q -e "$needle" "$file" 2>/dev/null; then
|
|
65
|
+
pass "$label"
|
|
66
|
+
else
|
|
67
|
+
fail "$label (pattern '$needle' not found)"
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
assert_not_in_file() {
|
|
72
|
+
local needle="$1" file="$2" label="$3"
|
|
73
|
+
if grep -q -e "$needle" "$file" 2>/dev/null; then
|
|
74
|
+
fail "$label (pattern '$needle' present)"
|
|
75
|
+
else
|
|
76
|
+
pass "$label"
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# -----------------------------------------------------------------------------
|
|
81
|
+
note "0. fixtures: temp HOME with resolver + fake credential store, fake curls"
|
|
82
|
+
# -----------------------------------------------------------------------------
|
|
83
|
+
FAKE_HOME="$WORK/home"
|
|
84
|
+
mkdir -p "$FAKE_HOME/.claude/lib"
|
|
85
|
+
cp "$LIB/credential-store-resolver.sh" "$FAKE_HOME/.claude/lib/credential-store-resolver.sh"
|
|
86
|
+
|
|
87
|
+
# Fake credential store: empty by default, returns FAKE_CRED_VALUE when set.
|
|
88
|
+
cat > "$FAKE_HOME/.claude/lib/credential-store.sh" <<'FAKE'
|
|
89
|
+
#!/bin/bash
|
|
90
|
+
case "${1:-}" in
|
|
91
|
+
get)
|
|
92
|
+
if [ -n "${FAKE_CRED_VALUE:-}" ]; then printf '%s' "$FAKE_CRED_VALUE"; exit 0; fi
|
|
93
|
+
exit 1 ;;
|
|
94
|
+
platform) echo "fake" ;;
|
|
95
|
+
*) exit 3 ;;
|
|
96
|
+
esac
|
|
97
|
+
FAKE
|
|
98
|
+
chmod +x "$FAKE_HOME/.claude/lib/credential-store.sh"
|
|
99
|
+
|
|
100
|
+
# Fake curl variants, each in its own PATH-prepend dir.
|
|
101
|
+
BIN_CURL_FAIL="$WORK/bin-curl-fail"
|
|
102
|
+
mkdir -p "$BIN_CURL_FAIL"
|
|
103
|
+
cat > "$BIN_CURL_FAIL/curl" <<'FAKE'
|
|
104
|
+
#!/bin/bash
|
|
105
|
+
echo "curl: (7) offline stub - connection refused" >&2
|
|
106
|
+
exit 7
|
|
107
|
+
FAKE
|
|
108
|
+
chmod +x "$BIN_CURL_FAIL/curl"
|
|
109
|
+
|
|
110
|
+
# Swagger meta stub: honours --output, emits the --write-out triple with a
|
|
111
|
+
# content type that matches neither json nor yaml, forcing the URL-suffix
|
|
112
|
+
# lowercase path (the former bash-4 ${URL,,} site).
|
|
113
|
+
BIN_CURL_META="$WORK/bin-curl-meta"
|
|
114
|
+
mkdir -p "$BIN_CURL_META"
|
|
115
|
+
cat > "$BIN_CURL_META/curl" <<'FAKE'
|
|
116
|
+
#!/bin/bash
|
|
117
|
+
out=""
|
|
118
|
+
prev=""
|
|
119
|
+
for a in "$@"; do
|
|
120
|
+
if [ "$prev" = "--output" ]; then out="$a"; fi
|
|
121
|
+
prev="$a"
|
|
122
|
+
done
|
|
123
|
+
if [ -n "$out" ]; then printf '{}' > "$out"; fi
|
|
124
|
+
printf '200\napplication/octet-stream\n2\n'
|
|
125
|
+
exit 0
|
|
126
|
+
FAKE
|
|
127
|
+
chmod +x "$BIN_CURL_META/curl"
|
|
128
|
+
|
|
129
|
+
# 401 stub: honours -o, prints the HTTP code the way curl -w '%{http_code}' does.
|
|
130
|
+
BIN_CURL_401="$WORK/bin-curl-401"
|
|
131
|
+
mkdir -p "$BIN_CURL_401"
|
|
132
|
+
cat > "$BIN_CURL_401/curl" <<'FAKE'
|
|
133
|
+
#!/bin/bash
|
|
134
|
+
out=""
|
|
135
|
+
prev=""
|
|
136
|
+
for a in "$@"; do
|
|
137
|
+
if [ "$prev" = "-o" ] || [ "$prev" = "--output" ]; then out="$a"; fi
|
|
138
|
+
prev="$a"
|
|
139
|
+
done
|
|
140
|
+
if [ -n "$out" ]; then printf '{}' > "$out"; fi
|
|
141
|
+
printf '401'
|
|
142
|
+
exit 0
|
|
143
|
+
FAKE
|
|
144
|
+
chmod +x "$BIN_CURL_401/curl"
|
|
145
|
+
|
|
146
|
+
# Fake security + uname (forces the macOS branch of credential-store.sh) so
|
|
147
|
+
# the no-secret-on-argv property of `security -i` writes can be asserted on
|
|
148
|
+
# any platform without touching a real keychain.
|
|
149
|
+
BIN_DARWIN="$WORK/bin-darwin"
|
|
150
|
+
mkdir -p "$BIN_DARWIN"
|
|
151
|
+
cat > "$BIN_DARWIN/uname" <<'FAKE'
|
|
152
|
+
#!/bin/bash
|
|
153
|
+
echo "Darwin"
|
|
154
|
+
FAKE
|
|
155
|
+
cat > "$BIN_DARWIN/security" <<'FAKE'
|
|
156
|
+
#!/bin/bash
|
|
157
|
+
printf '%s\n' "$*" >> "$SMOKE_CAPTURE_DIR/security-argv.log"
|
|
158
|
+
if [ "${1:-}" = "-i" ]; then
|
|
159
|
+
cat >> "$SMOKE_CAPTURE_DIR/security-stdin.log"
|
|
160
|
+
fi
|
|
161
|
+
exit 0
|
|
162
|
+
FAKE
|
|
163
|
+
chmod +x "$BIN_DARWIN/uname" "$BIN_DARWIN/security"
|
|
164
|
+
|
|
165
|
+
# -----------------------------------------------------------------------------
|
|
166
|
+
note "1. fetch-confluence.sh"
|
|
167
|
+
# -----------------------------------------------------------------------------
|
|
168
|
+
expect_rc 4 "no args exits 4 (usage)" \
|
|
169
|
+
"$SH" "$LIB/fetch-confluence.sh"
|
|
170
|
+
expect_rc 4 "--help exits 4 (usage)" \
|
|
171
|
+
"$SH" "$LIB/fetch-confluence.sh" --help
|
|
172
|
+
|
|
173
|
+
# Regression guard for the env-prefix parse bug: a plain page URL must get
|
|
174
|
+
# past host/pageId resolution and fail at the credential stage (exit 2),
|
|
175
|
+
# never with the parse error.
|
|
176
|
+
expect_rc 2 "URL parse reaches credential stage (viewpage.action?pageId)" \
|
|
177
|
+
env HOME="$FAKE_HOME" USER=smoke \
|
|
178
|
+
"$SH" "$LIB/fetch-confluence.sh" \
|
|
179
|
+
"https://confluence.example.com/pages/viewpage.action?pageId=12345"
|
|
180
|
+
assert_in_file "missing-token" "$ERR" "blocked JSON names missing-token"
|
|
181
|
+
assert_not_in_file "could not determine Confluence host" "$ERR" \
|
|
182
|
+
"no host-parse error (env-prefix regression)"
|
|
183
|
+
|
|
184
|
+
expect_rc 2 "URL parse reaches credential stage (cloud /wiki/spaces form)" \
|
|
185
|
+
env HOME="$FAKE_HOME" USER=smoke \
|
|
186
|
+
"$SH" "$LIB/fetch-confluence.sh" \
|
|
187
|
+
"https://tenant.atlassian.net/wiki/spaces/ENG/pages/99887/Some+Page"
|
|
188
|
+
assert_in_file "missing-token" "$ERR" "cloud form also blocked on token only"
|
|
189
|
+
|
|
190
|
+
# With a stub token and a failing curl, the failure must be the documented
|
|
191
|
+
# network error (3), proving parsing and auth wiring are past.
|
|
192
|
+
expect_rc 3 "stub token + offline curl exits 3 (network stage)" \
|
|
193
|
+
env HOME="$FAKE_HOME" USER=smoke FAKE_CRED_VALUE=fake-token \
|
|
194
|
+
PATH="$BIN_CURL_FAIL:$PATH" \
|
|
195
|
+
"$SH" "$LIB/fetch-confluence.sh" \
|
|
196
|
+
"https://confluence.example.com/pages/viewpage.action?pageId=12345"
|
|
197
|
+
assert_in_file "Confluence GET failed" "$ERR" "network-stage error message"
|
|
198
|
+
|
|
199
|
+
# -----------------------------------------------------------------------------
|
|
200
|
+
note "2. fetch-crashlytics.sh"
|
|
201
|
+
# -----------------------------------------------------------------------------
|
|
202
|
+
expect_rc 4 "no args exits 4 (usage)" \
|
|
203
|
+
"$SH" "$LIB/fetch-crashlytics.sh"
|
|
204
|
+
expect_rc 4 "--help exits 4 (usage)" \
|
|
205
|
+
"$SH" "$LIB/fetch-crashlytics.sh" --help
|
|
206
|
+
|
|
207
|
+
CRASH_URL="https://console.firebase.google.com/project/my-proj/crashlytics/app/ios:com.example.app/issues/abc123"
|
|
208
|
+
expect_rc 2 "URL parse reaches credential stage" \
|
|
209
|
+
env HOME="$FAKE_HOME" USER=smoke \
|
|
210
|
+
"$SH" "$LIB/fetch-crashlytics.sh" "$CRASH_URL"
|
|
211
|
+
assert_in_file "missing-token" "$ERR" "blocked JSON names missing-token"
|
|
212
|
+
|
|
213
|
+
# Stub SA JSON whose project_id mismatches the URL: proves parse + credential
|
|
214
|
+
# read + base64 decode + JSON validation all pass, fully offline (exit 5).
|
|
215
|
+
MISMATCH_SA_B64=$(printf '{"project_id":"other-proj"}' | base64 | tr -d '\n')
|
|
216
|
+
expect_rc 5 "stub SA JSON reaches project-mismatch check (exit 5)" \
|
|
217
|
+
env HOME="$FAKE_HOME" USER=smoke FAKE_CRED_VALUE="$MISMATCH_SA_B64" \
|
|
218
|
+
"$SH" "$LIB/fetch-crashlytics.sh" "$CRASH_URL"
|
|
219
|
+
assert_in_file "project_id" "$ERR" "mismatch error mentions project_id"
|
|
220
|
+
|
|
221
|
+
# The decoded SA JSON must never touch a predictable /tmp path.
|
|
222
|
+
assert_not_in_file "/tmp/.fc-sa" "$LIB/fetch-crashlytics.sh" \
|
|
223
|
+
"predictable /tmp SA path removed from source"
|
|
224
|
+
|
|
225
|
+
# -----------------------------------------------------------------------------
|
|
226
|
+
note "3. fetch-swagger.sh"
|
|
227
|
+
# -----------------------------------------------------------------------------
|
|
228
|
+
expect_rc 4 "no args exits 4 (usage)" \
|
|
229
|
+
"$SH" "$LIB/fetch-swagger.sh"
|
|
230
|
+
|
|
231
|
+
expect_rc 2 "offline curl exits 2 (network stage)" \
|
|
232
|
+
env PATH="$BIN_CURL_FAIL:$PATH" \
|
|
233
|
+
"$SH" "$LIB/fetch-swagger.sh" "https://spec.example.com/openapi.json"
|
|
234
|
+
|
|
235
|
+
# Uppercase .YAML suffix with a non-matching content type exercises the
|
|
236
|
+
# lowercase-URL branch (formerly bash-4 ${URL,,}, dies on /bin/bash 3.2).
|
|
237
|
+
expect_rc 0 "uppercase .YAML suffix detected via lowercase branch" \
|
|
238
|
+
env PATH="$BIN_CURL_META:$PATH" \
|
|
239
|
+
"$SH" "$LIB/fetch-swagger.sh" "https://spec.example.com/openapi.YAML"
|
|
240
|
+
assert_in_file '"format": "yaml"' "$OUT" "output reports yaml format"
|
|
241
|
+
|
|
242
|
+
# -----------------------------------------------------------------------------
|
|
243
|
+
note "4. fetch-fortify.sh"
|
|
244
|
+
# -----------------------------------------------------------------------------
|
|
245
|
+
expect_rc 6 "no host configured exits 6" \
|
|
246
|
+
env HOME="$FAKE_HOME" USER=smoke \
|
|
247
|
+
"$SH" "$LIB/fetch-fortify.sh" --version-id 123
|
|
248
|
+
expect_rc 4 "host override + no version id exits 4 (usage)" \
|
|
249
|
+
env HOME="$FAKE_HOME" USER=smoke FORTIFY_HOST_OVERRIDE=ssc.example.com \
|
|
250
|
+
"$SH" "$LIB/fetch-fortify.sh"
|
|
251
|
+
|
|
252
|
+
FORTIFY_URL="https://ssc.example.com/ssc/html/ssc/version/123/fix/ABCD-1"
|
|
253
|
+
expect_rc 2 "URL parse reaches credential stage" \
|
|
254
|
+
env HOME="$FAKE_HOME" USER=smoke FORTIFY_HOST_OVERRIDE=ssc.example.com \
|
|
255
|
+
"$SH" "$LIB/fetch-fortify.sh" "$FORTIFY_URL"
|
|
256
|
+
assert_in_file "missing-token" "$ERR" "blocked JSON names missing-token"
|
|
257
|
+
|
|
258
|
+
expect_rc 0 "stub token + offline curl degrades to empty result (exit 0)" \
|
|
259
|
+
env HOME="$FAKE_HOME" USER=smoke FORTIFY_HOST_OVERRIDE=ssc.example.com \
|
|
260
|
+
FAKE_CRED_VALUE=fake-token PATH="$BIN_CURL_FAIL:$PATH" \
|
|
261
|
+
"$SH" "$LIB/fetch-fortify.sh" "$FORTIFY_URL"
|
|
262
|
+
assert_in_file '"gateOutcome"' "$OUT" "normalized JSON still emitted"
|
|
263
|
+
|
|
264
|
+
# -----------------------------------------------------------------------------
|
|
265
|
+
note "5. figma-screenshot.sh"
|
|
266
|
+
# -----------------------------------------------------------------------------
|
|
267
|
+
expect_rc 1 "no args exits 1 (usage)" \
|
|
268
|
+
"$SH" "$LIB/figma-screenshot.sh"
|
|
269
|
+
expect_rc 0 "--help exits 0" \
|
|
270
|
+
"$SH" "$LIB/figma-screenshot.sh" --help
|
|
271
|
+
|
|
272
|
+
expect_rc 3 "no PAT resolvable exits 3 (Tier 3 required)" \
|
|
273
|
+
env -u FIGMA_PAT HOME="$FAKE_HOME" USER=smoke \
|
|
274
|
+
"$SH" "$LIB/figma-screenshot.sh" --file-key abc123 --node-id 1-2 \
|
|
275
|
+
--output-dir "$WORK/shots-no-pat"
|
|
276
|
+
|
|
277
|
+
expect_rc 2 "stub PAT + 401 curl exits 2 (auth stage, past parsing)" \
|
|
278
|
+
env HOME="$FAKE_HOME" USER=smoke FIGMA_PAT=fake-pat \
|
|
279
|
+
PATH="$BIN_CURL_401:$PATH" \
|
|
280
|
+
"$SH" "$LIB/figma-screenshot.sh" --file-key abc123 --node-id 1-2 \
|
|
281
|
+
--output-dir "$WORK/shots-401"
|
|
282
|
+
|
|
283
|
+
# -----------------------------------------------------------------------------
|
|
284
|
+
note "6. figma-mcp-refresh.sh"
|
|
285
|
+
# -----------------------------------------------------------------------------
|
|
286
|
+
expect_rc 2 "missing prefs exits 2" \
|
|
287
|
+
env HOME="$FAKE_HOME" USER=smoke \
|
|
288
|
+
"$SH" "$LIB/figma-mcp-refresh.sh"
|
|
289
|
+
|
|
290
|
+
if command -v jq >/dev/null 2>&1; then
|
|
291
|
+
PREFS_HOME="$WORK/home-prefs"
|
|
292
|
+
mkdir -p "$PREFS_HOME/.claude"
|
|
293
|
+
printf '{"global":{"keychainMapping":{"figma_mcp":"Smoke_Figma_MCP"}}}\n' \
|
|
294
|
+
> "$PREFS_HOME/.claude/multi-agent-preferences.json"
|
|
295
|
+
export SMOKE_CAPTURE_DIR="$WORK"
|
|
296
|
+
expect_rc 2 "mapped key + empty keychain exits 2 (no refresh token)" \
|
|
297
|
+
env HOME="$PREFS_HOME" USER=smoke SMOKE_CAPTURE_DIR="$WORK" \
|
|
298
|
+
PATH="$BIN_DARWIN:$PATH" \
|
|
299
|
+
"$SH" "$LIB/figma-mcp-refresh.sh"
|
|
300
|
+
else
|
|
301
|
+
note " (jq not installed - skipping mapped-key case)"
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# -----------------------------------------------------------------------------
|
|
305
|
+
note "7. credential-store.sh + keychain-save.sh"
|
|
306
|
+
# -----------------------------------------------------------------------------
|
|
307
|
+
expect_rc 0 "platform subcommand works" \
|
|
308
|
+
"$SH" "$LIB/credential-store.sh" platform
|
|
309
|
+
expect_rc 3 "get without key exits 3 (usage)" \
|
|
310
|
+
"$SH" "$LIB/credential-store.sh" get
|
|
311
|
+
expect_rc 1 "keychain-save without service name exits 1 (usage)" \
|
|
312
|
+
"$SH" "$SCRIPTS/keychain-save.sh"
|
|
313
|
+
|
|
314
|
+
# set <key> - : the secret must travel via stdin to `security -i`, never on
|
|
315
|
+
# the security argv. Fake uname forces the macOS branch; fake security logs
|
|
316
|
+
# argv and stdin separately.
|
|
317
|
+
rm -f "$WORK/security-argv.log" "$WORK/security-stdin.log"
|
|
318
|
+
SECRET_VALUE="topsecret-smoke-value-123"
|
|
319
|
+
rc=0
|
|
320
|
+
printf '%s' "$SECRET_VALUE" \
|
|
321
|
+
| env PATH="$BIN_DARWIN:$PATH" KEYCHAIN_DELEGATE=0 SMOKE_CAPTURE_DIR="$WORK" \
|
|
322
|
+
"$SH" "$LIB/credential-store.sh" set smoke-test-key - >"$OUT" 2>"$ERR" || rc=$?
|
|
323
|
+
if [ "$rc" -eq 0 ]; then
|
|
324
|
+
pass "set <key> - via stdin succeeds (rc=0)"
|
|
325
|
+
else
|
|
326
|
+
fail "set <key> - via stdin failed (rc=$rc)"
|
|
327
|
+
tail -5 "$ERR" | sed 's/^/ /'
|
|
328
|
+
fi
|
|
329
|
+
assert_in_file "^-i" "$WORK/security-argv.log" "security invoked in -i stdin mode"
|
|
330
|
+
assert_not_in_file "$SECRET_VALUE" "$WORK/security-argv.log" "secret absent from security argv"
|
|
331
|
+
assert_in_file "$SECRET_VALUE" "$WORK/security-stdin.log" "secret delivered via stdin"
|
|
332
|
+
|
|
333
|
+
# -----------------------------------------------------------------------------
|
|
334
|
+
note "8. static source checks (bash-3.2 compat + no secrets on argv)"
|
|
335
|
+
# -----------------------------------------------------------------------------
|
|
336
|
+
OWNED_FILES=(
|
|
337
|
+
"$LIB/fetch-confluence.sh"
|
|
338
|
+
"$LIB/fetch-crashlytics.sh"
|
|
339
|
+
"$LIB/fetch-swagger.sh"
|
|
340
|
+
"$LIB/fetch-fortify.sh"
|
|
341
|
+
"$LIB/figma-mcp-refresh.sh"
|
|
342
|
+
"$LIB/figma-screenshot.sh"
|
|
343
|
+
"$LIB/credential-store.sh"
|
|
344
|
+
"$SCRIPTS/keychain-save.sh"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if grep -nE '\$\{[A-Za-z_0-9]+(,,|\^\^)|mapfile |readarray |declare -A' "${OWNED_FILES[@]}"; then
|
|
348
|
+
fail "bash-4-only construct found (must run on /bin/bash 3.2)"
|
|
349
|
+
else
|
|
350
|
+
pass "no bash-4-only constructs"
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
if grep -nE -- '-H "(Authorization|X-Figma-Token)' "${OWNED_FILES[@]}"; then
|
|
354
|
+
fail "auth header passed to curl via argv (-H)"
|
|
355
|
+
else
|
|
356
|
+
pass "no auth headers on curl argv"
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
if grep -nE -- '--data-urlencode "(refresh_token|client_secret)' "${OWNED_FILES[@]}"; then
|
|
360
|
+
fail "OAuth secret passed to curl via argv (--data-urlencode)"
|
|
361
|
+
else
|
|
362
|
+
pass "no OAuth secrets on curl argv"
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
if grep -nE 'add-generic-password[^|]*-w "\$' "${OWNED_FILES[@]}"; then
|
|
366
|
+
fail "keychain secret passed to security via argv (-w)"
|
|
367
|
+
else
|
|
368
|
+
pass "no keychain secrets on security argv"
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
# shellcheck disable=SC2016 # literal $HOME is the pattern being hunted
|
|
372
|
+
if grep -nE '\$HOME/\.claude/lib/credential-store\.sh|~/\.claude/lib/credential-store\.sh' "${OWNED_FILES[@]}"; then
|
|
373
|
+
fail "hardcoded credential-store path (must go through the resolver)"
|
|
374
|
+
else
|
|
375
|
+
pass "credential-store access routed through the resolver"
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
# -----------------------------------------------------------------------------
|
|
379
|
+
echo ""
|
|
380
|
+
echo "smoke-fetchers-offline: $PASS passed, $FAIL failed"
|
|
381
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
382
|
+
exit 0
|
|
@@ -91,11 +91,11 @@ echo ""
|
|
|
91
91
|
echo "→ 8. registration + count consistency (36)"
|
|
92
92
|
grep -Fq '| `create-jira ' "$DISPATCHER" && pass "dispatcher routing: create-jira" || fail "dispatcher routing missing: generate"
|
|
93
93
|
grep -Fq "create-jira/SKILL.md" "$DISPATCHER" && pass "dispatcher loading row" || fail "dispatcher loading row missing"
|
|
94
|
-
grep -Fq "## 1. Command Inventory (
|
|
94
|
+
grep -Fq "## 1. Command Inventory (38 commands)" "$CONTRACT" && pass "contract count = 38" || fail "contract count wrong"
|
|
95
95
|
grep -Eq '(^|[ ,])create-jira([ ,]|$)' "$CONTRACT" && pass "contract list has create-jira" || fail "contract list missing generate"
|
|
96
|
-
grep -Fq "**
|
|
96
|
+
grep -Fq "**38 commands are synced**" "$SYNC" && pass "sync count = 38" || fail "sync count wrong"
|
|
97
97
|
SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
|
|
98
|
-
grep -Fq "**
|
|
98
|
+
grep -Fq "**38 commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = 38" || fail "sync SKILL count wrong (Copilot parity)"
|
|
99
99
|
grep -Fq "create-jira" "$ROOT/pipeline/skills/shared/core/multi-agent-help/SKILL.md" && pass "help SKILL lists create-jira" || fail "help SKILL missing create-jira (Copilot parity)"
|
|
100
100
|
grep -c "multi-agent:create-jira" "$HELP" | awk '{exit !($1>=2)}' && pass "help.md has EN + TR entries" || fail "help.md entries incomplete"
|
|
101
101
|
# no stale split-command references anywhere in the synced surfaces
|
|
@@ -110,11 +110,19 @@ assert_file "$TMP_CLAUDE/.claude/CLAUDE.md" "~/.claude/CLAUDE.md"
|
|
|
110
110
|
assert_file "$TMP_CLAUDE/.claude/multi-agent-preferences.json" "~/.claude/multi-agent-preferences.json"
|
|
111
111
|
assert_file "$TMP_CLAUDE/.claude/settings.json" "~/.claude/settings.json"
|
|
112
112
|
|
|
113
|
-
# settings.json must have the secret-detection hook
|
|
113
|
+
# settings.json must have the secret-detection hook. v11.4.1 changed the
|
|
114
|
+
# matcher from the dead "Bash(git commit:*)" (tool-name matchers never matched
|
|
115
|
+
# it) to "Bash"; command-level filtering happens inside pre-commit-check.sh.
|
|
116
|
+
if grep -q '"matcher": "Bash"' "$TMP_CLAUDE/.claude/settings.json" \
|
|
117
|
+
&& grep -q "pre-commit-check.sh" "$TMP_CLAUDE/.claude/settings.json"; then
|
|
118
|
+
pass "settings.json registers PreToolUse secret-scan hook (matcher Bash)"
|
|
119
|
+
else
|
|
120
|
+
fail "settings.json missing PreToolUse secret-scan hook with matcher Bash"
|
|
121
|
+
fi
|
|
114
122
|
if grep -q "Bash(git commit:\*)" "$TMP_CLAUDE/.claude/settings.json"; then
|
|
115
|
-
|
|
123
|
+
fail "settings.json still contains the dead Bash(git commit:*) matcher"
|
|
116
124
|
else
|
|
117
|
-
|
|
125
|
+
pass "settings.json has no dead Bash(git commit:*) matcher"
|
|
118
126
|
fi
|
|
119
127
|
if grep -q "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE" "$TMP_CLAUDE/.claude/settings.json"; then
|
|
120
128
|
pass "settings.json sets CLAUDE_AUTOCOMPACT_PCT_OVERRIDE"
|
|
@@ -136,6 +136,18 @@ else
|
|
|
136
136
|
else
|
|
137
137
|
fail "jira-id path output unexpected"
|
|
138
138
|
fi
|
|
139
|
+
|
|
140
|
+
# Bare "#316" and "316" must land in the gh-num branch (needsRepoPicker
|
|
141
|
+
# descriptor when no default owner/repo), NOT gh-short with an empty repo
|
|
142
|
+
# prefix that builds https://github.com/<owner>//issues/316.
|
|
143
|
+
for bare in "#316" "316"; do
|
|
144
|
+
out=$(ACCOUNT_DEFAULT_OWNER="" ACCOUNT_DEFAULT_REPO="" "$FETCHER" "$bare" 2>/dev/null || echo "")
|
|
145
|
+
if printf '%s' "$out" | python3 -c 'import json,sys; d=json.loads(sys.stdin.read()); assert d["kind"]=="github", d["kind"]; assert d["key"]=="316", d["key"]; assert d.get("needsRepoPicker") is True, d' 2>/dev/null; then
|
|
146
|
+
ok "bare '$bare' classified as gh-num (key=316, needsRepoPicker)"
|
|
147
|
+
else
|
|
148
|
+
fail "bare '$bare' misclassified: $(printf '%s' "$out" | head -c 200)"
|
|
149
|
+
fi
|
|
150
|
+
done
|
|
139
151
|
fi
|
|
140
152
|
|
|
141
153
|
# --- multi-repo-pipeline.sh: bridge --------------------------------------
|
|
@@ -294,6 +306,20 @@ assert d["projects"][1]["project"] == "repo-b"
|
|
|
294
306
|
fail "bridge output missing projects[] entries"
|
|
295
307
|
fi
|
|
296
308
|
|
|
309
|
+
# teardown guard: a worktreeRoot of "." (the current project root) must be
|
|
310
|
+
# refused loudly, never rm -rf'd.
|
|
311
|
+
UNSAFE_STATE="$FIXTURE_ROOT/unsafe-state.json"
|
|
312
|
+
sed 's|"worktreeRoot":"[^"]*"|"worktreeRoot":"."|' "$FIXTURE_ROOT/state.json" > "$UNSAFE_STATE"
|
|
313
|
+
GUARD_DIR=$(mktemp -d)
|
|
314
|
+
( cd "$GUARD_DIR" && "$MRP" teardown "$UNSAFE_STATE" ) >/dev/null 2>&1
|
|
315
|
+
guard_rc=$?
|
|
316
|
+
if [ "$guard_rc" -ne 0 ] && [ -d "$GUARD_DIR" ]; then
|
|
317
|
+
ok "teardown refuses worktreeRoot='.' (exit=$guard_rc, dir intact)"
|
|
318
|
+
else
|
|
319
|
+
fail "teardown did not refuse unsafe worktreeRoot='.' (exit=$guard_rc)"
|
|
320
|
+
fi
|
|
321
|
+
rm -rf "$GUARD_DIR"
|
|
322
|
+
|
|
297
323
|
# teardown
|
|
298
324
|
"$MRP" teardown "$FIXTURE_ROOT/state.json" >/dev/null 2>&1
|
|
299
325
|
if [ ! -d "$WORKTREE_ROOT" ]; then
|
|
@@ -357,13 +383,40 @@ EOF
|
|
|
357
383
|
|
|
358
384
|
# render-jira: bulleted PR list at top
|
|
359
385
|
rendered=$("$CMR" render-jira "$TMP/multi.json" "$TMP/body.md" 2>/dev/null)
|
|
360
|
-
bullet_count=$(printf '%s' "$rendered" | grep -c '^\* PR:' ||
|
|
386
|
+
bullet_count=$(printf '%s' "$rendered" | grep -c '^\* PR:' || true)
|
|
361
387
|
if [ "$bullet_count" = "2" ]; then
|
|
362
388
|
ok "render-jira prepends bulleted PR list (2 entries)"
|
|
363
389
|
else
|
|
364
390
|
fail "render-jira bullet count $bullet_count != 2"
|
|
365
391
|
fi
|
|
366
392
|
|
|
393
|
+
# render-jira/-conf with ZERO PR urls: must pass the body through unchanged
|
|
394
|
+
# (the old `grep -c || echo 0` idiom emitted "0\n0" here and prepended
|
|
395
|
+
# bogus "* PR:" bullets to the posted body).
|
|
396
|
+
cat > "$TMP/noprs.json" <<'EOF'
|
|
397
|
+
{
|
|
398
|
+
"schemaVersion":"2.1.0","taskId":"Z","shortId":1,"project":"my-app",
|
|
399
|
+
"branch":"feature/Z","baseBranch":"develop","currentPhase":6,"status":"in_progress",
|
|
400
|
+
"startedAt":"2026-04-27T12:00:00Z","phases":{},"identity":{"name":"T","email":"t@e.com"},
|
|
401
|
+
"projects":[
|
|
402
|
+
{"project":"my-app","projectRoot":"/tmp/a","branch":"feature/Z","baseBranch":"develop","identity":{"name":"T","email":"t@e.com"},"pr":null},
|
|
403
|
+
{"project":"common","projectRoot":"/tmp/b","branch":"feature/Z","baseBranch":"develop","identity":{"name":"T","email":"t@e.com"},"pr":null}
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
EOF
|
|
407
|
+
rendered=$("$CMR" render-jira "$TMP/noprs.json" "$TMP/body.md" 2>/dev/null)
|
|
408
|
+
if [ "$rendered" = "body content" ]; then
|
|
409
|
+
ok "render-jira with zero PR urls passes body through unchanged"
|
|
410
|
+
else
|
|
411
|
+
fail "render-jira with zero PR urls mutated body: $rendered"
|
|
412
|
+
fi
|
|
413
|
+
rendered=$("$CMR" render-conf "$TMP/noprs.json" "$TMP/body.md" 2>/dev/null)
|
|
414
|
+
if [ "$rendered" = "body content" ]; then
|
|
415
|
+
ok "render-conf with zero PR urls passes body through unchanged"
|
|
416
|
+
else
|
|
417
|
+
fail "render-conf with zero PR urls mutated body: $rendered"
|
|
418
|
+
fi
|
|
419
|
+
|
|
367
420
|
# render-conf: ## Related PRs heading
|
|
368
421
|
rendered=$("$CMR" render-conf "$TMP/multi.json" "$TMP/body.md" 2>/dev/null)
|
|
369
422
|
if printf '%s' "$rendered" | grep -q "## Related PRs"; then
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# smoke-pack-contents.sh - guard what `npm publish` would actually ship.
|
|
4
|
+
#
|
|
5
|
+
# 1. Deny patterns: package.json files[] carries negations (`!...`) for
|
|
6
|
+
# dev-only / personal-data files. The published tarball must contain
|
|
7
|
+
# no file matching any of them. Patterns are read from package.json at
|
|
8
|
+
# runtime (via node), so the smoke never goes stale when the deny list
|
|
9
|
+
# changes.
|
|
10
|
+
# 2. File-count band: the total packed file count is compared against the
|
|
11
|
+
# committed fixture pipeline/scripts/fixtures/pack-expected-count.txt.
|
|
12
|
+
# Drift beyond +-15 percent fails - that magnitude means the files[]
|
|
13
|
+
# globs picked up (or dropped) a whole subtree, which deserves a human
|
|
14
|
+
# look. Refresh the fixture after reviewing:
|
|
15
|
+
# npm pack --dry-run --json 2>/dev/null \
|
|
16
|
+
# | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s)[0].files.length))' \
|
|
17
|
+
# > pipeline/scripts/fixtures/pack-expected-count.txt
|
|
18
|
+
# 3. publint (optional): linted via `npx --no-install` so it only runs
|
|
19
|
+
# when the package is already installed - never a network fetch.
|
|
20
|
+
#
|
|
21
|
+
# Exit codes: 0=pass, 1=violation, 2=environment broken
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
26
|
+
FIXTURE="$REPO_ROOT/pipeline/scripts/fixtures/pack-expected-count.txt"
|
|
27
|
+
|
|
28
|
+
command -v npm >/dev/null 2>&1 || { echo "FAIL: npm not on PATH"; exit 2; }
|
|
29
|
+
command -v node >/dev/null 2>&1 || { echo "FAIL: node not on PATH"; exit 2; }
|
|
30
|
+
|
|
31
|
+
PASS=0
|
|
32
|
+
FAIL=0
|
|
33
|
+
pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
|
|
34
|
+
fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
|
|
35
|
+
|
|
36
|
+
cleanup() { [ -n "${TMP:-}" ] && rm -rf "$TMP"; }
|
|
37
|
+
trap cleanup EXIT
|
|
38
|
+
TMP=$(mktemp -d)
|
|
39
|
+
|
|
40
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
41
|
+
echo "→ 1. npm pack --dry-run --json"
|
|
42
|
+
if ! (cd "$REPO_ROOT" && npm pack --dry-run --json 2>/dev/null > "$TMP/pack.json"); then
|
|
43
|
+
echo "FAIL: npm pack --dry-run failed"
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Deny check runs in node so the patterns come straight from package.json
|
|
48
|
+
# files[] negations - never hardcoded here.
|
|
49
|
+
cat > "$TMP/check.mjs" <<'MJS'
|
|
50
|
+
import { readFileSync } from "node:fs";
|
|
51
|
+
|
|
52
|
+
const [packJsonPath, pkgJsonPath] = process.argv.slice(2);
|
|
53
|
+
const packed = JSON.parse(readFileSync(packJsonPath, "utf-8"))[0].files.map((f) => f.path);
|
|
54
|
+
const files = JSON.parse(readFileSync(pkgJsonPath, "utf-8")).files || [];
|
|
55
|
+
const denies = files.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
|
|
56
|
+
|
|
57
|
+
// Minimal glob -> regex: '**' spans path separators, '*' does not.
|
|
58
|
+
const toRegex = (glob) => {
|
|
59
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
60
|
+
const body = escaped
|
|
61
|
+
.split("**")
|
|
62
|
+
.map((part) => part.replace(/\*/g, "[^/]*"))
|
|
63
|
+
.join(".*");
|
|
64
|
+
return new RegExp("^" + body + "$");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const regexes = denies.map((d) => [d, toRegex(d)]);
|
|
68
|
+
let bad = 0;
|
|
69
|
+
for (const path of packed) {
|
|
70
|
+
for (const [glob, re] of regexes) {
|
|
71
|
+
if (re.test(path)) {
|
|
72
|
+
console.log(`DENY ${path} (matches !${glob})`);
|
|
73
|
+
bad++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.log(`DENYCOUNT ${bad}`);
|
|
78
|
+
console.log(`PATTERNS ${denies.length}`);
|
|
79
|
+
console.log(`COUNT ${packed.length}`);
|
|
80
|
+
MJS
|
|
81
|
+
|
|
82
|
+
CHECK=$(node "$TMP/check.mjs" "$TMP/pack.json" "$REPO_ROOT/package.json")
|
|
83
|
+
COUNT=$(echo "$CHECK" | awk '/^COUNT /{print $2}')
|
|
84
|
+
DENYCOUNT=$(echo "$CHECK" | awk '/^DENYCOUNT /{print $2}')
|
|
85
|
+
PATTERNS=$(echo "$CHECK" | awk '/^PATTERNS /{print $2}')
|
|
86
|
+
|
|
87
|
+
if [ -z "$COUNT" ] || [ -z "$DENYCOUNT" ]; then
|
|
88
|
+
echo "FAIL: deny-pattern checker produced no result (node error?)"
|
|
89
|
+
exit 2
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
pass "pack dry-run produced $COUNT files ($PATTERNS deny patterns loaded from package.json)"
|
|
93
|
+
|
|
94
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
95
|
+
echo "→ 2. deny patterns (files[] negations must not ship)"
|
|
96
|
+
if [ "$DENYCOUNT" -eq 0 ]; then
|
|
97
|
+
pass "no packed file matches a files[] negation"
|
|
98
|
+
else
|
|
99
|
+
fail "$DENYCOUNT packed file(s) match dev-only/personal-data exclusions:"
|
|
100
|
+
echo "$CHECK" | grep '^DENY ' | sed 's/^/ /'
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
104
|
+
echo "→ 3. file-count band vs fixture (+-15 percent)"
|
|
105
|
+
if [ ! -f "$FIXTURE" ]; then
|
|
106
|
+
fail "fixture missing: $FIXTURE - create it from the refresh command in this script's header"
|
|
107
|
+
else
|
|
108
|
+
EXPECTED=$(tr -cd '0-9' < "$FIXTURE")
|
|
109
|
+
if [ -z "$EXPECTED" ] || [ "$EXPECTED" -eq 0 ]; then
|
|
110
|
+
fail "fixture unreadable or zero: $FIXTURE"
|
|
111
|
+
else
|
|
112
|
+
DIFF=$((COUNT - EXPECTED))
|
|
113
|
+
[ "$DIFF" -lt 0 ] && DIFF=$((-DIFF))
|
|
114
|
+
LIMIT=$((EXPECTED * 15 / 100))
|
|
115
|
+
if [ "$DIFF" -le "$LIMIT" ]; then
|
|
116
|
+
pass "count $COUNT within +-15 percent of expected $EXPECTED (drift $DIFF, limit $LIMIT)"
|
|
117
|
+
else
|
|
118
|
+
fail "count $COUNT drifted beyond +-15 percent of expected $EXPECTED (drift $DIFF, limit $LIMIT)"
|
|
119
|
+
echo " Review what the files[] globs now include, then refresh the fixture"
|
|
120
|
+
echo " (refresh command is in this script's header comment)."
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
126
|
+
echo "→ 4. publint (optional, npx --no-install only)"
|
|
127
|
+
if (cd "$REPO_ROOT" && npx --no-install publint --help >/dev/null 2>&1); then
|
|
128
|
+
if OUT=$(cd "$REPO_ROOT" && npx --no-install publint 2>&1); then
|
|
129
|
+
pass "publint: no errors"
|
|
130
|
+
else
|
|
131
|
+
fail "publint reported errors"
|
|
132
|
+
echo "$OUT" | sed 's/^/ /'
|
|
133
|
+
fi
|
|
134
|
+
else
|
|
135
|
+
echo " ↷ SKIP: publint not installed (npx --no-install found no local package)"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
echo ""
|
|
139
|
+
echo "══ smoke-pack-contents: $PASS passed, $FAIL failed ══"
|
|
140
|
+
[ "$FAIL" -gt 0 ] && exit 1 || exit 0
|