105 lines
4.4 KiB
JavaScript
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/,
|
|
);
|
|
});
|