mandrel-platform 1.12.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 +370 -1
- 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 +647 -90
- package/scripts/env-doctor.test.mjs +772 -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 +215 -0
- package/scripts/select-semgrep-python.test.mjs +344 -0
- 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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-husky-hook-modes.test.mjs — regression guard for the mode bit that
|
|
4
|
+
* decides whether this repository's git hooks run at all.
|
|
5
|
+
*
|
|
6
|
+
* The bug this pins: `.husky/pre-commit` was tracked at mode 100644. Git
|
|
7
|
+
* 2.36+ refuses to execute a hook that is not marked executable, and the way
|
|
8
|
+
* it refuses is the problem — it prints
|
|
9
|
+
*
|
|
10
|
+
* hint: The '.../.husky/pre-commit' hook was ignored because it's not
|
|
11
|
+
* set as executable.
|
|
12
|
+
*
|
|
13
|
+
* to stderr and then lets the commit SUCCEED with exit 0. So the quality gate
|
|
14
|
+
* the hook exists to run (`quality-preview.js`, which blocks MI/CRAP drift at
|
|
15
|
+
* commit time) never ran for anybody, in any clone, from the day it was
|
|
16
|
+
* added — and nothing anywhere went red to say so. An inert gate and a
|
|
17
|
+
* passing gate are indistinguishable from the outside.
|
|
18
|
+
*
|
|
19
|
+
* Why the mode is asserted against the INDEX and not the filesystem. `chmod`
|
|
20
|
+
* on the working copy is not the fix and would not be caught here: git stores
|
|
21
|
+
* only two file modes, 100644 and 100755, and it is the tracked one that
|
|
22
|
+
* every fresh clone and every `git checkout` materializes. A working tree can
|
|
23
|
+
* be executable while the committed mode is not (and vice versa), so a
|
|
24
|
+
* `statSync` check would go green on the maintainer's machine and ship the
|
|
25
|
+
* defect to everyone else. `git ls-files -s` reads the index, which is the
|
|
26
|
+
* mode that actually propagates.
|
|
27
|
+
*
|
|
28
|
+
* Why this is not a one-line assertion on one path. The upstream cause is
|
|
29
|
+
* still live: the hook is installed by the vendored bootstrap
|
|
30
|
+
* (`.agents/scripts/lib/bootstrap/quality-bootstrap.js`) with a bare
|
|
31
|
+
* `fs.writeFileSync` and no `chmod`, so a hook file CREATED by a fresh
|
|
32
|
+
* bootstrap run is born 0644. (An existing file survives — `writeFileSync`
|
|
33
|
+
* truncates in place and preserves the mode — which is why fixing the tracked
|
|
34
|
+
* mode holds.) `.agents/**` is vendored payload re-materialized from the
|
|
35
|
+
* mandrel package, so that cause cannot be fixed here; this guard is the
|
|
36
|
+
* compensating control, and it therefore covers every hook the directory may
|
|
37
|
+
* grow, not just the one that was broken.
|
|
38
|
+
*
|
|
39
|
+
* Run: node --test scripts/check-husky-hook-modes.test.mjs
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import assert from "node:assert/strict";
|
|
43
|
+
import { test } from "node:test";
|
|
44
|
+
import { execFileSync } from "node:child_process";
|
|
45
|
+
|
|
46
|
+
const HOOKS_DIR = ".husky";
|
|
47
|
+
const EXECUTABLE = "100755";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every client-side hook name git will invoke. A file in `.husky/` whose name
|
|
51
|
+
* is not on this list is never run by git no matter what its mode is, so a
|
|
52
|
+
* typo (`pre_commit`, `precommit`) is the same silent no-op as a missing
|
|
53
|
+
* executable bit and is asserted against below.
|
|
54
|
+
*
|
|
55
|
+
* Source: `githooks(5)`, client-side hooks only — server-side hooks
|
|
56
|
+
* (`pre-receive`, `update`, `post-receive`) cannot fire from a clone.
|
|
57
|
+
*/
|
|
58
|
+
const GIT_CLIENT_HOOKS = new Set([
|
|
59
|
+
"applypatch-msg",
|
|
60
|
+
"pre-applypatch",
|
|
61
|
+
"post-applypatch",
|
|
62
|
+
"pre-commit",
|
|
63
|
+
"pre-merge-commit",
|
|
64
|
+
"prepare-commit-msg",
|
|
65
|
+
"commit-msg",
|
|
66
|
+
"post-commit",
|
|
67
|
+
"pre-rebase",
|
|
68
|
+
"post-checkout",
|
|
69
|
+
"post-merge",
|
|
70
|
+
"pre-push",
|
|
71
|
+
"pre-auto-gc",
|
|
72
|
+
"post-rewrite",
|
|
73
|
+
"sendemail-validate",
|
|
74
|
+
"post-index-change",
|
|
75
|
+
"reference-transaction",
|
|
76
|
+
"push-to-checkout",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Tracked entries directly under `.husky/`, as `{ mode, path, name }`.
|
|
81
|
+
*
|
|
82
|
+
* `git ls-files -s` emits `<mode> <object> <stage>\t<path>`. Husky's own
|
|
83
|
+
* `_/` shim directory and dotfiles (`.gitignore`) are not hooks and are
|
|
84
|
+
* excluded; everything else in the directory is one by convention.
|
|
85
|
+
*/
|
|
86
|
+
function trackedHooks() {
|
|
87
|
+
const out = execFileSync("git", ["ls-files", "-s", "--", HOOKS_DIR], {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return out
|
|
92
|
+
.split("\n")
|
|
93
|
+
.filter((line) => line.length > 0)
|
|
94
|
+
.map((line) => {
|
|
95
|
+
const [meta, path] = line.split("\t");
|
|
96
|
+
return { mode: meta.split(" ")[0], path, name: path.slice(HOOKS_DIR.length + 1) };
|
|
97
|
+
})
|
|
98
|
+
.filter(({ name }) => !name.startsWith(".") && !name.startsWith("_/"));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test("the quality-gate pre-commit hook is still tracked", () => {
|
|
102
|
+
// Without this the mode assertion below passes vacuously the moment the
|
|
103
|
+
// hook is deleted — which is the same outcome (no gate) by another route.
|
|
104
|
+
const names = trackedHooks().map((h) => h.name);
|
|
105
|
+
assert.ok(
|
|
106
|
+
names.includes("pre-commit"),
|
|
107
|
+
`${HOOKS_DIR}/pre-commit is not tracked; the commit-time quality gate would not run. Tracked: ${names.join(", ") || "(none)"}`,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("every tracked hook under .husky/ is executable in the index", () => {
|
|
112
|
+
const hooks = trackedHooks();
|
|
113
|
+
assert.ok(hooks.length > 0, `no tracked hooks found under ${HOOKS_DIR}/`);
|
|
114
|
+
|
|
115
|
+
const nonExecutable = hooks.filter((h) => h.mode !== EXECUTABLE);
|
|
116
|
+
assert.deepEqual(
|
|
117
|
+
nonExecutable.map((h) => `${h.path} (${h.mode})`),
|
|
118
|
+
[],
|
|
119
|
+
`git 2.36+ silently IGNORES a non-executable hook and lets the commit succeed with exit 0. ` +
|
|
120
|
+
`Fix the tracked mode with: git update-index --chmod=+x <path> (a bare chmod does not change it).`,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("every tracked hook under .husky/ has a name git will actually invoke", () => {
|
|
125
|
+
const unknown = trackedHooks()
|
|
126
|
+
.map((h) => h.name)
|
|
127
|
+
.filter((name) => !GIT_CLIENT_HOOKS.has(name));
|
|
128
|
+
|
|
129
|
+
assert.deepEqual(
|
|
130
|
+
unknown,
|
|
131
|
+
[],
|
|
132
|
+
`git invokes hooks by exact filename; a name outside githooks(5) never runs, as silently as a non-executable one.`,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* check-runner-runs-on.test.mjs — regression guard for
|
|
4
|
-
*
|
|
3
|
+
* check-runner-runs-on.test.mjs — regression guard for every shape the
|
|
4
|
+
* `runner` input can arrive in (Stories #421, #493).
|
|
5
5
|
*
|
|
6
6
|
* The bug this pins: every `runs-on:` site consumed the input raw as
|
|
7
7
|
* `${{ inputs.runner }}`. GitHub does not parse a JSON-array *string* in that
|
|
@@ -21,6 +21,25 @@
|
|
|
21
21
|
* expression from the workflow and EVALUATES it under Actions semantics, the
|
|
22
22
|
* same read-then-execute approach as check-toolchain-cache-default.test.mjs.
|
|
23
23
|
*
|
|
24
|
+
* The same failure had a second door, closed by #493: `runner: ''`. A
|
|
25
|
+
* workflow_call `default:` fires only when the key is ABSENT, so a caller
|
|
26
|
+
* who passes the key with an empty value — threading an unset input or a
|
|
27
|
+
* matrix value through — got the label `""` rather than `ubuntu-latest`, and
|
|
28
|
+
* with it the identical never-scheduled job. The fallback therefore lives at
|
|
29
|
+
* the `runs-on` site, and the byte-identical assertion below is what stops a
|
|
30
|
+
* fix that reaches six of the seven workflows from shipping as if it reached
|
|
31
|
+
* all seven.
|
|
32
|
+
*
|
|
33
|
+
* Story #493 also left a downstream door open, closed here: `runs-on` was not
|
|
34
|
+
* the only site reading `inputs.runner`. `pr-quality.yml`'s harden-runner
|
|
35
|
+
* egress-audit step gates on `startsWith(inputs.runner, 'ubuntu-')`, which was
|
|
36
|
+
* unreachable-but-consistent while `runner: ''` never scheduled a job. Once the
|
|
37
|
+
* empty value resolved to the hosted default, the job ran on ubuntu-latest
|
|
38
|
+
* while the gate read the raw `''` and skipped — a SECURITY step opting itself
|
|
39
|
+
* out with nothing red to show for it. So the gate is asserted the same way:
|
|
40
|
+
* extracted and EVALUATED, and required to AGREE with what `runs-on` resolves
|
|
41
|
+
* to for the same input.
|
|
42
|
+
*
|
|
24
43
|
* Run: node --test scripts/check-runner-runs-on.test.mjs
|
|
25
44
|
*/
|
|
26
45
|
|
|
@@ -28,6 +47,7 @@ import assert from "node:assert/strict";
|
|
|
28
47
|
import { test } from "node:test";
|
|
29
48
|
import { readFileSync, readdirSync } from "node:fs";
|
|
30
49
|
import { evaluate } from "./lib/actions-expression.mjs";
|
|
50
|
+
import { stepByName } from "./lib/yaml-step.mjs";
|
|
31
51
|
|
|
32
52
|
const WORKFLOW_DIR = ".github/workflows";
|
|
33
53
|
|
|
@@ -58,6 +78,28 @@ function runsOnExpressions(text) {
|
|
|
58
78
|
return out;
|
|
59
79
|
}
|
|
60
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The `default:` this workflow declares for its `runner` workflow_call input.
|
|
83
|
+
*
|
|
84
|
+
* Read from the YAML rather than hardcoded so the empty-input assertion below
|
|
85
|
+
* pins the real contract: whatever label a caller gets by omitting `runner`
|
|
86
|
+
* is the label an explicitly-empty `runner` must get too. Scanned line by
|
|
87
|
+
* line — the block boundary is indentation, and a dynamically-constructed
|
|
88
|
+
* regex is a SAST finding that buys nothing here.
|
|
89
|
+
*/
|
|
90
|
+
function declaredRunnerDefault(text) {
|
|
91
|
+
const lines = text.split("\n");
|
|
92
|
+
const start = lines.indexOf(" runner:");
|
|
93
|
+
assert.notEqual(start, -1, "no `runner:` workflow_call input found");
|
|
94
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
95
|
+
const line = lines[i];
|
|
96
|
+
if (line.trim() !== "" && !line.startsWith(" ")) break;
|
|
97
|
+
const m = line.match(/^\s+default:\s*'([^']*)'\s*$/);
|
|
98
|
+
if (m) return m[1];
|
|
99
|
+
}
|
|
100
|
+
return assert.fail("the `runner` input declares no `default:` to fall back to");
|
|
101
|
+
}
|
|
102
|
+
|
|
61
103
|
const WORKFLOWS = runnerWorkflows();
|
|
62
104
|
|
|
63
105
|
test("every workflow taking a `runner` input is covered by this guard", () => {
|
|
@@ -127,6 +169,38 @@ for (const { file, text } of WORKFLOWS) {
|
|
|
127
169
|
}
|
|
128
170
|
});
|
|
129
171
|
|
|
172
|
+
test(`${file}: an empty runner resolves to this workflow's documented default`, () => {
|
|
173
|
+
// The silent-queue shape #421 left behind. `runner: ''` is not exotic — a
|
|
174
|
+
// caller threading `runner: ${{ inputs.runner }}` or a matrix value that
|
|
175
|
+
// resolves to nothing passes it without meaning to, and `format('"{0}"',
|
|
176
|
+
// '')` yielded the label `""`. An empty label matches no runner, so the
|
|
177
|
+
// job sat `queued` with no logs and no red, exactly as the JSON-array
|
|
178
|
+
// string did. The fallback belongs at the `runs-on` site because the
|
|
179
|
+
// input `default:` only fires when the key is ABSENT, never when it is
|
|
180
|
+
// present and empty.
|
|
181
|
+
const fallback = declaredRunnerDefault(text);
|
|
182
|
+
assert.notEqual(fallback, "", `${file}: the declared default is itself empty`);
|
|
183
|
+
for (const { line, expr } of sites) {
|
|
184
|
+
const resolved = evaluate(expr, { runner: "" });
|
|
185
|
+
assert.equal(
|
|
186
|
+
typeof resolved,
|
|
187
|
+
"string",
|
|
188
|
+
`${file}:${line}: an empty runner must resolve to a single label, not ${JSON.stringify(resolved)}`,
|
|
189
|
+
);
|
|
190
|
+
assert.notEqual(
|
|
191
|
+
resolved,
|
|
192
|
+
"",
|
|
193
|
+
`${file}:${line}: an empty runner resolves to an empty label — no runner ` +
|
|
194
|
+
`carries it, so the job queues until the 24-hour timeout with nothing to read`,
|
|
195
|
+
);
|
|
196
|
+
assert.equal(
|
|
197
|
+
resolved,
|
|
198
|
+
fallback,
|
|
199
|
+
`${file}:${line}: an empty runner must land on the input's documented default`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
130
204
|
test(`${file}: every runs-on site resolves identically`, () => {
|
|
131
205
|
// One workflow must not drift into two dialects of the same decision.
|
|
132
206
|
const rendered = sites.map(({ expr }) =>
|
|
@@ -136,6 +210,25 @@ for (const { file, text } of WORKFLOWS) {
|
|
|
136
210
|
});
|
|
137
211
|
}
|
|
138
212
|
|
|
213
|
+
|
|
214
|
+
test("every runs-on expression across the seven workflows is byte-identical", () => {
|
|
215
|
+
// The per-workflow tests above each score one file, so a fix applied to six
|
|
216
|
+
// of the seven passes every one of them and ships the seventh still broken.
|
|
217
|
+
// This is the assertion a partial edit cannot survive: one decision, spelled
|
|
218
|
+
// one way, everywhere it is made.
|
|
219
|
+
const distinct = new Map();
|
|
220
|
+
for (const { file, text } of WORKFLOWS) {
|
|
221
|
+
for (const { line, expr } of runsOnExpressions(text)) {
|
|
222
|
+
if (!distinct.has(expr)) distinct.set(expr, []);
|
|
223
|
+
distinct.get(expr).push(`${file}:${line}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const report = [...distinct.entries()]
|
|
227
|
+
.map(([expr, at]) => `${expr} @ ${at.join(", ")}`)
|
|
228
|
+
.join("\n ");
|
|
229
|
+
assert.equal(distinct.size, 1, `runs-on expressions have drifted apart:\n ${report}`);
|
|
230
|
+
});
|
|
231
|
+
|
|
139
232
|
test("the documented array form resolves AND derives toolchain-cache 'false'", () => {
|
|
140
233
|
// The coupling the gap report found: before this fix the only `runner` value
|
|
141
234
|
// that derived the correct cache posture was the one that never reached a
|
|
@@ -152,3 +245,131 @@ test("the documented array form resolves AND derives toolchain-cache 'false'", (
|
|
|
152
245
|
assert.ok(cache, "no `cache: ${{ … }}` value found at the setup-toolchain call site");
|
|
153
246
|
assert.equal(evaluate(cache[1].trim(), { runner, "toolchain-cache": "auto" }), "false");
|
|
154
247
|
});
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// The harden-runner egress-audit gate (`pr-quality.yml`)
|
|
251
|
+
//
|
|
252
|
+
// `runs-on` was not the only expression reading `inputs.runner`. Anything that
|
|
253
|
+
// branches on the runner class has to resolve the input the SAME way, or the
|
|
254
|
+
// job and the step disagree about which machine they are on. This section
|
|
255
|
+
// pins that agreement behaviourally — extract the real `if:` and run it.
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The `${{ … }}` body of the harden-runner step's `if:` gate.
|
|
260
|
+
*
|
|
261
|
+
* Keyed off the step, not off a line pattern that happens to contain
|
|
262
|
+
* `startsWith` — the point is to score whatever expression actually guards
|
|
263
|
+
* that step, including one a future edit spells differently.
|
|
264
|
+
*/
|
|
265
|
+
function hardenRunnerGate(text) {
|
|
266
|
+
const block = stepByName(text, "Harden runner (egress audit)");
|
|
267
|
+
assert.match(
|
|
268
|
+
block,
|
|
269
|
+
/uses: step-security\/harden-runner@/,
|
|
270
|
+
"the extracted block is not the harden-runner step",
|
|
271
|
+
);
|
|
272
|
+
const m = block.match(/^\s*if:\s*\$\{\{(.+)\}\}\s*$/m);
|
|
273
|
+
assert.ok(m, "the harden-runner step has no single-expression `if:` gate to score");
|
|
274
|
+
return m[1].trim();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* What `runs-on` resolves to for `runner`, as a hosted-ubuntu predicate.
|
|
279
|
+
*
|
|
280
|
+
* A single string label starting with `ubuntu-` is a GitHub-hosted ubuntu
|
|
281
|
+
* image — the one environment where harden-runner installs its own monitor.
|
|
282
|
+
* An ARRAY (the documented self-hosted form) is not, regardless of the labels
|
|
283
|
+
* inside it: harden-runner ships its agent in a self-hosted runner image, so
|
|
284
|
+
* the step is correctly a no-op there.
|
|
285
|
+
*/
|
|
286
|
+
function resolvesToHostedUbuntu(runsOnExpr, runner) {
|
|
287
|
+
const resolved = evaluate(runsOnExpr, { runner });
|
|
288
|
+
return typeof resolved === "string" && resolved.startsWith("ubuntu-");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const QUALITY_FILE = `${WORKFLOW_DIR}/pr-quality.yml`;
|
|
292
|
+
const QUALITY_TEXT = readFileSync(QUALITY_FILE, "utf8");
|
|
293
|
+
|
|
294
|
+
test("pr-quality.yml: the harden-runner gate reaches every tier through one anchor", () => {
|
|
295
|
+
// The gate is written once (`&harden-runner`) and aliased into the other
|
|
296
|
+
// tiers. A second literal copy could carry a stale expression that every
|
|
297
|
+
// behavioural assertion below would miss, because they score the anchor.
|
|
298
|
+
assert.match(QUALITY_TEXT, /^ {6}- &harden-runner$/m, "the harden-runner anchor is missing");
|
|
299
|
+
assert.ok(
|
|
300
|
+
(QUALITY_TEXT.match(/^ {6}- \*harden-runner$/gm) ?? []).length > 0,
|
|
301
|
+
"expected the harden-runner anchor to be aliased into the other tiers",
|
|
302
|
+
);
|
|
303
|
+
assert.equal(
|
|
304
|
+
(QUALITY_TEXT.match(/uses: step-security\/harden-runner@/g) ?? []).length,
|
|
305
|
+
1,
|
|
306
|
+
"expected exactly one harden-runner step — a second one would bypass the anchor",
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("pr-quality.yml: an empty runner keeps the egress audit ON", () => {
|
|
311
|
+
// The regression. `runner: ''` resolves to the hosted ubuntu-latest default
|
|
312
|
+
// at `runs-on` (#493), so the job DOES run on a GitHub-hosted machine — but
|
|
313
|
+
// a gate reading the raw input saw `startsWith('', 'ubuntu-')` → false and
|
|
314
|
+
// skipped. Nothing goes red when a step is skipped, so the egress baseline
|
|
315
|
+
// silently disappears for exactly the callers who never asked to opt out.
|
|
316
|
+
const gate = hardenRunnerGate(QUALITY_TEXT);
|
|
317
|
+
assert.equal(
|
|
318
|
+
evaluate(gate, { runner: "", "enable-harden-runner": true }),
|
|
319
|
+
true,
|
|
320
|
+
"an empty runner lands on hosted ubuntu-latest, so the egress audit must run there",
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("pr-quality.yml: the gate agrees with what runs-on resolves to", () => {
|
|
325
|
+
// The real contract, and the one that survives a respelling of either
|
|
326
|
+
// expression: the step runs precisely when the job is on a hosted ubuntu
|
|
327
|
+
// image. Scoring both sides against the same input is what makes a future
|
|
328
|
+
// change to one of them fail here instead of shipping a silent divergence.
|
|
329
|
+
const gate = hardenRunnerGate(QUALITY_TEXT);
|
|
330
|
+
const [runsOn] = runsOnExpressions(QUALITY_TEXT);
|
|
331
|
+
for (const runner of [
|
|
332
|
+
"",
|
|
333
|
+
"ubuntu-latest",
|
|
334
|
+
"ubuntu-24.04",
|
|
335
|
+
"ubuntu-22.04",
|
|
336
|
+
"ubuntu-latest-8-cores",
|
|
337
|
+
"macos-14",
|
|
338
|
+
"windows-latest",
|
|
339
|
+
'["self-hosted","beestera-runner"]',
|
|
340
|
+
'["ubuntu-latest"]',
|
|
341
|
+
]) {
|
|
342
|
+
assert.equal(
|
|
343
|
+
evaluate(gate, { runner, "enable-harden-runner": true }),
|
|
344
|
+
resolvesToHostedUbuntu(runsOn.expr, runner),
|
|
345
|
+
`runner ${JSON.stringify(runner)}: the gate and the resolved runs-on disagree ` +
|
|
346
|
+
`about whether this job is on a GitHub-hosted ubuntu image`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("pr-quality.yml: `enable-harden-runner: false` still opts out everywhere", () => {
|
|
352
|
+
// The documented escape hatch. A fallback added to the runner half of the
|
|
353
|
+
// gate must not make the boolean half unreachable — `false && …` yields
|
|
354
|
+
// `false`, but only if the operands stayed in that order.
|
|
355
|
+
const gate = hardenRunnerGate(QUALITY_TEXT);
|
|
356
|
+
for (const runner of ["", "ubuntu-latest", "ubuntu-24.04", '["self-hosted","x"]']) {
|
|
357
|
+
assert.equal(
|
|
358
|
+
evaluate(gate, { runner, "enable-harden-runner": false }),
|
|
359
|
+
false,
|
|
360
|
+
`runner ${JSON.stringify(runner)}: opting out must win regardless of the runner`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("pr-quality.yml: the toolchain-cache derivation reads an empty runner as hosted", () => {
|
|
366
|
+
// The sibling `inputs.runner` reader, checked rather than assumed. It is
|
|
367
|
+
// correct as written for `runner: ''` — but only incidentally, because
|
|
368
|
+
// `contains('', 'self-hosted')` is false and the derivation is
|
|
369
|
+
// self-hosted-side. Pinning it here means a future inversion to a
|
|
370
|
+
// hosted-side test (`contains(runner, 'ubuntu')`) trips instead of quietly
|
|
371
|
+
// disabling the cache for every empty-runner caller.
|
|
372
|
+
const cache = QUALITY_TEXT.match(/^\s*cache:\s*\$\{\{(.+)\}\}\s*$/m);
|
|
373
|
+
assert.ok(cache, "no `cache: ${{ … }}` value found at the setup-toolchain call site");
|
|
374
|
+
assert.equal(evaluate(cache[1].trim(), { runner: "", "toolchain-cache": "auto" }), "true");
|
|
375
|
+
});
|