@biffo/cli 0.301.20 → 0.301.22
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/_skeletons/plugin-template/scripts/check-closing-keywords.mjs +14 -5
- package/_skeletons/plugin-template/scripts/js-dependency-audit.sh +77 -10
- package/_skeletons/sibling-template/scripts/check-closing-keywords.mjs +14 -5
- package/_skeletons/sibling-template/scripts/js-dependency-audit.sh +77 -10
- package/package.json +1 -1
|
@@ -424,8 +424,16 @@ export function deliberateClosingReferences(text, { code = true } = {}) {
|
|
|
424
424
|
* carried only `Refs #133`. The issue closed on merge anyway. The lesson
|
|
425
425
|
* recorded then: *keeping a denial out of the commit is not sufficient —
|
|
426
426
|
* GitHub's linker reads the PR description text on its own.*
|
|
427
|
-
* - #1238 / #1021 (2026-08-03) —
|
|
428
|
-
*
|
|
427
|
+
* - biffo-template#1238 / biffo-template#1021 (2026-08-03) — this repo's own
|
|
428
|
+
* issues, not an external one: `- **Does not close #1021.**` in the body,
|
|
429
|
+
* `Refs #1021` in the commit, #1021 closed by the squash-merge. (#1872
|
|
430
|
+
* re-checked this entry against the wrong repo — tabsii-platform, where no
|
|
431
|
+
* such pair exists at that date — and, not finding it there, filed a
|
|
432
|
+
* doubt. Re-verified directly against biffo-template's own #1021/#1238:
|
|
433
|
+
* the pairing, the quoted lines and the date all check out exactly as
|
|
434
|
+
* recorded. The two bare `#N`s were ambiguous next to the other entries'
|
|
435
|
+
* explicit `tabsii-platform#76`/`tabsii-crm#133` qualifiers, which is the
|
|
436
|
+
* actual defect #1872 found — now spelled out with the repo name.)
|
|
429
437
|
*
|
|
430
438
|
* The recorded fix each time was a *practice*: "never write a closing keyword
|
|
431
439
|
* in prose". Three occurrences produced a rule and no mechanism, and the
|
|
@@ -661,9 +669,10 @@ function formatNegatedFailure({ negated }) {
|
|
|
661
669
|
...negated.map((n) => ` ${n.source}, line ${n.lineNumber}: ${n.line}`),
|
|
662
670
|
'',
|
|
663
671
|
'This has now happened four times (tabsii-platform#76, tabsii-crm#133,',
|
|
664
|
-
'#1021 via #1238 — see #1245). Keeping the
|
|
665
|
-
'message is not enough: GitHub reads the PR
|
|
666
|
-
'(#1334) a commit message on its own,
|
|
672
|
+
'biffo-template#1021 via biffo-template#1238 — see #1245). Keeping the',
|
|
673
|
+
'denial out of the commit message is not enough: GitHub reads the PR',
|
|
674
|
+
'description on its own — and (#1334) a commit message on its own,',
|
|
675
|
+
'independent of the body.',
|
|
667
676
|
'',
|
|
668
677
|
'Rewrite the line so no closing keyword sits in front of the reference:',
|
|
669
678
|
...refs.map((r) => ` - \`Refs ${r}\`, or "leaves ${r} open"`),
|
|
@@ -80,12 +80,29 @@ attempts=3
|
|
|
80
80
|
inconclusive=0
|
|
81
81
|
failed=0
|
|
82
82
|
|
|
83
|
-
# Audit one directory. Returns 0 if clean or inconclusive, 1 on a real finding
|
|
84
|
-
#
|
|
83
|
+
# Audit one directory. Returns 0 if clean or inconclusive, 1 on a real finding,
|
|
84
|
+
# AND writes a one-word verdict (`ok` / `fail` / `inconclusive`) to `$4`.
|
|
85
|
+
#
|
|
86
|
+
# The verdict file exists because this function is invoked backgrounded (`&`,
|
|
87
|
+
# see the audit loop below) so the four (or however many) trees run in
|
|
88
|
+
# parallel rather than paying their registry round-trip one after another
|
|
89
|
+
# (#1874: 4 trees serially cost 15m21s in a real CI run and blew the job's
|
|
90
|
+
# 20-minute cap). A backgrounded call is a forked subshell — every variable it
|
|
91
|
+
# touches, including `inconclusive`/`failed` below, is a copy in that child
|
|
92
|
+
# process and vanishes when it exits. The return code has the same problem: a
|
|
93
|
+
# background job's exit status is only visible via `wait "$pid"`, one pid at a
|
|
94
|
+
# time, which is no simpler than a file and less robust (a killed/never-run
|
|
95
|
+
# job leaves nothing to wait on). So each invocation reports for itself, in
|
|
96
|
+
# writing, and the parent tallies the results after `wait` once every
|
|
97
|
+
# invocation has finished.
|
|
98
|
+
#
|
|
99
|
+
# `$1` is the directory, `$2` a human label, `$3` extra pnpm flags, `$4` the
|
|
100
|
+
# result file this invocation must write its verdict to.
|
|
85
101
|
audit_dir() {
|
|
86
102
|
dir="$1"
|
|
87
103
|
label="$2"
|
|
88
104
|
extra="$3"
|
|
105
|
+
resultfile="$4"
|
|
89
106
|
|
|
90
107
|
for attempt in $(seq 1 "$attempts"); do
|
|
91
108
|
# printf, never echo: the CI step runs `sh scripts/...` i.e. dash, whose
|
|
@@ -122,12 +139,14 @@ audit_dir() {
|
|
|
122
139
|
if [ "$((high + crit))" -gt 0 ]; then
|
|
123
140
|
echo "::error::${label}: ${crit} critical + ${high} high advisory(ies) across ${total} package(s); registry answered ${seen_at}."
|
|
124
141
|
printf '%s' "$out" | jq '.advisories // .metadata.vulnerabilities' 2>/dev/null | head -c 4000
|
|
142
|
+
echo "fail" >"$resultfile"
|
|
125
143
|
return 1
|
|
126
144
|
fi
|
|
127
145
|
# A bare "no advisories" is not falsifiable. State the population, the
|
|
128
146
|
# severities that did NOT block, and when the registry was asked, so a
|
|
129
147
|
# reader can tell a clean tree from a tree nobody looked at properly.
|
|
130
148
|
echo "${label}: 0 critical, 0 high across ${total} package(s) (${mod} moderate, ${low} low — reported, not blocking); registry answered ${seen_at}."
|
|
149
|
+
echo "ok" >"$resultfile"
|
|
131
150
|
return 0
|
|
132
151
|
fi
|
|
133
152
|
|
|
@@ -137,7 +156,7 @@ audit_dir() {
|
|
|
137
156
|
done
|
|
138
157
|
|
|
139
158
|
echo "::error::${label}: audit could not run after ${attempts} attempts (the registry returned a non-JSON/error response). Advisory scanning was NOT performed for this tree, so this is INCONCLUSIVE and BLOCKS — a gate that cannot see its input must not report clean (#1269, #591)."
|
|
140
|
-
inconclusive
|
|
159
|
+
echo "inconclusive" >"$resultfile"
|
|
141
160
|
return 0
|
|
142
161
|
}
|
|
143
162
|
|
|
@@ -197,11 +216,31 @@ for lock in $ALL_LOCKS; do
|
|
|
197
216
|
fi
|
|
198
217
|
done
|
|
199
218
|
|
|
200
|
-
# Audit each discovered tree.
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
219
|
+
# Audit each discovered tree IN PARALLEL (#1874). Each `audit_dir` call is a
|
|
220
|
+
# real network round-trip, with its own retry/backoff, to registry.npmjs.org
|
|
221
|
+
# — run one after another they cost roughly N x the slowest single tree (a
|
|
222
|
+
# real CI run measured 4 trees / 15m21s, blowing the job's 20-minute cap
|
|
223
|
+
# mid-way through ~9 other required guard steps). Backgrounding them lets the
|
|
224
|
+
# round-trips overlap instead.
|
|
225
|
+
#
|
|
226
|
+
# A one-word-per-tree result file (see `audit_dir` above) is how the parent
|
|
227
|
+
# shell learns each backgrounded verdict back: `$TMP_DIR` is created via
|
|
228
|
+
# `mktemp -d` (collision-safe by construction — no need to hand-roll a `$$`
|
|
229
|
+
# suffix on top of it) and torn down by the EXIT/HUP/INT/TERM trap below
|
|
230
|
+
# whichever way this script leaves.
|
|
231
|
+
#
|
|
232
|
+
# The workspace's own lockfile is audited WITHOUT --ignore-workspace, so pnpm
|
|
233
|
+
# resolves it normally; every other discovered lockfile is a separate,
|
|
234
|
+
# vendored project and needs the flag, or pnpm walks up, finds the workspace,
|
|
235
|
+
# and silently audits THAT instead — reporting clean for a tree it never
|
|
236
|
+
# looked at, which is this exact defect one level down.
|
|
237
|
+
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/js-dependency-audit.XXXXXX") || {
|
|
238
|
+
echo "::error::js-dependency-audit: could not create a temp directory for parallel results (mktemp failed)." >&2
|
|
239
|
+
exit 2
|
|
240
|
+
}
|
|
241
|
+
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
|
|
242
|
+
|
|
243
|
+
i=0
|
|
205
244
|
for lock in $ALL_LOCKS; do
|
|
206
245
|
dir=$(dirname "$lock")
|
|
207
246
|
dir_abs=$(cd "$dir" 2>/dev/null && pwd -P)
|
|
@@ -210,13 +249,41 @@ for lock in $ALL_LOCKS; do
|
|
|
210
249
|
"$REPO_ROOT"/*) rel=${dir_abs#"$REPO_ROOT"/} ;;
|
|
211
250
|
*) rel="$dir_abs" ;;
|
|
212
251
|
esac
|
|
252
|
+
i=$((i + 1))
|
|
253
|
+
resultfile="$TMP_DIR/result.$i"
|
|
213
254
|
if [ "$dir_abs" = "$WORKSPACE_ABS" ]; then
|
|
214
|
-
audit_dir "$dir" "pnpm audit (workspace: ${rel})" ""
|
|
255
|
+
audit_dir "$dir" "pnpm audit (workspace: ${rel})" "" "$resultfile" &
|
|
215
256
|
else
|
|
216
|
-
audit_dir "$dir" "pnpm audit (${rel})" "--ignore-workspace"
|
|
257
|
+
audit_dir "$dir" "pnpm audit (${rel})" "--ignore-workspace" "$resultfile" &
|
|
217
258
|
fi
|
|
218
259
|
done
|
|
219
260
|
|
|
261
|
+
# Wait for every backgrounded audit_dir before reading any result file back —
|
|
262
|
+
# reading early would race a tree that is still auditing.
|
|
263
|
+
wait
|
|
264
|
+
|
|
265
|
+
# Tally the verdicts the backgrounded invocations wrote for themselves. A
|
|
266
|
+
# result file that is missing or unreadable (the subshell was killed before
|
|
267
|
+
# it could write, or never started) fails CLOSED as inconclusive rather than
|
|
268
|
+
# being silently skipped — the same posture as the empty-discovery check
|
|
269
|
+
# above: a tree this run cannot account for is not a clean tree.
|
|
270
|
+
i=0
|
|
271
|
+
for lock in $ALL_LOCKS; do
|
|
272
|
+
i=$((i + 1))
|
|
273
|
+
resultfile="$TMP_DIR/result.$i"
|
|
274
|
+
verdict=$(cat "$resultfile" 2>/dev/null)
|
|
275
|
+
case "$verdict" in
|
|
276
|
+
ok) ;;
|
|
277
|
+
fail) failed=1 ;;
|
|
278
|
+
inconclusive) inconclusive=$((inconclusive + 1)) ;;
|
|
279
|
+
*)
|
|
280
|
+
lock_dir=$(dirname "$lock")
|
|
281
|
+
echo "::error::js-dependency-audit: no verdict recorded for ${lock_dir} (expected ok/fail/inconclusive in ${resultfile}). Treating as inconclusive."
|
|
282
|
+
inconclusive=$((inconclusive + 1))
|
|
283
|
+
;;
|
|
284
|
+
esac
|
|
285
|
+
done
|
|
286
|
+
|
|
220
287
|
if [ "$failed" -ne 0 ]; then
|
|
221
288
|
exit 1
|
|
222
289
|
fi
|
|
@@ -424,8 +424,16 @@ export function deliberateClosingReferences(text, { code = true } = {}) {
|
|
|
424
424
|
* carried only `Refs #133`. The issue closed on merge anyway. The lesson
|
|
425
425
|
* recorded then: *keeping a denial out of the commit is not sufficient —
|
|
426
426
|
* GitHub's linker reads the PR description text on its own.*
|
|
427
|
-
* - #1238 / #1021 (2026-08-03) —
|
|
428
|
-
*
|
|
427
|
+
* - biffo-template#1238 / biffo-template#1021 (2026-08-03) — this repo's own
|
|
428
|
+
* issues, not an external one: `- **Does not close #1021.**` in the body,
|
|
429
|
+
* `Refs #1021` in the commit, #1021 closed by the squash-merge. (#1872
|
|
430
|
+
* re-checked this entry against the wrong repo — tabsii-platform, where no
|
|
431
|
+
* such pair exists at that date — and, not finding it there, filed a
|
|
432
|
+
* doubt. Re-verified directly against biffo-template's own #1021/#1238:
|
|
433
|
+
* the pairing, the quoted lines and the date all check out exactly as
|
|
434
|
+
* recorded. The two bare `#N`s were ambiguous next to the other entries'
|
|
435
|
+
* explicit `tabsii-platform#76`/`tabsii-crm#133` qualifiers, which is the
|
|
436
|
+
* actual defect #1872 found — now spelled out with the repo name.)
|
|
429
437
|
*
|
|
430
438
|
* The recorded fix each time was a *practice*: "never write a closing keyword
|
|
431
439
|
* in prose". Three occurrences produced a rule and no mechanism, and the
|
|
@@ -661,9 +669,10 @@ function formatNegatedFailure({ negated }) {
|
|
|
661
669
|
...negated.map((n) => ` ${n.source}, line ${n.lineNumber}: ${n.line}`),
|
|
662
670
|
'',
|
|
663
671
|
'This has now happened four times (tabsii-platform#76, tabsii-crm#133,',
|
|
664
|
-
'#1021 via #1238 — see #1245). Keeping the
|
|
665
|
-
'message is not enough: GitHub reads the PR
|
|
666
|
-
'(#1334) a commit message on its own,
|
|
672
|
+
'biffo-template#1021 via biffo-template#1238 — see #1245). Keeping the',
|
|
673
|
+
'denial out of the commit message is not enough: GitHub reads the PR',
|
|
674
|
+
'description on its own — and (#1334) a commit message on its own,',
|
|
675
|
+
'independent of the body.',
|
|
667
676
|
'',
|
|
668
677
|
'Rewrite the line so no closing keyword sits in front of the reference:',
|
|
669
678
|
...refs.map((r) => ` - \`Refs ${r}\`, or "leaves ${r} open"`),
|
|
@@ -80,12 +80,29 @@ attempts=3
|
|
|
80
80
|
inconclusive=0
|
|
81
81
|
failed=0
|
|
82
82
|
|
|
83
|
-
# Audit one directory. Returns 0 if clean or inconclusive, 1 on a real finding
|
|
84
|
-
#
|
|
83
|
+
# Audit one directory. Returns 0 if clean or inconclusive, 1 on a real finding,
|
|
84
|
+
# AND writes a one-word verdict (`ok` / `fail` / `inconclusive`) to `$4`.
|
|
85
|
+
#
|
|
86
|
+
# The verdict file exists because this function is invoked backgrounded (`&`,
|
|
87
|
+
# see the audit loop below) so the four (or however many) trees run in
|
|
88
|
+
# parallel rather than paying their registry round-trip one after another
|
|
89
|
+
# (#1874: 4 trees serially cost 15m21s in a real CI run and blew the job's
|
|
90
|
+
# 20-minute cap). A backgrounded call is a forked subshell — every variable it
|
|
91
|
+
# touches, including `inconclusive`/`failed` below, is a copy in that child
|
|
92
|
+
# process and vanishes when it exits. The return code has the same problem: a
|
|
93
|
+
# background job's exit status is only visible via `wait "$pid"`, one pid at a
|
|
94
|
+
# time, which is no simpler than a file and less robust (a killed/never-run
|
|
95
|
+
# job leaves nothing to wait on). So each invocation reports for itself, in
|
|
96
|
+
# writing, and the parent tallies the results after `wait` once every
|
|
97
|
+
# invocation has finished.
|
|
98
|
+
#
|
|
99
|
+
# `$1` is the directory, `$2` a human label, `$3` extra pnpm flags, `$4` the
|
|
100
|
+
# result file this invocation must write its verdict to.
|
|
85
101
|
audit_dir() {
|
|
86
102
|
dir="$1"
|
|
87
103
|
label="$2"
|
|
88
104
|
extra="$3"
|
|
105
|
+
resultfile="$4"
|
|
89
106
|
|
|
90
107
|
for attempt in $(seq 1 "$attempts"); do
|
|
91
108
|
# printf, never echo: the CI step runs `sh scripts/...` i.e. dash, whose
|
|
@@ -122,12 +139,14 @@ audit_dir() {
|
|
|
122
139
|
if [ "$((high + crit))" -gt 0 ]; then
|
|
123
140
|
echo "::error::${label}: ${crit} critical + ${high} high advisory(ies) across ${total} package(s); registry answered ${seen_at}."
|
|
124
141
|
printf '%s' "$out" | jq '.advisories // .metadata.vulnerabilities' 2>/dev/null | head -c 4000
|
|
142
|
+
echo "fail" >"$resultfile"
|
|
125
143
|
return 1
|
|
126
144
|
fi
|
|
127
145
|
# A bare "no advisories" is not falsifiable. State the population, the
|
|
128
146
|
# severities that did NOT block, and when the registry was asked, so a
|
|
129
147
|
# reader can tell a clean tree from a tree nobody looked at properly.
|
|
130
148
|
echo "${label}: 0 critical, 0 high across ${total} package(s) (${mod} moderate, ${low} low — reported, not blocking); registry answered ${seen_at}."
|
|
149
|
+
echo "ok" >"$resultfile"
|
|
131
150
|
return 0
|
|
132
151
|
fi
|
|
133
152
|
|
|
@@ -137,7 +156,7 @@ audit_dir() {
|
|
|
137
156
|
done
|
|
138
157
|
|
|
139
158
|
echo "::error::${label}: audit could not run after ${attempts} attempts (the registry returned a non-JSON/error response). Advisory scanning was NOT performed for this tree, so this is INCONCLUSIVE and BLOCKS — a gate that cannot see its input must not report clean (#1269, #591)."
|
|
140
|
-
inconclusive
|
|
159
|
+
echo "inconclusive" >"$resultfile"
|
|
141
160
|
return 0
|
|
142
161
|
}
|
|
143
162
|
|
|
@@ -197,11 +216,31 @@ for lock in $ALL_LOCKS; do
|
|
|
197
216
|
fi
|
|
198
217
|
done
|
|
199
218
|
|
|
200
|
-
# Audit each discovered tree.
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
219
|
+
# Audit each discovered tree IN PARALLEL (#1874). Each `audit_dir` call is a
|
|
220
|
+
# real network round-trip, with its own retry/backoff, to registry.npmjs.org
|
|
221
|
+
# — run one after another they cost roughly N x the slowest single tree (a
|
|
222
|
+
# real CI run measured 4 trees / 15m21s, blowing the job's 20-minute cap
|
|
223
|
+
# mid-way through ~9 other required guard steps). Backgrounding them lets the
|
|
224
|
+
# round-trips overlap instead.
|
|
225
|
+
#
|
|
226
|
+
# A one-word-per-tree result file (see `audit_dir` above) is how the parent
|
|
227
|
+
# shell learns each backgrounded verdict back: `$TMP_DIR` is created via
|
|
228
|
+
# `mktemp -d` (collision-safe by construction — no need to hand-roll a `$$`
|
|
229
|
+
# suffix on top of it) and torn down by the EXIT/HUP/INT/TERM trap below
|
|
230
|
+
# whichever way this script leaves.
|
|
231
|
+
#
|
|
232
|
+
# The workspace's own lockfile is audited WITHOUT --ignore-workspace, so pnpm
|
|
233
|
+
# resolves it normally; every other discovered lockfile is a separate,
|
|
234
|
+
# vendored project and needs the flag, or pnpm walks up, finds the workspace,
|
|
235
|
+
# and silently audits THAT instead — reporting clean for a tree it never
|
|
236
|
+
# looked at, which is this exact defect one level down.
|
|
237
|
+
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/js-dependency-audit.XXXXXX") || {
|
|
238
|
+
echo "::error::js-dependency-audit: could not create a temp directory for parallel results (mktemp failed)." >&2
|
|
239
|
+
exit 2
|
|
240
|
+
}
|
|
241
|
+
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
|
|
242
|
+
|
|
243
|
+
i=0
|
|
205
244
|
for lock in $ALL_LOCKS; do
|
|
206
245
|
dir=$(dirname "$lock")
|
|
207
246
|
dir_abs=$(cd "$dir" 2>/dev/null && pwd -P)
|
|
@@ -210,13 +249,41 @@ for lock in $ALL_LOCKS; do
|
|
|
210
249
|
"$REPO_ROOT"/*) rel=${dir_abs#"$REPO_ROOT"/} ;;
|
|
211
250
|
*) rel="$dir_abs" ;;
|
|
212
251
|
esac
|
|
252
|
+
i=$((i + 1))
|
|
253
|
+
resultfile="$TMP_DIR/result.$i"
|
|
213
254
|
if [ "$dir_abs" = "$WORKSPACE_ABS" ]; then
|
|
214
|
-
audit_dir "$dir" "pnpm audit (workspace: ${rel})" ""
|
|
255
|
+
audit_dir "$dir" "pnpm audit (workspace: ${rel})" "" "$resultfile" &
|
|
215
256
|
else
|
|
216
|
-
audit_dir "$dir" "pnpm audit (${rel})" "--ignore-workspace"
|
|
257
|
+
audit_dir "$dir" "pnpm audit (${rel})" "--ignore-workspace" "$resultfile" &
|
|
217
258
|
fi
|
|
218
259
|
done
|
|
219
260
|
|
|
261
|
+
# Wait for every backgrounded audit_dir before reading any result file back —
|
|
262
|
+
# reading early would race a tree that is still auditing.
|
|
263
|
+
wait
|
|
264
|
+
|
|
265
|
+
# Tally the verdicts the backgrounded invocations wrote for themselves. A
|
|
266
|
+
# result file that is missing or unreadable (the subshell was killed before
|
|
267
|
+
# it could write, or never started) fails CLOSED as inconclusive rather than
|
|
268
|
+
# being silently skipped — the same posture as the empty-discovery check
|
|
269
|
+
# above: a tree this run cannot account for is not a clean tree.
|
|
270
|
+
i=0
|
|
271
|
+
for lock in $ALL_LOCKS; do
|
|
272
|
+
i=$((i + 1))
|
|
273
|
+
resultfile="$TMP_DIR/result.$i"
|
|
274
|
+
verdict=$(cat "$resultfile" 2>/dev/null)
|
|
275
|
+
case "$verdict" in
|
|
276
|
+
ok) ;;
|
|
277
|
+
fail) failed=1 ;;
|
|
278
|
+
inconclusive) inconclusive=$((inconclusive + 1)) ;;
|
|
279
|
+
*)
|
|
280
|
+
lock_dir=$(dirname "$lock")
|
|
281
|
+
echo "::error::js-dependency-audit: no verdict recorded for ${lock_dir} (expected ok/fail/inconclusive in ${resultfile}). Treating as inconclusive."
|
|
282
|
+
inconclusive=$((inconclusive + 1))
|
|
283
|
+
;;
|
|
284
|
+
esac
|
|
285
|
+
done
|
|
286
|
+
|
|
220
287
|
if [ "$failed" -ne 0 ]; then
|
|
221
288
|
exit 1
|
|
222
289
|
fi
|