Skip to content

Commit 651a574

Browse files
guyofeckclaude
andcommitted
test(ci): prove the cooldown rejects a freshly published release
Dima Ryskin asked for a test rather than a code review of #597: "Can we stack another PR on top of that and check if installing a fresh-package is rejected? I used npmjs.com/package/electron-nightly for always 'fresh' versions". Adds .github/workflows/cooldown-check.yml plus check_cooldown.sh, which probes the real bunfig.toml setting in a temp project. Three cases, because an exit code alone does not distinguish a working guardrail from a broken probe: control `bun add lodash` must succeed, so a red result means the cooldown fired rather than the environment being broken. floating `bun add electron-nightly` must not land a version inside the cooldown — either refused, or resolved to an older one. Asserted on the resolved version's publish time from the registry, so silently installing a one-day-old version cannot pass. exact pin `bun add electron-nightly@<newest>` must be refused. This is the bypass that matters: a PR pinning an exact fresh version. The cooldown is read out of bunfig.toml rather than hardcoded, so the test cannot drift from the setting it verifies. Two constraints shaped the workflow: - It must NOT run the embargo gateway. Gatewayed, embargo would reject the fresh release itself and the run would prove nothing about Bun. check_wix_proxy_steps gains a third exemption, and now records why each one exists: the frozenset becomes a dict of path -> reason, printed per exemption on success, so an exemption cannot be added without stating its justification. - No third-party actions. The org now sets github_owned_allowed with an empty patterns_allowed and requires SHA pinning, so oven-sh/setup-bun is not usable here; Bun is installed from a run step and actions/checkout is pinned to 3d3c42e5 (v7.0.1). Verified: bash -n clean; bunfig parsing smoke-tested (604800 -> 7 days); check_wix_proxy_steps reports 12 of 12 gatewayed jobs with all 3 exempt jobs confirmed to abstain; 13 of 13 unit tests pass (1 new, asserting each exemption prints its reason). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 42cb58b commit 651a574

4 files changed

Lines changed: 197 additions & 27 deletions

File tree

