Skip to content

Commit 4cd07ce

Browse files
committed
fix: stop the nightly crying wolf, and prove the deny is actually armed
Three from the final whole-diff review. The worst was self-inflicted and would have shipped as a recurring 4am alarm. WEBJS_REQUIRE_NETWORK did double duty: it selected the live files AND promoted every upstream-trouble skip into a failure. The nightly always sets it, and the runners only select those files under it, so the transport-level skip that #1219 built was unreachable everywhere automated. A single jspm 503 or DNS blip would have redded the job and filed a bug issue, which is exactly the cry-wolf outcome the workflow's own header argues against. The two concerns are separate variables now: WEBJS_REQUIRE_NETWORK selects and lifts the deny, WEBJS_FAIL_ON_SKIP promotes, and the nightly sets only the first. A skip becomes a warning annotation instead, so a permanently skipping test is still visible without waking anyone for an outage. The live pin test had a wider version of the same trap. Its skip fired only on a non-zero exit, but a hiccup on the bundle GET leaves pin exiting 0 with the entry pinned and no hash, so the integrity assertion hard-failed on an outage the exit code had already forgiven. It skips there too now. withJspmDouble threw from a finally, which REPLACES an in-flight error. A refused request usually travels with the assertion it broke, so that discarded the message explaining what went wrong, in the case where it matters most. The restore stays in the finally; the report moved to the success path. And the guard file claimed to assert both enforcement mechanisms while never checking the deny was armed in a running process. Its other tests exercise a pure function and grep runner sources, so an inverted self-install would leave them all green while the required job went back to reaching jspm. It now spawns a preloaded child and asserts both branches of the switch.
1 parent e454972 commit 4cd07ce

6 files changed

Lines changed: 130 additions & 26 deletions

File tree

.github/workflows/vendor-cdn.yml

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ name: Vendor CDN contract (nightly)
2222
# outage; it would just be a red somebody is told to ignore, which is how a
2323
# real failure gets ignored too.
2424
#
25-
# WEBJS_REQUIRE_NETWORK does double duty. It selects the live files, and it
26-
# turns their upstream-trouble SKIP into a FAILURE. Without that second half a
27-
# permanently skipping test is indistinguishable from a passing one, which is
28-
# the exact way live coverage rots into decoration.
25+
# WEBJS_REQUIRE_NETWORK selects the live files and lifts the test-run deny. It
26+
# does NOT promote their upstream-trouble skip into a failure, and that
27+
# separation is deliberate: it briefly did both, and since this job always sets
28+
# it, the transport-level skip was unreachable wherever the tests actually run.
29+
# A single jspm 503 or DNS blip at 04:20 UTC would then have redded the job and
30+
# filed the issue below, which is precisely the cry-wolf failure the paragraph
31+
# above argues against.
32+
#
33+
# A permanently skipping test still must not pass for a healthy one, so the run
34+
# is scanned for skips and annotates a warning instead. That is visible in the
35+
# run summary without waking anyone for an outage. `WEBJS_FAIL_ON_SKIP=1`
36+
# promotes a skip to a failure when you want to force the question by hand.
2937
#
3038
# There is deliberately no `pull_request` trigger, so this can never become a
3139
# required check and can never block a merge.
@@ -61,12 +69,30 @@ jobs:
6169
cache: npm
6270
- run: npm ci
6371
- name: Run the live CDN tests
72+
id: live
6473
env:
6574
WEBJS_REQUIRE_NETWORK: '1'
6675
run: |
76+
set -o pipefail
6777
node --test \
6878
packages/server/test/vendor/jspm-cdn.live.test.js \
69-
test/vendor-cli/vendor-pin.live.test.mjs
79+
test/vendor-cli/vendor-pin.live.test.mjs 2>&1 | tee live.log
80+
81+
- name: Warn if a live check only skipped
82+
# Runs even when the step above failed, so a partial skip is still
83+
# reported. A skip means jspm could not answer, which is upstream's
84+
# problem, not a regression; it is surfaced rather than escalated.
85+
if: always()
86+
run: |
87+
set -euo pipefail
88+
skipped=$(grep -c '^# SKIP\|^ℹ skipped' live.log 2>/dev/null || true)
89+
if grep -q 'SKIP ' live.log 2>/dev/null; then
90+
echo "::warning title=Live jspm check skipped::jspm.io could not answer at least one check. \
91+
Not a regression, but if this repeats for days the live coverage has stopped running. \
92+
Re-run with WEBJS_FAIL_ON_SKIP=1 to force it to fail instead."
93+
grep 'SKIP ' live.log || true
94+
fi
95+
echo "skip markers: ${skipped}"
7096
7197
- name: Report a failure on the tracking issue
7298
if: failure()

