@biffo/cli 0.177.1 → 0.177.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.
@@ -148,4 +148,14 @@ jobs:
148
148
  with:
149
149
  enable-cache: true
150
150
  - run: uv sync --all-groups
151
- - run: uv run pip-audit
151
+ # Fails on a real advisory, but treats an unreachable PyPI/OSV as
152
+ # INCONCLUSIVE rather than reddening a required check for an
153
+ # infrastructure hiccup (#591, #743). The raw `uv run pip-audit` this
154
+ # replaces exits non-zero identically whether it found a vulnerability or
155
+ # simply could not reach the advisory database.
156
+ #
157
+ # The script is byte-identical to biffo-template's copy and to every other
158
+ # satellite's, distributed by that repo's shared-sync.sh. Do not edit it
159
+ # here; edit it upstream.
160
+ - name: Dependency audit
161
+ run: sh scripts/py-dependency-audit.sh
@@ -0,0 +1,133 @@
1
+ #!/bin/sh
2
+ #
3
+ # JS dependency audit that fails the build on a real high/critical advisory but
4
+ # NOT on a broken audit registry (#591).
5
+ #
6
+ # `pnpm audit --audit-level=high` exits non-zero identically whether it found a
7
+ # vulnerability OR simply couldn't parse the registry's response — and the npm
8
+ # audit endpoint intermittently returns a non-JSON/gzip body pnpm chokes on
9
+ # ("Unexpected token, is not valid JSON"), which then fails the required JS check
10
+ # on every open PR at once, blocking the whole merge queue for an infrastructure
11
+ # hiccup that has nothing to do with the PR.
12
+ #
13
+ # `pnpm audit --json` disambiguates: a real run yields `.metadata.vulnerabilities`
14
+ # (counts by severity); a registry/parse failure yields `.error`. So we fail only
15
+ # on a genuine high+critical finding, retry a transient registry error a few
16
+ # times, and treat a persistent one as INCONCLUSIVE (warn, don't block) rather
17
+ # than as a vulnerability. "Couldn't run the check" and "the check found a
18
+ # problem" must not be the same signal on a required gate.
19
+ #
20
+ # ## Skeleton lockfiles are audited too (#644)
21
+ #
22
+ # `_skeletons/**/` are scaffolding trees: `biffo sibling create` / `plugin
23
+ # create` copy them verbatim into a brand-new repo. They sit OUTSIDE the pnpm
24
+ # workspace, so `pnpm install` never installs them and a workspace-scoped audit
25
+ # never sees them — which meant every sibling scaffolded from this template was
26
+ # born with four high-severity advisories nobody could see. Dependabot found
27
+ # them; the required JS gate could not, by construction.
28
+ #
29
+ # That is the fail-open shape this file already exists to fight, one level up:
30
+ # the gate was green because it was not looking, and "we checked and it is
31
+ # clean" read identically to "we never checked".
32
+ #
33
+ # ## This file is distributed verbatim (#743)
34
+ #
35
+ # Both skeletons and every existing sibling/plugin repo hold a BYTE-IDENTICAL
36
+ # copy, listed in `shared-files.json` and pushed by `scripts/shared-sync.sh`.
37
+ # Until then the hardening above lived in this repo only, while both skeletons
38
+ # shipped the raw `pnpm audit --audit-level=high` that #591 was filed about — so
39
+ # every satellite was born with the original defect and reddened its required JS
40
+ # check on any registry hiccup.
41
+ #
42
+ # It therefore takes its target from the CURRENT WORKING DIRECTORY — the CI
43
+ # job's `working-directory` — rather than assuming a repo layout: `.` is the
44
+ # root workspace here, `apps/frontend` in a sibling, and the `_skeletons/**`
45
+ # sweep below simply finds nothing outside this repo. Keep it layout-agnostic,
46
+ # or the copies stop being interchangeable and `shared-sync.sh --check` starts
47
+ # reporting drift that is really divergence.
48
+ #
49
+ # POSIX sh (the CI step runs `sh scripts/...`, i.e. dash) — no `pipefail`.
50
+ set -u
51
+
52
+ # jq is the parser this whole distinction rests on. Without it EVERY response
53
+ # reads as unparseable, so the script reports INCONCLUSIVE on every invocation
54
+ # and the gate goes green while scanning nothing — the exact fail-open shape it
55
+ # exists to prevent, and precisely what the dash `echo` defect did until #717. A
56
+ # missing jq is a deterministic environment defect, not a transient registry
57
+ # hiccup, so it fails loudly instead of degrading into a permanent pass.
58
+ if ! command -v jq >/dev/null 2>&1; then
59
+ echo "::error::jq is not installed on this runner. The dependency audit cannot tell a clean run from an unparseable one without it, and would report INCONCLUSIVE forever — a gate that is green because it never ran. Install jq."
60
+ exit 1
61
+ fi
62
+
63
+ attempts=3
64
+ inconclusive=0
65
+ failed=0
66
+
67
+ # Audit one directory. Returns 0 if clean or inconclusive, 1 on a real finding.
68
+ # `$1` is the directory, `$2` a human label, `$3` extra pnpm flags.
69
+ audit_dir() {
70
+ dir="$1"
71
+ label="$2"
72
+ extra="$3"
73
+
74
+ for attempt in $(seq 1 "$attempts"); do
75
+ # printf, never echo: the CI step runs `sh scripts/...` i.e. dash, whose
76
+ # `echo` interprets backslash escapes. Advisory payloads contain them, so
77
+ # `echo "$out" | jq` mangles the JSON and jq rejects it — the run then reads
78
+ # as "the registry returned junk" and reports INCONCLUSIVE. That is this
79
+ # gate failing open inside the very fix that exists to stop it failing open
80
+ # (#591): green, every time, while scanning nothing.
81
+ #
82
+ # The flag differs by tree, and both directions matter:
83
+ # - skeletons need --ignore-workspace, or pnpm walks up, finds this repo's
84
+ # workspace root and audits THAT instead — reporting a clean result for
85
+ # a tree it never looked at, which is the failure this step closes.
86
+ # - the workspace must NOT have it, or pnpm has no project to audit and
87
+ # every run reports INCONCLUSIVE. That fails open: the gate goes green
88
+ # forever while scanning nothing. (Caught by running this script before
89
+ # trusting it — the workspace audit had silently stopped working.)
90
+ # shellcheck disable=SC2086
91
+ out="$(cd "$dir" 2>/dev/null && pnpm audit --json $extra 2>/dev/null)"
92
+
93
+ if printf '%s' "$out" | jq -e '.metadata.vulnerabilities' >/dev/null 2>&1; then
94
+ high="$(printf '%s' "$out" | jq '.metadata.vulnerabilities.high // 0')"
95
+ crit="$(printf '%s' "$out" | jq '.metadata.vulnerabilities.critical // 0')"
96
+ if [ "$((high + crit))" -gt 0 ]; then
97
+ echo "::error::${label}: ${crit} critical + ${high} high advisory(ies)."
98
+ printf '%s' "$out" | jq '.advisories // .metadata.vulnerabilities' 2>/dev/null | head -c 4000
99
+ return 1
100
+ fi
101
+ echo "${label}: no high/critical advisories."
102
+ return 0
103
+ fi
104
+
105
+ msg="$(printf '%s' "$out" | jq -r '.error.message // "no parseable audit output"' 2>/dev/null | head -c 200)"
106
+ echo "${label}: attempt ${attempt}/${attempts} could not run: ${msg}"
107
+ [ "$attempt" -lt "$attempts" ] && sleep "$((attempt * 3))"
108
+ done
109
+
110
+ echo "::warning::${label}: audit could not run after ${attempts} attempts (the registry returned a non-JSON/error response); treating as INCONCLUSIVE and not blocking. Advisory scanning was NOT performed for this tree — see #591."
111
+ inconclusive=$((inconclusive + 1))
112
+ return 0
113
+ }
114
+
115
+ # The workspace itself.
116
+ audit_dir "." "pnpm audit (workspace)" "" || failed=1
117
+
118
+ # Every scaffolding tree with its own lockfile. Discovered rather than listed,
119
+ # so a new skeleton is covered the day it lands instead of the day someone
120
+ # remembers to add it here.
121
+ for lock in $(find _skeletons -name pnpm-lock.yaml -not -path '*/node_modules/*' 2>/dev/null | sort); do
122
+ audit_dir "$(dirname "$lock")" "pnpm audit ($(dirname "$lock"))" "--ignore-workspace" || failed=1
123
+ done
124
+
125
+ if [ "$failed" -ne 0 ]; then
126
+ exit 1
127
+ fi
128
+
129
+ if [ "$inconclusive" -ne 0 ]; then
130
+ echo "${inconclusive} tree(s) could not be audited; see warnings above."
131
+ fi
132
+
133
+ exit 0
@@ -0,0 +1,145 @@
1
+ #!/bin/sh
2
+ #
3
+ # Python dependency audit that fails the build on a real advisory but NOT on a
4
+ # broken registry/network response (#591) — same class of flake as the JS
5
+ # dependency audit (scripts/js-dependency-audit.sh), applied preventively here
6
+ # since pip-audit queries PyPI/OSV over the network per dependency and is
7
+ # subject to the same kind of transient hiccup.
8
+ #
9
+ # `pip-audit -f json` emits a `{"dependencies": [{"name", "version", "vulns"},
10
+ # ...]}` shape on a real run; a network/parse failure produces no parseable
11
+ # JSON at all (traceback on stderr, non-zero exit, empty/partial stdout). So we
12
+ # fail only when the parsed output actually contains a vulnerability, retry a
13
+ # few times on an unparseable run, and treat a persistent failure as
14
+ # INCONCLUSIVE (warn, don't block) rather than as a vulnerability.
15
+ # "Couldn't run the check" and "the check found a problem" must not be the same
16
+ # signal on a required gate.
17
+ #
18
+ # ## Skeleton lockfiles are audited too (#719)
19
+ #
20
+ # `_skeletons/**/` are scaffolding trees: `biffo sibling create` / `plugin
21
+ # create` copy them verbatim into a brand-new repo. They are NOT part of this
22
+ # repo's uv project, so `uv sync` never installs them and a workspace-scoped
23
+ # `uv run pip-audit` never saw them — which meant the Python half of every
24
+ # scaffolded sibling came from a lockfile no CI gate had ever looked at.
25
+ #
26
+ # #644 closed exactly this hole for the JS skeleton lockfile, where it had
27
+ # already cost four unseen high-severity advisories; this is the same hole one
28
+ # language over. It is the fail-open shape this file already exists to fight,
29
+ # one level up: the gate was green because it was not looking, and "we checked
30
+ # and it is clean" read identically to "we never checked".
31
+ #
32
+ # Unlike `pnpm audit --ignore-workspace`, pip-audit cannot simply be run inside
33
+ # a skeleton directory: the skeleton is not an installed environment, and
34
+ # pip-audit's default mode audits an environment. So we export `--frozen`
35
+ # requirements from the skeleton's own lockfile (a read of the committed lock,
36
+ # no re-resolution) and audit those with `-r`, from the workspace, which is
37
+ # where pip-audit is actually installed.
38
+ #
39
+ # ## This file is distributed verbatim (#743)
40
+ #
41
+ # Both skeletons and every existing sibling/plugin repo hold a BYTE-IDENTICAL
42
+ # copy, listed in `shared-files.json` and pushed by `scripts/shared-sync.sh`.
43
+ # Until then the hardening above lived in this repo only, while both skeletons
44
+ # shipped a raw `uv run pip-audit` — so every satellite was born with the
45
+ # original defect and reddened a required check on any PyPI/OSV hiccup.
46
+ #
47
+ # It therefore takes its target from the CURRENT WORKING DIRECTORY — the CI
48
+ # job's `working-directory` — rather than assuming a repo layout: `.` is the
49
+ # root project here and in a plugin repo, `services/api` in a sibling, and the
50
+ # `_skeletons/**` sweep below simply finds nothing outside this repo. Keep it
51
+ # layout-agnostic, or the copies stop being interchangeable and
52
+ # `shared-sync.sh --check` starts reporting drift that is really divergence.
53
+ #
54
+ # POSIX sh (the CI step runs `sh scripts/...`, i.e. dash) — no `pipefail`.
55
+ set -u
56
+
57
+ # jq is the parser this whole distinction rests on. Without it EVERY response
58
+ # reads as unparseable, so the script reports INCONCLUSIVE on every invocation
59
+ # and the gate goes green while scanning nothing — the exact fail-open shape it
60
+ # exists to prevent, and precisely what the dash `echo` defect did until #717. A
61
+ # missing jq is a deterministic environment defect, not a transient network
62
+ # hiccup, so it fails loudly instead of degrading into a permanent pass.
63
+ if ! command -v jq >/dev/null 2>&1; then
64
+ echo "::error::jq is not installed on this runner. The dependency audit cannot tell a clean run from an unparseable one without it, and would report INCONCLUSIVE forever — a gate that is green because it never ran. Install jq."
65
+ exit 1
66
+ fi
67
+
68
+ attempts=3
69
+ inconclusive=0
70
+ failed=0
71
+
72
+ # Audit one dependency set. Returns 0 if clean or inconclusive, 1 on a real
73
+ # finding. `$1` is a human label, `$2` the extra pip-audit flags that select
74
+ # what to audit — empty for this workspace's installed environment, or
75
+ # `-r <file>` for a requirements file exported from a skeleton lockfile.
76
+ audit_deps() {
77
+ label="$1"
78
+ extra="$2"
79
+
80
+ for attempt in $(seq 1 "$attempts"); do
81
+ # shellcheck disable=SC2086
82
+ out="$(uv run pip-audit -f json $extra 2>/dev/null)"
83
+
84
+ # printf, never echo: the CI step runs `sh scripts/...` i.e. dash, whose
85
+ # `echo` interprets backslash escapes. Advisory payloads contain them, so
86
+ # `echo "$out" | jq` mangles the JSON and jq rejects it — the run then reads
87
+ # as "the registry returned junk" and reports INCONCLUSIVE. That is this
88
+ # gate failing open inside the very fix that exists to stop it failing open
89
+ # (#591): green, every time, while scanning nothing. #644 found the JS gate
90
+ # had been doing exactly that on every run.
91
+ if printf '%s' "$out" | jq -e '.dependencies' >/dev/null 2>&1; then
92
+ vuln_count="$(printf '%s' "$out" | jq '[.dependencies[].vulns[]?] | length')"
93
+ if [ "$vuln_count" -gt 0 ]; then
94
+ echo "::error::${label}: ${vuln_count} vulnerability(ies)."
95
+ printf '%s' "$out" | jq '[.dependencies[] | select(.vulns | length > 0)]' 2>/dev/null | head -c 4000
96
+ return 1
97
+ fi
98
+ echo "${label}: no vulnerabilities found."
99
+ return 0
100
+ fi
101
+
102
+ echo "${label}: attempt ${attempt}/${attempts} produced no parseable JSON output."
103
+ [ "$attempt" -lt "$attempts" ] && sleep "$((attempt * 3))"
104
+ done
105
+
106
+ echo "::warning::${label}: could not produce parseable output after ${attempts} attempts (a transient network/registry error); treating as INCONCLUSIVE and not blocking. Advisory scanning was NOT performed for this tree — see #591."
107
+ inconclusive=$((inconclusive + 1))
108
+ return 0
109
+ }
110
+
111
+ # The workspace itself.
112
+ audit_deps "pip-audit (workspace)" "" || failed=1
113
+
114
+ # Every scaffolding tree with its own lockfile. Discovered rather than listed,
115
+ # so a new skeleton is covered the day it lands instead of the day someone
116
+ # remembers to add it here.
117
+ for lock in $(find _skeletons -name uv.lock -not -path '*/node_modules/*' 2>/dev/null | sort); do
118
+ dir="$(dirname "$lock")"
119
+ reqs="$(mktemp)"
120
+
121
+ # `--frozen` reads the committed lock verbatim instead of re-resolving, so
122
+ # what gets audited is exactly what a scaffolded sibling would be born with.
123
+ # `--no-emit-project` drops the skeleton package itself (nothing published to
124
+ # audit); `--no-dev` keeps the audit to what actually ships.
125
+ if (cd "$dir" && uv export --frozen --no-dev --no-emit-project) >"$reqs" 2>/dev/null && [ -s "$reqs" ]; then
126
+ audit_deps "pip-audit (${dir})" "-r $reqs" || failed=1
127
+ else
128
+ # An export failure is "couldn't run", not "clean" — same discipline as a
129
+ # transient pip-audit error, and it must never read as a pass.
130
+ echo "::warning::pip-audit (${dir}): could not export requirements from ${lock}; treating as INCONCLUSIVE and not blocking. Advisory scanning was NOT performed for this tree — see #719."
131
+ inconclusive=$((inconclusive + 1))
132
+ fi
133
+
134
+ rm -f "$reqs"
135
+ done
136
+
137
+ if [ "$failed" -ne 0 ]; then
138
+ exit 1
139
+ fi
140
+
141
+ if [ "$inconclusive" -ne 0 ]; then
142
+ echo "${inconclusive} tree(s) could not be audited; see warnings above."
143
+ fi
144
+
145
+ exit 0
@@ -82,7 +82,19 @@ jobs:
82
82
  run: pnpm run build