.github/scripts/check_cooldown.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env bash
2+
# Prove the supply-chain cooldown in bunfig.toml actually rejects a fresh release.
3+
#
4+
# Requested by Dima Ryskin (Wix secplatform) as the review for #597: "i think the
5+
# best 'review' would be a test. Can we stack another PR on top of that and check
6+
# if installing a fresh-package is rejected? I used
7+
# npmjs.com/package/electron-nightly for always 'fresh' versions".
8+
#
9+
# This MUST run without the Wix embargo gateway. With the gateway in front,
10+
# embargo would refuse the fresh version itself and the run would prove nothing
11+
# about Bun's guardrail — which is what the publish workflows actually rely on.
12+
#
13+
# Three cases, because "it errored" is not the only pass and not the only failure:
14+
# control a long-stable package still installs, so a red result means the
15+
# cooldown fired rather than the probe being broken
16+
# floating `bun add <pkg>` must not land a version inside the cooldown —
17+
# either refused, or silently resolved to an older one
18+
# exact pin `bun add <pkg>@<fresh-version>` must be refused. This is the
19+
# bypass path that matters: a PR pinning an exact fresh version.
20+
#
21+
# Linux/GNU only (runs on ubuntu-latest). Age arithmetic is done in Node to avoid
22+
# date(1) portability problems.
23+
set -uo pipefail
24+
25+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
26+
FRESH_PKG="electron-nightly" # publishes nightly, so its latest is always fresh
27+
CONTROL_PKG="lodash" # unchanged for years; must install
28+
29+
# Read the policy from bunfig.toml rather than hardcoding it, so this test cannot
30+
# drift away from the setting it is supposed to be verifying.
31+
COOLDOWN_SECONDS="$(sed -nE 's/^[[:space:]]*minimumReleaseAge[[:space:]]*=[[:space:]]*([0-9]+).*/\1/p' "$REPO_ROOT/bunfig.toml" | head -1)"
32+
if [ -z "$COOLDOWN_SECONDS" ]; then
33+
echo "FAIL: no minimumReleaseAge found in bunfig.toml — nothing to verify"
34+
exit 1
35+
fi
36+
echo "Cooldown under test: ${COOLDOWN_SECONDS}s ($((COOLDOWN_SECONDS / 86400)) days)"
37+
echo "Bun: $(bun --version)"
38+
echo
39+
40+
WORK="$(mktemp -d)"
41+
trap 'rm -rf "$WORK"' EXIT
42+
cp "$REPO_ROOT/bunfig.toml" "$WORK/bunfig.toml"
43+
cd "$WORK"
44+
printf '{ "name": "cooldown-probe", "private": true, "version": "0.0.0" }\n' >package.json
45+
46+
failures=0
47+
48+
# Age in seconds of a specific published version, per the registry's own metadata.
49+
published_age_seconds() {
50+
curl -sS "https://registry.npmjs.org/$1" | node -e "
51+
let raw = '';
52+
process.stdin.on('data', (d) => (raw += d)).on('end', () => {
53+
const when = JSON.parse(raw).time?.['$2'];
54+
if (!when) { console.error('no publish time for $1@$2'); process.exit(1); }
55+
console.log(Math.floor((Date.now() - Date.parse(when)) / 1000));
56+
});
57+
"
58+
}
59+
60+
echo "── control: bun add $CONTROL_PKG ──────────────────────────────"
61+
if bun add "$CONTROL_PKG" >control.log 2>&1; then
62+
echo "PASS control package installed, so the probe environment works"
63+
else
64+
echo "FAIL control package could not install — the probe is broken, not the cooldown"
65+
sed 's/^/ /' control.log
66+
failures=$((failures + 1))
67+
fi
68+
echo
69+
70+
echo "── floating: bun add $FRESH_PKG ───────────────────────────────"
71+
if bun add "$FRESH_PKG" >floating.log 2>&1; then
72+
resolved="$(node -p "require('$WORK/node_modules/$FRESH_PKG/package.json').version")"
73+
age="$(published_age_seconds "$FRESH_PKG" "$resolved")" || age=""
74+
if [ -z "$age" ]; then
75+
echo "FAIL installed $resolved but could not determine its publish time"
76+
failures=$((failures + 1))
77+
elif [ "$age" -ge "$COOLDOWN_SECONDS" ]; then
78+
echo "PASS resolved $resolved, published ${age}s ago (>= cooldown)"
79+
echo " fresh versions were filtered out of resolution"
80+
else
81+
echo "FAIL installed $resolved, published only ${age}s ago — inside the cooldown"
82+
failures=$((failures + 1))
83+
fi
84+
else
85+
echo "PASS bun refused to install $FRESH_PKG"
86+
sed 's/^/ /' floating.log | tail -5
87+
fi
88+
echo
89+
90+
echo "── exact pin: bun add $FRESH_PKG@<newest> ─────────────────────"
91+
newest="$(curl -sS "https://registry.npmjs.org/$FRESH_PKG" | node -e "
92+
let raw = '';
93+
process.stdin.on('data', (d) => (raw += d)).on('end', () => {
94+
const doc = JSON.parse(raw);
95+
console.log(doc['dist-tags'].nightly ?? doc['dist-tags'].latest);
96+
});
97+
")"
98+
newest_age="$(published_age_seconds "$FRESH_PKG" "$newest")" || newest_age=""
99+
echo "Newest published: $newest (${newest_age:-unknown}s old)"
100+
101+
if [ -n "$newest_age" ] && [ "$newest_age" -ge "$COOLDOWN_SECONDS" ]; then
102+
echo "SKIP newest version is already older than the cooldown; nothing fresh to reject"
103+
echo " (unexpected for $FRESH_PKG — check it is still publishing nightly)"
104+
elif bun add "$FRESH_PKG@$newest" >pinned.log 2>&1; then
105+
echo "FAIL an exact pin bypassed the cooldown and installed $newest"
106+
echo " a PR pinning a fresh version would defeat the guardrail"
107+
failures=$((failures + 1))
108+
else
109+
echo "PASS bun refused the exact fresh pin $newest"
110+
sed 's/^/ /' pinned.log | tail -5
111+
fi
112+
echo
113+
114+
if [ "$failures" -gt 0 ]; then
115+
echo "RESULT: $failures check(s) failed — the cooldown does not hold"
116+
exit 1
117+
fi
118+
echo "RESULT: cooldown holds — fresh releases cannot enter the dependency tree"