packages/server/test/vendor/jspm-cdn.live.test.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,26 @@
1313
* whole job is to talk to jspm, and a double can only ever return what this
1414
* repo already believes about the API. So these two assertions stay real, and
1515
* `.github/workflows/vendor-cdn.yml` runs them nightly with
16-
* `WEBJS_REQUIRE_NETWORK=1`, which ALSO turns a skip into a failure. Without
17-
* that, a permanently skipping test is indistinguishable from a passing one.
16+
* `WEBJS_REQUIRE_NETWORK=1`, and surfaces any skip as a warning annotation, so
17+
* a permanently skipping test is visible rather than indistinguishable from a
18+
* passing one.
1819
*
1920
* Upstream trouble skips rather than reds, judged at the transport: a throw, a
2021
* 5xx, or a 429 is jspm having a bad moment. A 4xx does not skip, because by
2122
* then a ground-truth call has just succeeded against the same fixture, so
2223
* upstream is demonstrably healthy and a 4xx means OUR request is malformed.
2324
* That distinction is #1219's, and it is the reason this file can be run
2425
* nightly without becoming a source of false alarms.
26+
*
27+
* That only holds if the skip is REACHABLE where the file runs. It briefly was
28+
* not: `WEBJS_REQUIRE_NETWORK` both selected these files and promoted every
29+
* skip to a failure, and the nightly always sets it, so the transport
30+
* distinction had no effect anywhere automated and a single 503 at 04:20 UTC
31+
* would have filed a bug issue. The two concerns are separate variables now.
32+
* `WEBJS_REQUIRE_NETWORK` selects the files and lifts the deny;
33+
* `WEBJS_FAIL_ON_SKIP` promotes a skip, and the nightly does NOT set it. The
34+
* nightly instead reports skips as a warning annotation, so a permanently
35+
* skipping test is visible without waking anyone for an outage.
2536
*/
2637
import { test } from 'node:test';
2738
import assert from 'node:assert/strict';
@@ -37,18 +48,22 @@ const GENERATE_BODY = (install) => JSON.stringify({
3748
* Build a loud skip for one fixture.
3849
*
3950
* Loud on purpose: a silent skip is how a real regression hides, so the reason
40-
* and the fixture are always named. Under `WEBJS_REQUIRE_NETWORK` the skip
41-
* becomes a FAILURE instead, which is what makes the nightly job able to tell
42-
* "jspm changed under us" from "everything is fine". A normal run is
43-
* unaffected, since the runners exclude this file entirely.
51+
* and the fixture are always named, and the nightly turns any skip into a
52+
* warning annotation.
53+
*
54+
* `WEBJS_FAIL_ON_SKIP` promotes it to a failure. Deliberately NOT the same
55+
* variable that selects this file, and deliberately not set by the nightly:
56+
* upstream being down is not a regression, and a job that reds on it is a job
57+
* whose reds get ignored. Set it by hand when you want to know that the check
58+
* genuinely ran.
4459
*
4560
* @param {import('node:test').TestContext} t
4661
* @param {string} fixture
4762
*/
4863
function skipper(t, fixture) {
4964
return (reason) => {
5065
const first = String(reason).split('\n')[0];
51-
if (process.env.WEBJS_REQUIRE_NETWORK) {
66+
if (process.env.WEBJS_FAIL_ON_SKIP) {
5267
assert.fail(`live jspm check could not run (${fixture}): ${first}`);
5368
}
5469
console.warn(`[jspm-cdn.live] SKIP ${fixture} (${first})`);

test/fixtures/deny-live-hosts.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ export function denyLiveHosts(realFetch, onDenied) {
7777
};
7878
}
7979

80+
/**
81+
* Set on `globalThis` when the self-install below has run, so a guard can
82+
* prove the preload actually took effect in a process rather than only that a
83+
* runner's source mentions it. An inverted or dropped self-install is
84+
* otherwise invisible: every unit test of `denyLiveHosts` keeps passing while
85+
* the required job goes back to reaching jspm.
86+
*/
87+
export const DENY_INSTALLED_FLAG = '__webjsDenyLiveHostsInstalled';
88+
8089
if (!process.env.WEBJS_REQUIRE_NETWORK) {
8190
/** @type {Set<string>} */
8291
const seen = new Set();
@@ -92,4 +101,5 @@ if (!process.env.WEBJS_REQUIRE_NETWORK) {
92101
seen.add(key);
93102
process.stderr.write(`[deny-live-hosts] refused ${key}\n`);
94103
}));
104+
/** @type {any} */ (globalThis)[DENY_INSTALLED_FLAG] = true;
95105
}

test/fixtures/jspm-double.mjs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,16 +230,23 @@ export async function withJspmDouble(opts, body) {
230230
const original = globalThis.fetch;
231231
globalThis.fetch = /** @type {any} */ (double);
232232
clearVendorCache();
233+
let result;
233234
try {
234-
return await body(double);
235+
result = await body(double);
235236
} finally {
236237
globalThis.fetch = original;
237238
clearVendorCache();
238-
if (double.unexpected.length) {
239-
throw new Error(
240-
`the jspm double was asked for ${double.unexpected.length} request(s) it does not serve:\n ` +
241-
`${double.unexpected.join('\n ')}`,
242-
);
243-
}
244239
}
240+
// Deliberately OUTSIDE the finally. Throwing from a finally REPLACES an
241+
// in-flight error, and a refused request usually travels with the assertion
242+
// it broke, so raising it there would discard the message that explains
243+
// what actually went wrong. Restoring is what the finally is for; reporting
244+
// happens only on the success path, where nothing is being displaced.
245+
if (double.unexpected.length) {
246+
throw new Error(
247+
`the jspm double was asked for ${double.unexpected.length} request(s) it does not serve:\n ` +
248+
`${double.unexpected.join('\n ')}`,
249+
);
250+
}
251+
return result;
245252
}

test/repo-health/live-cdn-callers.test.mjs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ import { readdirSync, readFileSync } from 'node:fs';
3131
import { join, resolve, dirname, sep } from 'node:path';
3232
import { fileURLToPath } from 'node:url';
3333

34-
import { denyLiveHosts, DENIED_HOSTS } from '../fixtures/deny-live-hosts.mjs';
34+
import { spawnSync } from 'node:child_process';
35+
import { pathToFileURL } from 'node:url';
36+
37+
import { denyLiveHosts, DENIED_HOSTS, DENY_INSTALLED_FLAG } from '../fixtures/deny-live-hosts.mjs';
3538

3639
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
3740

@@ -144,6 +147,37 @@ test('both runners install the deny and skip live files, unless the network is r
144147
"the preload must come AFTER bun's `test` subcommand");
145148
});
146149