83
83
  - name: Dependency audit
84
84
  if: ${{ !cancelled() }}
85
- run: pnpm audit --audit-level=high
85
+ # Fails on a real high/critical advisory, but treats a broken audit
86
+ # registry as INCONCLUSIVE rather than reddening a required check for an
87
+ # infrastructure hiccup (#591, #743). The raw `pnpm audit
88
+ # --audit-level=high` this replaces exits non-zero identically whether it
89
+ # found a vulnerability or simply could not parse the registry's
90
+ # response, so any npm blip failed this job on every open PR at once.
91
+ #
92
+ # The relative path is deliberate. This job's working-directory is
93
+ # apps/frontend, which is exactly the tree to audit, and the script
94
+ # audits its own cwd — it is byte-identical to biffo-template's copy and
95
+ # to every other satellite's, distributed by that repo's shared-sync.sh,
96
+ # so it cannot assume a layout. Do not edit it here; edit it upstream.
97
+ run: sh ../../scripts/js-dependency-audit.sh
86
98
 
87
99
  python:
88
100
  name: Python (lint, types, test, security)
@@ -124,7 +136,11 @@ jobs:
124
136
  path: services/api/bandit-report.json
125
137
  - name: Dependency audit
126
138
  if: ${{ !cancelled() }}
127
- run: uv run pip-audit
139
+ # Same treatment as the JS job's Dependency audit above: fails on a real
140
+ # advisory, INCONCLUSIVE on an unreachable PyPI/OSV (#591, #743). cwd is
141
+ # services/api, which is the environment `uv sync` installed and so the
142
+ # one to audit.
143
+ run: sh ../../scripts/py-dependency-audit.sh
128
144
 