.github/scripts/check_wix_proxy_steps.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
44
There is still no per-job opt-out marker by design. A job that genuinely cannot
55
run the proxy changes this script in the same PR, so the exception gets reviewed
6-
in the open — which is exactly how PUBLISH_WORKFLOWS below came to exist.
6+
in the open — which is exactly how GATEWAY_EXEMPT_WORKFLOWS below came to exist.
77
8-
The rule is bidirectional: non-publish jobs must run the proxy, and publish jobs
9-
must not.
8+
The rule is bidirectional: jobs must run the proxy, and jobs in an exempt
9+
workflow must not.
1010
"""
1111

1212
from __future__ import annotations
@@ -34,15 +34,17 @@
3434
# client_max_body_size so nginx's 1 MB default rejects a packument carrying the
3535
# base64 tarball.
3636
#
37-
# Keyed on exact filename, and every exemption is printed on success, so this
38-
# cannot quietly grow. Revisit once the embargo publish bug is fixed: these
39-
# workflows should go back to being gatewayed like everything else.
40-
PUBLISH_WORKFLOWS = frozenset(
41-
{
42-
".github/workflows/manual-publish.yml",
43-
".github/workflows/preview-publish.yml",
44-
}
45-
)
37+
# Keyed on exact filename, each with the reason it abstains, and every exemption
38+
# is printed on success — so this cannot quietly grow. Revisit once the embargo
39+
# publish bug is fixed: the publish workflows should go back to being gatewayed
40+
# like everything else.
41+
GATEWAY_EXEMPT_WORKFLOWS = {
42+
".github/workflows/manual-publish.yml": "publishes; installs are frozen-lockfile only",
43+
".github/workflows/preview-publish.yml": "publishes; installs are frozen-lockfile only",
44+
# The gateway would reject the fresh release itself, so a gatewayed run could
45+
# not tell us whether Bun's cooldown works. This job exists to prove it does.
46+
".github/workflows/cooldown-check.yml": "must reach the registry ungatewayed to test the cooldown",
47+
}
4648

4749
FIX_HINT = """Every job must run the Wix gateway proxy immediately after a checkout that
4850
puts it on disk, or that job's npm installs bypass the Wix embargo gateway.
@@ -143,16 +145,15 @@ def job_problem(job: dict, workflows: frozenset[str]) -> str | None:
143145
return _checkout_problem(steps[0])
144146

145147

146-
def publish_job_problem(job: dict) -> str | None:
147-
"""Describe why this publish job wrongly runs the proxy, or None if it abstains."""
148+
def exempt_job_problem(job: dict, reason: str) -> str | None:
149+
"""Describe why this exempt job wrongly runs the proxy, or None if it abstains."""
148150
if "uses" in job:
149151
return None
150152
steps = job.get("steps") or []
151153
if any(_uses(step) == PROXY_ACTION for step in steps):
152154
return (
153-
"runs the Wix gateway proxy, but publish workflows must not: the gateway "
154-
"cannot carry `npm publish`, so these workflows rely on the committed "
155-
"lockfile plus min-release-age in .npmrc instead"
155+
"runs the Wix gateway proxy, but this workflow is exempt and must not "
156+
f"({reason})"
156157
)
157158
return None
158159

@@ -181,10 +182,10 @@ def main(repo_root: pathlib.Path = REPO_ROOT) -> int:
181182
lines = _job_lines(text)
182183
for job_id, job in ((yaml.safe_load(text) or {}).get("jobs") or {}).items():
183184
job = job or {}
184-
if rel in PUBLISH_WORKFLOWS:
185+
if rel in GATEWAY_EXEMPT_WORKFLOWS:
185186
exempt += 1
186187
exempt_paths.add(rel)
187-
problem = publish_job_problem(job)
188+
problem = exempt_job_problem(job, GATEWAY_EXEMPT_WORKFLOWS[rel])
188189
else:
189190
jobs += 1
190191
calls += "uses" in job
@@ -206,10 +207,9 @@ def main(repo_root: pathlib.Path = REPO_ROOT) -> int:
206207
f"delegate to the workflow they call)."
207208
)
208209
if exempt:
209-
print(
210-
f"Publish workflows exempt by policy, and verified to abstain "
211-
f"({exempt} job(s)): " + ", ".join(sorted(exempt_paths))
212-
)
210+
print(f"Exempt by policy, and verified to abstain ({exempt} job(s)):")
211+
for rel in sorted(exempt_paths):
212+
print(f" {rel}{GATEWAY_EXEMPT_WORKFLOWS[rel]}")
213213
return 0
214214