150+
test('the preload actually arms the deny in a real process', () => {
151+
// Everything else here checks `denyLiveHosts` as a pure function or greps a
152+
// runner's source, and neither notices if the self-install at the bottom of
153+
// the fixture is inverted or deleted: the unit tests stay green while the
154+
// required job goes back to reaching jspm. That is the same class of defect
155+
// three earlier review rounds found in this guard, so prove the install by
156+
// running it.
157+
//
158+
// Spawned rather than asserted on this process, so the check does not depend
159+
// on how THIS file was launched, and so both branches of the env switch can
160+
// be exercised in one test.
161+
const fixture = pathToFileURL(join(ROOT, 'test/fixtures/deny-live-hosts.mjs')).href;
162+
const probe = 'const r = await fetch("https://api.jspm.io/generate", { method: "POST" });'
163+
+ `console.log(JSON.stringify({ status: r.status, armed: Boolean(globalThis[${JSON.stringify(DENY_INSTALLED_FLAG)}]) }));`;
164+
165+
const run = (env) => {
166+
const r = spawnSync(process.execPath, ['--import', fixture, '--input-type=module', '-e', probe],
167+
{ encoding: 'utf8', env: { ...process.env, ...env }, timeout: 30_000 });
168+
return JSON.parse((r.stdout || '{}').trim() || '{}');
169+
};
170+
171+
const denied = run({ WEBJS_REQUIRE_NETWORK: '' });
172+
assert.equal(denied.armed, true, 'the preload must install itself by default');
173+
assert.equal(denied.status, 503, 'a jspm call in a preloaded process must be denied, not sent');
174+
175+
// The opt-out has to actually opt out, or the nightly could never reach the
176+
// real CDN. Asserting the flag rather than a live status keeps this offline.
177+
const allowed = run({ WEBJS_REQUIRE_NETWORK: '1' });
178+
assert.notEqual(allowed.armed, true, 'WEBJS_REQUIRE_NETWORK must lift the deny');
179+
});
180+
147181
test('every allowlisted live caller is a *.live.test.* file that exists', () => {
148182
for (const entry of LIVE_CALLERS) {
149183
assert.ok(entry.file.includes(LIVE_MARKER),

test/vendor-cli/vendor-pin.live.test.mjs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,19 @@ test('pin resolves picocolors against the real CDN and hashes the bytes', async
5454
`import pico from 'picocolors';\nexport default () => pico.green('ok');`);
5555

5656
const { code, stdout, stderr } = await runCli(['vendor', 'pin'], dir);
57-
if (code !== 0) {
58-
// A failed pin here is upstream trouble far more often than a regression,
59-
// and the CLI already names the reason. Under WEBJS_REQUIRE_NETWORK the
60-
// nightly wants to know, so fail loudly there instead of skipping.
61-
const why = `exit ${code}: ${(stderr || stdout).split('\n').filter(Boolean).slice(-1)[0] || 'no output'}`;
62-
if (process.env.WEBJS_REQUIRE_NETWORK) {
57+
// Upstream trouble is not a regression. `WEBJS_FAIL_ON_SKIP` promotes it,
58+
// and is deliberately NOT the variable that selects this file nor one the
59+
// nightly sets, so a jspm outage does not red a scheduled run.
60+
const skip = (why) => {
61+
if (process.env.WEBJS_FAIL_ON_SKIP) {
6362
assert.fail(`live \`webjs vendor pin\` could not run (${why})`);
6463
}
6564
console.warn(`[vendor-pin.live] SKIP live pin (${why})`);
6665
t.skip('jspm.io was not in a state that can answer a pin');
66+
};
67+
68+
if (code !== 0) {
69+
skip(`exit ${code}: ${(stderr || stdout).split('\n').filter(Boolean).slice(-1)[0] || 'no output'}`);
6770
return;
6871
}
6972

@@ -74,6 +77,15 @@ test('pin resolves picocolors against the real CDN and hashes the bytes', async
7477
// The offline double mints this tail, so its absence is what proves this
7578
// run really went to the network rather than picking up a stray preload.
7679
assert.doesNotMatch(url, /\/double\.js$/, 'this test must NOT be running against the double');
80+
81+
// A hiccup on the BUNDLE GET is the wider version of the same trap: pin
82+
// exits 0 with the entry pinned and no hash, and the CLI says so, so
83+
// asserting the hash outright would hard-fail on an outage the exit code
84+
// already forgave.
85+
if (!parsed.integrity || !parsed.integrity[url]) {
86+
skip('jspm.io resolved the package but would not serve its bundle to hash');
87+
return;
88+
}
7789
assert.match(parsed.integrity[url], /^sha384-/,
7890
'the bundle behind the resolved url must have been fetched and hashed');
7991
} finally {

0 commit comments

Comments
 (0)