129
145
  security-secrets:
130
146
  name: Secret Scan
@@ -0,0 +1,133 @@
1
+ #!/bin/sh
2
+ #
3
+ # JS dependency audit that fails the build on a real high/critical advisory but
4
+ # NOT on a broken audit registry (#591).
5
+ #
6
+ # `pnpm audit --audit-level=high` exits non-zero identically whether it found a
7
+ # vulnerability OR simply couldn't parse the registry's response — and the npm
8
+ # audit endpoint intermittently returns a non-JSON/gzip body pnpm chokes on
9
+ # ("Unexpected token, is not valid JSON"), which then fails the required JS check
10
+ # on every open PR at once, blocking the whole merge queue for an infrastructure
11
+ # hiccup that has nothing to do with the PR.
12
+ #
13
+ # `pnpm audit --json` disambiguates: a real run yields `.metadata.vulnerabilities`
14
+ # (counts by severity); a registry/parse failure yields `.error`. So we fail only
15
+ # on a genuine high+critical finding, retry a transient registry error a few
16
+ # times, and treat a persistent one as INCONCLUSIVE (warn, don't block) rather
17
+ # than as a vulnerability. "Couldn't run the check" and "the check found a
18
+ # problem" must not be the same signal on a required gate.
19
+ #
20
+ # ## Skeleton lockfiles are audited too (#644)
21
+ #
22
+ # `_skeletons/**/` are scaffolding trees: `biffo sibling create` / `plugin
23
+ # create` copy them verbatim into a brand-new repo. They sit OUTSIDE the pnpm
24
+ # workspace, so `pnpm install` never installs them and a workspace-scoped audit
25
+ # never sees them — which meant every sibling scaffolded from this template was
26
+ # born with four high-severity advisories nobody could see. Dependabot found
27
+ # them; the required JS gate could not, by construction.
28
+ #
29
+ # That is the fail-open shape this file already exists to fight, one level up:
30
+ # the gate was green because it was not looking, and "we checked and it is
31
+ # clean" read identically to "we never checked".
32
+ #
33
+ # ## This file is distributed verbatim (#743)
34
+ #
35
+ # Both skeletons and every existing sibling/plugin repo hold a BYTE-IDENTICAL
36
+ # copy, listed in `shared-files.json` and pushed by `scripts/shared-sync.sh`.
37
+ # Until then the hardening above lived in this repo only, while both skeletons
38
+ # shipped the raw `pnpm audit --audit-level=high` that #591 was filed about — so
39
+ # every satellite was born with the original defect and reddened its required JS
40
+ # check on any registry hiccup.
41
+ #
42
+ # It therefore takes its target from the CURRENT WORKING DIRECTORY — the CI
43
+ # job's `working-directory` — rather than assuming a repo layout: `.` is the
44
+ # root workspace here, `apps/frontend` in a sibling, and the `_skeletons/**`
45
+ # sweep below simply finds nothing outside this repo. Keep it layout-agnostic,
46
+ # or the copies stop being interchangeable and `shared-sync.sh --check` starts
47
+ # reporting drift that is really divergence.
48
+ #
49
+ # POSIX sh (the CI step runs `sh scripts/...`, i.e. dash) — no `pipefail`.
50
+ set -u
51
+
52
+ # jq is the parser this whole distinction rests on. Without it EVERY response
53
+ # reads as unparseable, so the script reports INCONCLUSIVE on every invocation
54
+ # and the gate goes green while scanning nothing — the exact fail-open shape it
55
+ # exists to prevent, and precisely what the dash `echo` defect did until #717. A
56
+ # missing jq is a deterministic environment defect, not a transient registry
57
+ # hiccup, so it fails loudly instead of degrading into a permanent pass.
58
+ if ! command -v jq >/dev/null 2>&1; then
59
+ echo "::error::jq is not installed on this runner. The dependency audit cannot tell a clean run from an unparseable one without it, and would report INCONCLUSIVE forever — a gate that is green because it never ran. Install jq."
60
+ exit 1
61
+ fi
62
+
63
+ attempts=3
64
+ inconclusive=0
65
+ failed=0
66
+
67
+ # Audit one directory. Returns 0 if clean or inconclusive, 1 on a real finding.
68
+ # `$1` is the directory, `$2` a human label, `$3` extra pnpm flags.
69
+ audit_dir() {
70
+ dir="$1"
71
+ label="$2"
72
+ extra="$3"
73
+
74
+ for attempt in $(seq 1 "$attempts"); do
75
+ # printf, never echo: the CI step runs `sh scripts/...` i.e. dash, whose
76
+ # `echo` interprets backslash escapes. Advisory payloads contain them, so
77
+ # `echo "$out" | jq` mangles the JSON and jq rejects it — the run then reads
78
+ # as "the registry returned junk" and reports INCONCLUSIVE. That is this
79
+ # gate failing open inside the very fix that exists to stop it failing open
80
+ # (#591): green, every time, while scanning nothing.
81
+ #
82
+ # The flag differs by tree, and both directions matter:
83
+ # - skeletons need --ignore-workspace, or pnpm walks up, finds this repo's
84
+ # workspace root and audits THAT instead — reporting a clean result for
85
+ # a tree it never looked at, which is the failure this step closes.
86
+ # - the workspace must NOT have it, or pnpm has no project to audit and
87
+ # every run reports INCONCLUSIVE. That fails open: the gate goes green
88
+ # forever while scanning nothing. (Caught by running this script before
89
+ # trusting it — the workspace audit had silently stopped working.)
90
+ # shellcheck disable=SC2086
91
+ out="$(cd "$dir" 2>/dev/null && pnpm audit --json $extra 2>/dev/null)"
92
+
93
+ if printf '%s' "$out" | jq -e '.metadata.vulnerabilities' >/dev/null 2>&1; then
94
+ high="$(printf '%s' "$out" | jq '.metadata.vulnerabilities.high // 0')"
95
+ crit="$(printf '%s' "$out" | jq '.metadata.vulnerabilities.critical // 0')"
96
+ if [ "$((high + crit))" -gt 0 ]; then
97
+ echo "::error::${label}: ${crit} critical + ${high} high advisory(ies)."
98
+ printf '%s' "$out" | jq '.advisories // .metadata.vulnerabilities' 2>/dev/null | head -c 4000
99
+ return 1
100
+ fi
101
+ echo "${label}: no high/critical advisories."
102
+ return 0
103
+ fi
104
+
105
+ msg="$(printf '%s' "$out" | jq -r '.error.message // "no parseable audit output"' 2>/dev/null | head -c 200)"
106
+ echo "${label}: attempt ${attempt}/${attempts} could not run: ${msg}"
107
+ [ "$attempt" -lt "$attempts" ] && sleep "$((attempt * 3))"
108
+ done
109
+
110
+ echo "::warning::${label}: audit could not run after ${attempts} attempts (the registry returned a non-JSON/error response); treating as INCONCLUSIVE and not blocking. Advisory scanning was NOT performed for this tree — see #591."
111
+ inconclusive=$((inconclusive + 1))
112
+ return 0
113
+ }
114
+
115
+ # The workspace itself.
116
+ audit_dir "." "pnpm audit (workspace)" "" || failed=1
117
+
118
+ # Every scaffolding tree with its own lockfile. Discovered rather than listed,
119
+ # so a new skeleton is covered the day it lands instead of the day someone
120
+ # remembers to add it here.
121
+ for lock in $(find _skeletons -name pnpm-lock.yaml -not -path '*/node_modules/*' 2>/dev/null | sort); do
122
+ audit_dir "$(dirname "$lock")" "pnpm audit ($(dirname "$lock"))" "--ignore-workspace" || failed=1
123
+ done
124
+
125
+ if [ "$failed" -ne 0 ]; then
126
+ exit 1
127
+ fi
128
+
129
+ if [ "$inconclusive" -ne 0 ]; then
130
+ echo "${inconclusive} tree(s) could not be audited; see warnings above."
131
+ fi
132
+
133
+ exit 0
@@ -0,0 +1,145 @@
1
+ #!/bin/sh
2
+ #
3
+ # Python dependency audit that fails the build on a real advisory but NOT on a
4
+ # broken registry/network response (#591) — same class of flake as the JS
5
+ # dependency audit (scripts/js-dependency-audit.sh), applied preventively here
6
+ # since pip-audit queries PyPI/OSV over the network per dependency and is
7
+ # subject to the same kind of transient hiccup.
8
+ #
9
+ # `pip-audit -f json` emits a `{"dependencies": [{"name", "version", "vulns"},
10
+ # ...]}` shape on a real run; a network/parse failure produces no parseable
11
+ # JSON at all (traceback on stderr, non-zero exit, empty/partial stdout). So we
12
+ # fail only when the parsed output actually contains a vulnerability, retry a
13
+ # few times on an unparseable run, and treat a persistent failure as
14
+ # INCONCLUSIVE (warn, don't block) rather than as a vulnerability.
15
+ # "Couldn't run the check" and "the check found a problem" must not be the same
16
+ # signal on a required gate.
17
+ #
18
+ # ## Skeleton lockfiles are audited too (#719)
19
+ #
20
+ # `_skeletons/**/` are scaffolding trees: `biffo sibling create` / `plugin
21
+ # create` copy them verbatim into a brand-new repo. They are NOT part of this
22
+ # repo's uv project, so `uv sync` never installs them and a workspace-scoped
23
+ # `uv run pip-audit` never saw them — which meant the Python half of every
24
+ # scaffolded sibling came from a lockfile no CI gate had ever looked at.
25
+ #
26
+ # #644 closed exactly this hole for the JS skeleton lockfile, where it had
27
+ # already cost four unseen high-severity advisories; this is the same hole one
28
+ # language over. It is the fail-open shape this file already exists to fight,
29
+ # one level up: the gate was green because it was not looking, and "we checked
30
+ # and it is clean" read identically to "we never checked".
31
+ #
32
+ # Unlike `pnpm audit --ignore-workspace`, pip-audit cannot simply be run inside
33
+ # a skeleton directory: the skeleton is not an installed environment, and
34
+ # pip-audit's default mode audits an environment. So we export `--frozen`
35
+ # requirements from the skeleton's own lockfile (a read of the committed lock,
36
+ # no re-resolution) and audit those with `-r`, from the workspace, which is
37
+ # where pip-audit is actually installed.
38
+ #
39
+ # ## This file is distributed verbatim (#743)
40
+ #
41
+ # Both skeletons and every existing sibling/plugin repo hold a BYTE-IDENTICAL
42
+ # copy, listed in `shared-files.json` and pushed by `scripts/shared-sync.sh`.
43
+ # Until then the hardening above lived in this repo only, while both skeletons
44
+ # shipped a raw `uv run pip-audit` — so every satellite was born with the
45
+ # original defect and reddened a required check on any PyPI/OSV hiccup.
46
+ #
47
+ # It therefore takes its target from the CURRENT WORKING DIRECTORY — the CI
48
+ # job's `working-directory` — rather than assuming a repo layout: `.` is the
49
+ # root project here and in a plugin repo, `services/api` in a sibling, and the
50
+ # `_skeletons/**` sweep below simply finds nothing outside this repo. Keep it
51
+ # layout-agnostic, or the copies stop being interchangeable and
52
+ # `shared-sync.sh --check` starts reporting drift that is really divergence.
53
+ #
54
+ # POSIX sh (the CI step runs `sh scripts/...`, i.e. dash) — no `pipefail`.
55
+ set -u
56
+
57
+ # jq is the parser this whole distinction rests on. Without it EVERY response
58
+ # reads as unparseable, so the script reports INCONCLUSIVE on every invocation
59
+ # and the gate goes green while scanning nothing — the exact fail-open shape it
60
+ # exists to prevent, and precisely what the dash `echo` defect did until #717. A
61
+ # missing jq is a deterministic environment defect, not a transient network
62
+ # hiccup, so it fails loudly instead of degrading into a permanent pass.
63
+ if ! command -v jq >/dev/null 2>&1; then
64
+ echo "::error::jq is not installed on this runner. The dependency audit cannot tell a clean run from an unparseable one without it, and would report INCONCLUSIVE forever — a gate that is green because it never ran. Install jq."
65
+ exit 1
66
+ fi
67
+
68
+ attempts=3
69
+ inconclusive=0
70
+ failed=0
71
+
72
+ # Audit one dependency set. Returns 0 if clean or inconclusive, 1 on a real
73
+ # finding. `$1` is a human label, `$2` the extra pip-audit flags that select
74
+ # what to audit — empty for this workspace's installed environment, or
75
+ # `-r <file>` for a requirements file exported from a skeleton lockfile.
76
+ audit_deps() {
77
+ label="$1"
78
+ extra="$2"
79
+
80
+ for attempt in $(seq 1 "$attempts"); do
81
+ # shellcheck disable=SC2086
82
+ out="$(uv run pip-audit -f json $extra 2>/dev/null)"
83
+
84
+ # printf, never echo: the CI step runs `sh scripts/...` i.e. dash, whose
85
+ # `echo` interprets backslash escapes. Advisory payloads contain them, so
86
+ # `echo "$out" | jq` mangles the JSON and jq rejects it — the run then reads
87
+ # as "the registry returned junk" and reports INCONCLUSIVE. That is this
88
+ # gate failing open inside the very fix that exists to stop it failing open
89
+ # (#591): green, every time, while scanning nothing. #644 found the JS gate
90
+ # had been doing exactly that on every run.
91
+ if printf '%s' "$out" | jq -e '.dependencies' >/dev/null 2>&1; then
92
+ vuln_count="$(printf '%s' "$out" | jq '[.dependencies[].vulns[]?] | length')"
93
+ if [ "$vuln_count" -gt 0 ]; then
94
+ echo "::error::${label}: ${vuln_count} vulnerability(ies)."
95
+ printf '%s' "$out" | jq '[.dependencies[] | select(.vulns | length > 0)]' 2>/dev/null | head -c 4000
96
+ return 1
97
+ fi
98
+ echo "${label}: no vulnerabilities found."
99
+ return 0
100
+ fi
101
+
102
+ echo "${label}: attempt ${attempt}/${attempts} produced no parseable JSON output."
103
+ [ "$attempt" -lt "$attempts" ] && sleep "$((attempt * 3))"
104
+ done
105
+
106
+ echo "::warning::${label}: could not produce parseable output after ${attempts} attempts (a transient network/registry error); treating as INCONCLUSIVE and not blocking. Advisory scanning was NOT performed for this tree — see #591."
107
+ inconclusive=$((inconclusive + 1))
108
+ return 0
109
+ }
110
+
111
+ # The workspace itself.
112
+ audit_deps "pip-audit (workspace)" "" || failed=1
113
+
114
+ # Every scaffolding tree with its own lockfile. Discovered rather than listed,
115
+ # so a new skeleton is covered the day it lands instead of the day someone
116
+ # remembers to add it here.
117
+ for lock in $(find _skeletons -name uv.lock -not -path '*/node_modules/*' 2>/dev/null | sort); do
118
+ dir="$(dirname "$lock")"
119
+ reqs="$(mktemp)"
120
+
121
+ # `--frozen` reads the committed lock verbatim instead of re-resolving, so
122
+ # what gets audited is exactly what a scaffolded sibling would be born with.
123
+ # `--no-emit-project` drops the skeleton package itself (nothing published to
124
+ # audit); `--no-dev` keeps the audit to what actually ships.
125
+ if (cd "$dir" && uv export --frozen --no-dev --no-emit-project) >"$reqs" 2>/dev/null && [ -s "$reqs" ]; then
126
+ audit_deps "pip-audit (${dir})" "-r $reqs" || failed=1
127
+ else
128
+ # An export failure is "couldn't run", not "clean" — same discipline as a
129
+ # transient pip-audit error, and it must never read as a pass.
130
+ echo "::warning::pip-audit (${dir}): could not export requirements from ${lock}; treating as INCONCLUSIVE and not blocking. Advisory scanning was NOT performed for this tree — see #719."
131
+ inconclusive=$((inconclusive + 1))
132
+ fi
133
+
134
+ rm -f "$reqs"
135
+ done
136
+
137
+ if [ "$failed" -ne 0 ]; then
138
+ exit 1
139
+ fi
140
+
141
+ if [ "$inconclusive" -ne 0 ]; then
142
+ echo "${inconclusive} tree(s) could not be audited; see warnings above."
143
+ fi
144
+
145
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.177.1",
3
+ "version": "0.177.2",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",