215215

.github/scripts/test_check_wix_proxy_steps.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,8 @@ def test_job_with_no_body_is_reported_as_missing_the_proxy(self):
331331
self.assertIn('Job "build" does not run the Wix gateway proxy.', output)
332332

333333

334-
class PublishExemptionTests(unittest.TestCase):
335-
"""Publish workflows must abstain from the proxy; everything else must run it."""
334+
class GatewayExemptionTests(unittest.TestCase):
335+
"""Exempt workflows must abstain from the proxy; everything else must run it."""
336336

337337
PUBLISH_WITHOUT_PROXY = textwrap.dedent("""\
338338
name: Manual Package Publish
@@ -368,9 +368,18 @@ def test_publish_workflow_may_omit_the_proxy(self):
368368
code, output = run_main(root)
369369

370370
self.assertEqual(code, 0)
371-
self.assertIn("exempt by policy", output)
371+
self.assertIn("Exempt by policy", output)
372372
self.assertIn(".github/workflows/manual-publish.yml", output)
373373

374+
def test_each_exemption_prints_its_reason(self):
375+
# An exemption is only reviewable if the run says why it exists.
376+
with fixture_repo(**{"cooldown-check": self.PUBLISH_WITHOUT_PROXY}) as root:
377+
code, output = run_main(root)
378+
379+
self.assertEqual(code, 0)
380+
expected = checker.GATEWAY_EXEMPT_WORKFLOWS[".github/workflows/cooldown-check.yml"]
381+
self.assertIn(expected, output)
382+
374383
def test_exempt_jobs_are_not_counted_as_verified(self):
375384
with fixture_repo(
376385
**{"manual-publish": self.PUBLISH_WITHOUT_PROXY, "good": COMPLIANT_WORKFLOW}
@@ -385,7 +394,7 @@ def test_publish_workflow_running_the_proxy_is_rejected(self):
385394
code, output = run_main(root)
386395

387396
self.assertEqual(code, 1)
388-
self.assertIn("but publish workflows must not", output)
397+
self.assertIn("this workflow is exempt and must not", output)
389398

390399
def test_a_non_publish_workflow_still_needs_the_proxy(self):
391400
with fixture_repo(**{"some-publish-helper": self.PUBLISH_WITHOUT_PROXY}) as root:
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Supply-chain Cooldown Check
2+
3+
# Proves the `minimumReleaseAge` cooldown in bunfig.toml actually rejects a
4+
# freshly published release — the test Dima Ryskin asked for as the review of the
5+
# interim policy, using electron-nightly for reliably "fresh" versions.
6+
#
7+
# Deliberately does NOT run the Wix gateway proxy: with embargo in front, the
8+
# gateway would refuse the fresh version and this would prove nothing about Bun's
9+
# guardrail, which is what the publish workflows rely on. check-wix-proxy.yml
10+
# knows about this exemption and verifies the job abstains.
11+
#
12+
# Uses no third-party actions. `oven-sh/setup-bun` is not on the org allowlist
13+
# (github_owned_allowed only), and SHA pinning is now required — so Bun is
14+
# installed from a run step and actions/checkout is pinned by SHA.
15+
16+
on:
17+
workflow_dispatch:
18+
pull_request:
19+
paths:
20+
- "bunfig.toml"
21+
- ".github/scripts/check_cooldown.sh"
22+
- ".github/workflows/cooldown-check.yml"
23+
24+
jobs:
25+
cooldown:
26+
runs-on: ubuntu-latest
27+
permissions:
28+
contents: read
29+
30+
steps:
31+
# actions/checkout v7.0.1, SHA-pinned per the org policy.
32+
- name: Checkout code
33+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
34+
35+
- name: Install Bun
36+
# Not oven-sh/setup-bun: third-party actions are not allowlisted for this
37+
# org, and the cooldown does not apply to Bun's own installer anyway.
38+
run: |
39+
curl -fsSL https://bun.sh/install | bash
40+
echo "$HOME/.bun/bin" >>"$GITHUB_PATH"
41+
42+
- name: Verify the cooldown rejects a fresh release
43+
run: bash .github/scripts/check_cooldown.sh

0 commit comments

Comments
 (0)