mandrel-platform 1.13.0 → 1.13.1
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/README.md +6 -2
- package/package.json +4 -2
- package/scripts/audit-check.mjs +210 -47
- package/scripts/audit-check.test.mjs +479 -49
- package/scripts/check-action-download-retries.mjs +155 -11
- package/scripts/check-action-download-retries.test.mjs +168 -3
- package/scripts/check-advisory-scan-setup.test.mjs +186 -5
- package/scripts/check-coverage-threshold.mjs +112 -17
- package/scripts/check-coverage-threshold.test.mjs +335 -0
- package/scripts/check-husky-hook-modes.test.mjs +134 -0
- package/scripts/check-runner-runs-on.test.mjs +223 -2
- package/scripts/check-semgrep-lockfile.test.mjs +238 -7
- package/scripts/check-workflow-lint-tier.test.mjs +176 -0
- package/scripts/check-workflow-portability.mjs +6 -3
- package/scripts/check-workflow-portability.test.mjs +1 -1
- package/scripts/env-doctor.mjs +493 -62
- package/scripts/env-doctor.test.mjs +529 -3
- package/scripts/fixtures/npm-audit-v2.json +66 -0
- package/scripts/install-git-hooks.mjs +123 -0
- package/scripts/install-git-hooks.test.mjs +160 -0
- package/scripts/issue-intake.test.mjs +351 -21
- package/scripts/runner-toggle.test.mjs +407 -0
- package/scripts/select-semgrep-python.sh +98 -17
- package/scripts/select-semgrep-python.test.mjs +139 -12
- package/scripts/update-semgrep-rules.mjs +83 -5
- package/scripts/update-semgrep-rules.test.mjs +55 -1
- package/scripts/workflow-lint-gate.test.mjs +128 -0
|
@@ -65,6 +65,44 @@ export function jobKeys(block) {
|
|
|
65
65
|
.map((l) => l.trim().split(":")[0]);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The composite's single `run:` block — everything after its `run: |` line.
|
|
70
|
+
* The infra-failure assertions below are about SHELL CONTROL FLOW, so they
|
|
71
|
+
* must not be able to match the action's header comments, which describe that
|
|
72
|
+
* flow in prose using the same words.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} text
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function runBlock(text) {
|
|
78
|
+
const marker = "\n run: |\n";
|
|
79
|
+
const start = text.indexOf(marker);
|
|
80
|
+
assert.notEqual(start, -1, "the composite's run: block was not found");
|
|
81
|
+
const block = text.slice(start + marker.length);
|
|
82
|
+
assert.ok(block.includes("set -euo pipefail"), "run: block did not resolve to the script");
|
|
83
|
+
return block;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The body of one shell function defined at the run-block's own indent.
|
|
88
|
+
* Scoped like `jobBlock`: the point is to assert what `infra_fail` does, not
|
|
89
|
+
* that the two words appear somewhere in a 300-line file.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} run @param {string} name
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
export function shellFunction(run, name) {
|
|
95
|
+
const lines = run.split(/\r?\n/);
|
|
96
|
+
const start = lines.findIndex((l) => l.trim() === `${name}() {`);
|
|
97
|
+
assert.notEqual(start, -1, `shell function '${name}' not defined`);
|
|
98
|
+
const body = [];
|
|
99
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
100
|
+
if (lines[i].trim() === "}") return body.join("\n");
|
|
101
|
+
body.push(lines[i]);
|
|
102
|
+
}
|
|
103
|
+
assert.fail(`shell function '${name}' is unterminated`);
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
/**
|
|
69
107
|
* The block declaring one composite-action input. Anchored to a line start
|
|
70
108
|
* because the action's header carries USAGE COMMENTS that contain the same
|
|
@@ -322,3 +360,141 @@ test("the docs explain the advisory posture, the dial, shellcheck and provenance
|
|
|
322
360
|
assert.match(section, /checksum/i);
|
|
323
361
|
assert.match(section, /no checksums file/i, "the zizmor provenance caveat must be recorded");
|
|
324
362
|
});
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Infrastructure failure routes to the gate, not to `exit 1` (Story #496, AC-2)
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
//
|
|
368
|
+
// Before #496 a release-CDN blip on either binary `exit 1`-ed inside this
|
|
369
|
+
// inline bash BEFORE the gate script was ever invoked, so the enforcement dial
|
|
370
|
+
// the gate owns could not see it: an advisory tier reddened every consumer's
|
|
371
|
+
// required check on someone else's outage. These assertions pin the routing —
|
|
372
|
+
// the behaviour on the other side of it is unit-tested end-to-end in
|
|
373
|
+
// `workflow-lint-gate.test.mjs`, which runs the real gate.
|
|
374
|
+
|
|
375
|
+
test("the gate is the ONE exit from this step — every path funnels through it", () => {
|
|
376
|
+
const run = runBlock(ACTION);
|
|
377
|
+
assert.equal(
|
|
378
|
+
run.split("workflow-lint-gate.mjs").length - 1,
|
|
379
|
+
1,
|
|
380
|
+
"the gate must be invoked from exactly one place (`run_gate`), or the infra " +
|
|
381
|
+
"path and the normal path can drift into disagreeing about the dial",
|
|
382
|
+
);
|
|
383
|
+
const gate = shellFunction(run, "run_gate");
|
|
384
|
+
assert.match(
|
|
385
|
+
gate,
|
|
386
|
+
/WORKFLOW_LINT_INFRA_FAILURE="\$\{1:-\}"/,
|
|
387
|
+
"the reason travels to the gate as an env var, empty on the normal path",
|
|
388
|
+
);
|
|
389
|
+
assert.match(gate, /WORKFLOW_LINT_ENFORCE="\$\{WORKFLOW_LINT_ENFORCE\}"/);
|
|
390
|
+
assert.match(gate, /node "\$\{ACTION_PATH\}\/workflow-lint-gate\.mjs"/);
|
|
391
|
+
assert.match(run, /^\s*run_gate ""$/m, "the normal path still invokes the gate");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("infra_fail writes empty reports and hands the reason to the gate", () => {
|
|
395
|
+
const body = shellFunction(runBlock(ACTION), "infra_fail");
|
|
396
|
+
assert.match(body, /echo "\[\]" > "\$\{al_report\}"/);
|
|
397
|
+
assert.match(body, /echo "\[\]" > "\$\{zz_report\}"/);
|
|
398
|
+
assert.match(body, /run_gate "\$1"/, "the failure reason is forwarded, not swallowed");
|
|
399
|
+
assert.match(
|
|
400
|
+
body,
|
|
401
|
+
/exit "\$infra_code"/,
|
|
402
|
+
"the step's exit code is the GATE's verdict, not a hard-coded 1",
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("the download branches route to infra_fail rather than exiting 1", () => {
|
|
407
|
+
const run = runBlock(ACTION);
|
|
408
|
+
// curl's failure must be CAPTURED — an unguarded curl under `set -e` aborts
|
|
409
|
+
// the step before any of this routing can run.
|
|
410
|
+
for (const varName of ["al_curl", "zz_curl"]) {
|
|
411
|
+
assert.ok(run.includes(`${varName}=$?`), `${varName} does not capture curl's exit code`);
|
|
412
|
+
assert.ok(
|
|
413
|
+
run.includes(`if [ "$${varName}" -ne 0 ]; then`),
|
|
414
|
+
`${varName} is captured but never branched on`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
assert.equal(
|
|
418
|
+
run.split('infra_fail "download-failed:').length - 1,
|
|
419
|
+
2,
|
|
420
|
+
"both downloads (actionlint and zizmor) must route to infra_fail",
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("the checksum-mismatch branches route to infra_fail rather than exiting 1", () => {
|
|
425
|
+
const run = runBlock(ACTION);
|
|
426
|
+
assert.equal(
|
|
427
|
+
run.split('infra_fail "checksum-mismatch:').length - 1,
|
|
428
|
+
2,
|
|
429
|
+
"both checksum comparisons must route to infra_fail",
|
|
430
|
+
);
|
|
431
|
+
// Trust is unchanged: a mismatch still never reaches extract/execute.
|
|
432
|
+
const mismatch = run.slice(run.indexOf('infra_fail "checksum-mismatch: actionlint'));
|
|
433
|
+
assert.ok(
|
|
434
|
+
mismatch.indexOf("tar -xzf") > 0,
|
|
435
|
+
"the mismatch branch must precede extraction — an unverified archive is never opened",
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("an unmapped platform routes to infra_fail from all four sites", () => {
|
|
440
|
+
const run = runBlock(ACTION);
|
|
441
|
+
assert.equal(
|
|
442
|
+
run.split('infra_fail "unmapped-platform:').length - 1,
|
|
443
|
+
4,
|
|
444
|
+
"unsupported OS, unsupported arch, and the two unmapped checksum slugs",
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("no infrastructure path exits 1 directly any more", () => {
|
|
449
|
+
const run = runBlock(ACTION);
|
|
450
|
+
const hardExits = run
|
|
451
|
+
.split(/\r?\n/)
|
|
452
|
+
.filter((l) => l.trim() === "exit 1" || l.trim().endsWith("; exit 1 ;;"));
|
|
453
|
+
assert.deepEqual(
|
|
454
|
+
hardExits,
|
|
455
|
+
[],
|
|
456
|
+
"a bare `exit 1` here bypasses the gate's dial entirely, which is the defect " +
|
|
457
|
+
"Story #496 removed. Route the failure through infra_fail instead.",
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("a genuine TOOL failure still exits non-zero regardless of the dial", () => {
|
|
462
|
+
// The split matters: a linter that RAN and crashed is about the caller's
|
|
463
|
+
// tree, not about a third-party CDN, so it is red either way.
|
|
464
|
+
const run = runBlock(ACTION);
|
|
465
|
+
assert.match(run, /exit "\$al_code"/);
|
|
466
|
+
assert.match(run, /exit "\$zz_code"/);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
// The posture is documented on both surfaces (Story #496, AC-3)
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
test("the enforce input describes the advisory infra-failure posture", () => {
|
|
474
|
+
const desc = actionInput(ACTION, "enforce").replace(/\s+/g, " ");
|
|
475
|
+
assert.match(desc, /INFRASTRUCTURE FAILURES follow this same dial/);
|
|
476
|
+
assert.match(desc, /exits 0 while the tier is advisory/);
|
|
477
|
+
assert.match(
|
|
478
|
+
desc,
|
|
479
|
+
/TOOL failure/,
|
|
480
|
+
"the description must still name the class that is red regardless",
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("the docs no longer claim infrastructure failures always fail the tier", () => {
|
|
485
|
+
assert.ok(
|
|
486
|
+
!/always fails? the tier/i.test(DOCS),
|
|
487
|
+
"the retired sentence (download/checksum/tool failures always fail the tier) " +
|
|
488
|
+
"is now wrong for two of those three classes",
|
|
489
|
+
);
|
|
490
|
+
const start = DOCS.indexOf("### Workflow lint tier (`enable-workflow-lint`)");
|
|
491
|
+
assert.notEqual(start, -1);
|
|
492
|
+
const section = DOCS.slice(start, start + 9000);
|
|
493
|
+
assert.match(section, /infrastructure failure/i);
|
|
494
|
+
assert.match(
|
|
495
|
+
section,
|
|
496
|
+
/exits 0 while the tier\s+is advisory/,
|
|
497
|
+
"the section must state the advisory-until-enforced behaviour explicitly",
|
|
498
|
+
);
|
|
499
|
+
assert.match(section, /never extracted or executed/, "the trust caveat must survive");
|
|
500
|
+
});
|
|
@@ -48,8 +48,11 @@
|
|
|
48
48
|
* are no logs, and `gh pr checks` reports `pending 0`, indistinguishable
|
|
49
49
|
* from a busy fleet. Normalize to
|
|
50
50
|
* `fromJSON(startsWith(inputs.runner, '[') && inputs.runner ||
|
|
51
|
-
* format('"{0}"', inputs.runner))`, which accepts both
|
|
52
|
-
* shapes
|
|
51
|
+
* format('"{0}"', inputs.runner || 'ubuntu-latest'))`, which accepts both
|
|
52
|
+
* documented shapes and lands an EMPTY value on the default label rather
|
|
53
|
+
* than on `""` — a `default:` fires only when the key is absent, so an
|
|
54
|
+
* empty-but-present value reaches the site and would queue forever too
|
|
55
|
+
* (#493). (Caused #419 / v1.5.0; behaviour is covered by
|
|
53
56
|
* check-runner-runs-on.test.mjs, which evaluates the real expression
|
|
54
57
|
* rather than matching its spelling.)
|
|
55
58
|
*
|
|
@@ -301,7 +304,7 @@ export function checkWorkflowContent(content) {
|
|
|
301
304
|
`JSON-encoded label-array string resolves to ONE unmatchable label ` +
|
|
302
305
|
`name and the job queues until the 24-hour timeout, silently. ` +
|
|
303
306
|
`Normalize it: \`fromJSON(startsWith(inputs.runner, '[') && ` +
|
|
304
|
-
`inputs.runner || format('"{0}"', inputs.runner))\`.`,
|
|
307
|
+
`inputs.runner || format('"{0}"', inputs.runner || 'ubuntu-latest'))\`.`,
|
|
305
308
|
});
|
|
306
309
|
});
|
|
307
310
|
|
|
@@ -223,7 +223,7 @@ function runnerWorkflow(value) {
|
|
|
223
223
|
|
|
224
224
|
const NORMALIZED =
|
|
225
225
|
"${{ fromJSON(startsWith(inputs.runner, '[') && inputs.runner " +
|
|
226
|
-
"|| format('\"{0}\"', inputs.runner)) }}";
|
|
226
|
+
"|| format('\"{0}\"', inputs.runner || 'ubuntu-latest')) }}";
|
|
227
227
|
|
|
228
228
|
test("Rule 4: a raw `runs-on: ${{ inputs.runner }}` is a violation", () => {
|
|
229
229
|
const violations = checkWorkflowContent(runnerWorkflow("${{ inputs.runner }}"));
|