Files
stack/scripts/gate-wiring.test.mjs
T

105 lines
4.4 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const root = process.cwd();
const expectedTriggers = `when:
# PR + manual CI run on any branch — the pull_request pipeline is the merge gate.
# push CI is restricted to protected branches (main) so a feature-branch push no
# longer fires a redundant SECOND pipeline alongside its PR pipeline. This ~halves
# CI load on the storage-constrained runner with zero loss of gating (branch
# protection requires no push/ci status context; main still gets full push CI).
- event: [pull_request, manual]
- event: push
branch: main`;
const expectedGateStep = ` image: *node_image
# Woodpecker's shallow marker makes merge-base reject even present parents;
# full history is required for activation ancestry and manifest provenance.
commands:
- *enable_pnpm
- apk add --no-cache bubblewrap
- if [ -f .git/shallow ]; then git fetch --unshallow --no-tags origin; fi
- pnpm gate:verify
depends_on:
- install
- sanitization
- upgrade-guard`;
export function assertUnprivilegedGateStep(pipeline) {
assert.doesNotMatch(
pipeline,
/privileged/i,
'no pull-request pipeline step may declare privilege',
);
for (const line of pipeline.split('\n')) {
const candidate = line.trimStart().replace(/^-\s+/, '');
if (/^(?:["'!<].*|[A-Za-z_][A-Za-z0-9_-]*\s+):(?:\s|$)/.test(candidate)) {
assert.fail(`non-canonical or merged YAML key is forbidden: ${candidate}`);
}
}
const triggerMatches = [...pipeline.matchAll(/^when:\n([\s\S]*?)(?=\n\n)/gm)];
assert.equal(triggerMatches.length, 1, 'exactly one top-level trigger is required');
assert.equal(
`when:\n${triggerMatches[0][1].trimEnd()}`,
expectedTriggers,
'top-level triggers must match closed PR/main construction',
);
const matches = [
...pipeline.matchAll(/\n gate-verify:\n([\s\S]*?)(?=\n [a-z][a-z0-9-]+:|\nservices:|$)/g),
];
assert.equal(matches.length, 1, 'exactly one gate-verify step is required');
// Closed textual construction by design: accepting arbitrary YAML syntax here
// would require a duplicate-key-preserving parser. Exact equality rejects all
// extra keys, quoted/escaped key spellings, aliases, and mapping merges.
assert.equal(matches[0][1].trimEnd(), expectedGateStep, 'gate-verify step must match closed unprivileged construction');
}
test('package.json exposes the canonical gate:verify command', async () => {
const packageJson = JSON.parse(await readFile(`${root}/package.json`, 'utf8'));
assert.equal(packageJson.scripts['gate:verify'], 'node scripts/gate-verify.mjs');
});
test('Woodpecker runs the closed unprivileged gate construction on every pipeline', async () => {
const pipeline = await readFile(`${root}/.woodpecker/ci.yml`, 'utf8');
assertUnprivilegedGateStep(pipeline);
});
test('gate wiring rejects privilege syntax, merges, duplicate keys, and trigger narrowing', async () => {
const pipeline = await readFile(`${root}/.woodpecker/ci.yml`, 'utf8');
const additions = [
' privileged: *enabled\n',
' "privileged": true\n',
" 'privileged': true\n",
' privileged : true\n',
' "priv\\u0069leged": true\n',
' <<: *privileged-step\n',
' "<<": *privileged-step\n',
];
for (const addition of additions) {
const changed = pipeline.replace(' gate-verify:\n image:', ` gate-verify:\n${addition} image:`);
assert.throws(() => assertUnprivilegedGateStep(changed));
}
const privilegedInstall = pipeline.replace(
' install:\n image:',
' install:\n privileged: true\n image:',
);
const duplicate = `${pipeline}\n gate-verify:\n image: *node_image\n`;
const noPullRequest = pipeline.replace(
' - event: [pull_request, manual]',
' - event: manual',
);
const filteredPullRequest = pipeline.replace(
' - event: [pull_request, manual]',
' - event: [pull_request, manual]\n path: [scripts/**]',
);
assert.throws(() => assertUnprivilegedGateStep(privilegedInstall), /privilege/i);
assert.throws(() => assertUnprivilegedGateStep(duplicate), /exactly one gate-verify/);
assert.throws(() => assertUnprivilegedGateStep(noPullRequest), /closed PR\/main construction/);
assert.throws(
() => assertUnprivilegedGateStep(filteredPullRequest),
/closed PR\/main construction/,
);
});