test(ri-050): RI-1-002 publish-gate negative controls (#1275) (#1305)
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/publish Pipeline failed
This commit was merged in pull request #1305.
This commit is contained in:
@@ -9,7 +9,10 @@ import { STAGES } from './verify-release.mjs';
|
||||
// SDLC-D-034 checkout invariant: publication in .woodpecker/publish.yml is
|
||||
// bound to exact-commit terminal verification. This suite parses the real
|
||||
// pipeline files and fails red when the gate is bypassed, weakened, or drifts
|
||||
// out of sync with the canonical `pnpm verify:release` command.
|
||||
// out of sync with the canonical `pnpm verify:release` command. The negative
|
||||
// controls for pipeline DAG/bypass shapes live in
|
||||
// scripts/publish-gate-structure.test.mjs (RI-1-002); this file owns the
|
||||
// canonical-command composition controls.
|
||||
|
||||
// Reuse the monorepo's existing YAML parser (@mosaicstack/mosaic's direct
|
||||
// dependency) instead of adding a root dependency or vendoring a parser.
|
||||
@@ -219,13 +222,16 @@ steps:
|
||||
assert.throws(() => assertPublishGate(parseYaml(noIdentityPipeline)), /CI_COMMIT_SHA/);
|
||||
});
|
||||
|
||||
test('the canonical verify:release stages mirror the PR CI pipeline one-for-one', async () => {
|
||||
const ci = parseYaml(await readFile(ciYmlPath, 'utf8'));
|
||||
const canonical = Object.fromEntries(STAGES.map((stage) => [stage.name, stage.commands]));
|
||||
// The composition check: the canonical stage table must mirror the PR CI
|
||||
// pipeline's complete mandatory set. Parameterized by the stage list so the
|
||||
// subset negative control below can prove a dropped stage goes red (RI-1-002:
|
||||
// the canonical command cannot silently lose a check).
|
||||
function assertStagesMirrorCi(stages, ci) {
|
||||
const canonical = Object.fromEntries(stages.map((stage) => [stage.name, stage.commands]));
|
||||
|
||||
// The complete mandatory set, in gate order.
|
||||
assert.deepEqual(
|
||||
STAGES.map((stage) => stage.name),
|
||||
stages.map((stage) => stage.name),
|
||||
['sanitization', 'upgrade-guard', 'typecheck', 'lint', 'format', 'test', 'build'],
|
||||
);
|
||||
|
||||
@@ -269,6 +275,26 @@ test('the canonical verify:release stages mirror the PR CI pipeline one-for-one'
|
||||
`ci.yml test step must keep its pipeline-level prerequisite '${fragment}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test('the canonical verify:release stages mirror the PR CI pipeline one-for-one', async () => {
|
||||
const ci = parseYaml(await readFile(ciYmlPath, 'utf8'));
|
||||
assertStagesMirrorCi(STAGES, ci);
|
||||
});
|
||||
|
||||
test('a subset stage list fails the composition check — a dropped stage cannot pass silently', async () => {
|
||||
const ci = parseYaml(await readFile(ciYmlPath, 'utf8'));
|
||||
// Drop each stage one at a time: every stage is load-bearing, so every drop
|
||||
// must go red. If any drop went green, a refactor could silently delete a
|
||||
// mandatory check from the canonical command.
|
||||
for (const stage of STAGES) {
|
||||
const subset = STAGES.filter((entry) => entry.name !== stage.name);
|
||||
assert.throws(
|
||||
() => assertStagesMirrorCi(subset, ci),
|
||||
Error,
|
||||
`composition check must fail when the '${stage.name}' stage is dropped from the table`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the root package.json exposes verify:release as the canonical command', async () => {
|
||||
|
||||
Reference in New Issue
Block a user