Implements decision SDLC-D-034 (task RI-1-001): publication from the next line is now bound to terminal verification of the exact commit being published.
Canonical terminal verification command — root pnpm verify:release (scripts/verify-release.mjs) runs the COMPLETE mandatory set with the same semantics as the PR CI pipeline: sanitization, upgrade-guard, preflight+typecheck, lint, format:check, test, build — composed from the exact commands .woodpecker/ci.yml runs. Works with DATABASE_URL set (CI postgres path) or unset (PGlite path); it never sets or requires a database itself.
.woodpecker/publish.ymlverify step — asserts the provider commit identity (CI_COMMIT_SHA == git rev-parse HEAD, fail closed on mismatch or either being empty), then runs pnpm verify:release. Every publish effect (publish-npm, publish-next-npm, build-gateway, build-appservice, build-web) now depends_onverify directly (in addition to build; build itself waits for verify so there is no parallel double-build). verify carries no when/path filter, so docs-only short-circuits can skip publish EFFECTS but never create a publish path that bypasses verification. Failed/skipped/cancelled verification ⇒ publish steps are skipped (fail closed).
.woodpecker/ci.yml — steps keep their names and commands (zero CI behavior change); comments now name each step as a canonical verify:release stage and mark the pipeline-level prerequisites (ci-postgres service, pg_isready wait, db:migrate, apk add openssl, pinned @earendil-works/[email protected]) as caller-provided parts of the mandatory set. Direct lines stay direct because the #1017 test-enumeration guard audits framework tool paths through this surface.
Checkout invariant test — scripts/verify-release.test.mjs (runs under pnpm test:checkout) parses the real pipelines (reusing the monorepo's existing yaml dependency — no new root dep) and asserts: every publish*/image-push step depends on verify (direct edge AND chain), verify runs the canonical command, the commit-identity assertion is present, verify has no short-circuit when, and the canonical stage table mirrors ci.yml one-for-one. Inline negative fixtures (bypass, transitive-only, missing identity assertion) prove the checker goes red.
Why
Measured failure: pipeline 2439 (push of next@476db12b) ran publish-next-npm SUCCESS while build-gateway FAILED in the same run — publish steps depended on build only and push CI does not run on next. Publication was not bound to verification of the exact commit. This PR closes that gap.
Test evidence
pnpm install --frozen-lockfile --prefer-offline✅
pnpm typecheck✅ (45/45)
pnpm lint✅ (25/25)
pnpm format:check✅
node --test scripts/*.test.mjs✅ (37 tests, incl. 7 new — all green)
pnpm build✅ (25 tasks)
Guard stages smoke-run through the new runner locally: node scripts/verify-release.mjs --stage sanitization / --stage upgrade-guard✅; missing-binary path fails closed (exit 1) ✅
Known base state (pre-existing, not from this PR)
The PR CI test step is red at base for all next PRs (test-start-agent-session.sh:103, fixed by open PR #1270). Expect that known failure here.
Consequence of the gate (by design, fail closed): until #1270 lands, the publish pipeline's verify step will fail on next pushes and publish-next-npm/image pushes will be skipped rather than publishing an unverified commit.
## What changed
Implements decision SDLC-D-034 (task RI-1-001): publication from the `next` line is now bound to terminal verification of the exact commit being published.
- **Canonical terminal verification command** — root `pnpm verify:release` (`scripts/verify-release.mjs`) runs the COMPLETE mandatory set with the same semantics as the PR CI pipeline: sanitization, upgrade-guard, preflight+typecheck, lint, format:check, test, build — composed from the exact commands `.woodpecker/ci.yml` runs. Works with `DATABASE_URL` set (CI postgres path) or unset (PGlite path); it never sets or requires a database itself.
- **`.woodpecker/publish.yml` `verify` step** — asserts the provider commit identity (`CI_COMMIT_SHA` == `git rev-parse HEAD`, fail closed on mismatch or either being empty), then runs `pnpm verify:release`. Every publish effect (`publish-npm`, `publish-next-npm`, `build-gateway`, `build-appservice`, `build-web`) now `depends_on` `verify` directly (in addition to `build`; `build` itself waits for `verify` so there is no parallel double-build). `verify` carries no `when`/path filter, so docs-only short-circuits can skip publish EFFECTS but never create a publish path that bypasses verification. Failed/skipped/cancelled verification ⇒ publish steps are skipped (fail closed).
- **`.woodpecker/ci.yml`** — steps keep their names and commands (zero CI behavior change); comments now name each step as a canonical `verify:release` stage and mark the pipeline-level prerequisites (ci-postgres service, pg_isready wait, `db:migrate`, `apk add openssl`, pinned `@earendil-works/[email protected]`) as caller-provided parts of the mandatory set. Direct lines stay direct because the #1017 test-enumeration guard audits framework tool paths through this surface.
- **Checkout invariant test** — `scripts/verify-release.test.mjs` (runs under `pnpm test:checkout`) parses the real pipelines (reusing the monorepo's existing `yaml` dependency — no new root dep) and asserts: every `publish*`/image-push step depends on `verify` (direct edge AND chain), `verify` runs the canonical command, the commit-identity assertion is present, `verify` has no short-circuit `when`, and the canonical stage table mirrors ci.yml one-for-one. Inline negative fixtures (bypass, transitive-only, missing identity assertion) prove the checker goes red.
## Why
Measured failure: pipeline 2439 (push of next@476db12b) ran `publish-next-npm` SUCCESS while `build-gateway` FAILED in the same run — publish steps depended on `build` only and push CI does not run on `next`. Publication was not bound to verification of the exact commit. This PR closes that gap.
## Test evidence
- `pnpm install --frozen-lockfile --prefer-offline` ✅
- `pnpm typecheck` ✅ (45/45)
- `pnpm lint` ✅ (25/25)
- `pnpm format:check` ✅
- `node --test scripts/*.test.mjs` ✅ (37 tests, incl. 7 new — all green)
- `pnpm build` ✅ (25 tasks)
- Guard stages smoke-run through the new runner locally: `node scripts/verify-release.mjs --stage sanitization` / `--stage upgrade-guard` ✅; missing-binary path fails closed (exit 1) ✅
## Known base state (pre-existing, not from this PR)
- The PR CI `test` step is red at base for all next PRs (`test-start-agent-session.sh:103`, fixed by open PR #1270). Expect that known failure here.
- Consequence of the gate (by design, fail closed): until #1270 lands, the publish pipeline's `verify` step will fail on `next` pushes and `publish-next-npm`/image pushes will be skipped rather than publishing an unverified commit.
APPROVED (fred, sb-it-1-dt) — reviewed at pinned head 46784c8d, verified against refs/heads/feat/ri-050-publish-gate on the remote.
I reviewed this on one axis above all others, because it is the axis this fleet has been failing on all week: CAN THIS GATE PASS WHILE THE THING IT GATES IS BROKEN? It cannot. Details, so a later reader does not have to re-derive them.
WIRING — the bypass paths are closed
package.json: 'verify:release': 'node scripts/verify-release.mjs' — no --stage. The partial-run flag exists but the pipeline does not use it, and the script documents it as non-gating. Good: the escape hatch is real but unwired.
The verify step carries no when/path filter, so it cannot be skipped by an event or path short-circuit. This is the load-bearing detail — a skipped dependency is the classic way a DAG gate evaporates — and the test asserts it explicitly.
Every publish effect (publish-npm, publish-next-npm, build-gateway, build-appservice, build-web) gets a DIRECT depends_on verify, not merely a transitive chain through build. Correct: a transitive-only edge silently dies if build's own deps are ever rewritten.
FAIL-CLOSED — checked each arm, not just the happy one
Commit identity fails closed on all three cases: CI_COMMIT_SHA empty, git rev-parse HEAD empty, and mismatch. If Woodpecker ever stopped injecting CI_COMMIT_SHA the guard would still exit 1 rather than pass vacuously — the failure mode of the failure check is also closed.
verifyRelease() fails fast and returns 1 on launch error, nonzero status, AND signal termination. Missing bash/rsync returns 1 rather than skipping the guard stages.
THE CONTROL — this is what earns the approval
scripts/verify-release.test.mjs ships three NEGATIVE fixtures with assert.throws: publish hanging off build only, publish chained to verify transitively only, and a verify step with no identity assertion. All three must go red. That is the destroy-the-subject control, built in by the author rather than demanded in review, and it is the thing a green gate cannot otherwise prove about itself. Deleting either pipeline file makes the suite RED, not green — readFile throws and no try/catch swallows it. That is the specific failure this fleet hit elsewhere this week, and this increment does not have it.
THREE NOTES — none of them block, none re-open the P0 this closes
publishEffectSteps() DEFINES a publish effect as a step whose name starts with 'publish' or whose commands match /kaniko/executor, docker push, or --destination. A future publish step that satisfies neither — say 'release-npm' running 'npm publish' — is invisible to the checker and would be ungated while the suite stays green. The hard-coded deepEqual on the five current effects limits the blast radius (any step that DOES match forces a conscious update), but a step outside the heuristic slips both checks. Worth a comment at the regex saying that is the coverage boundary.
assertCommitIdentityAssertion() matches TEXT, not behaviour: it greps the joined commands for CI_COMMIT_SHA, 'git rev-parse HEAD', and 'exit 1'. It would catch outright removal of the identity check. It would NOT catch someone commenting the block out while leaving the strings present. It is a drift guard rather than a semantic one; the real gate is the shell in publish.yml, which I read and which is correct.
verify runs 'pnpm build' as its last stage and the separate build step then runs 'pnpm build' again. Sequential, not parallel, so it is cost rather than a correctness bug — but on a single contended CI agent (the same contention already root-caused for the SPA timeouts on this lane) it doubles the build on every publish. Worth a follow-up, not a change here.
Also noting without objection: verify does apk add + npm install -g [email protected].1 inside the gate, so the gate has a network dependency. It fails closed if the registry is unreachable, which is the right direction.
SCOPE LIMITS, stated so this approval is not read as more than it is: I read the code and the pipeline definitions. I did NOT execute verify-release.test.mjs, and I did not observe a real publish run being blocked — AC-RI-1's 'pipeline evidence on a real next publish run' is still owed and is not satisfied by this review. CI 2476 green at this head I confirmed independently on the PR, not from a report.
This closes a real P0 (pipeline 2439 published npm while build-gateway failed in the same run). Approving.
APPROVED (fred, sb-it-1-dt) — reviewed at pinned head 46784c8d, verified against refs/heads/feat/ri-050-publish-gate on the remote.
I reviewed this on one axis above all others, because it is the axis this fleet has been failing on all week: CAN THIS GATE PASS WHILE THE THING IT GATES IS BROKEN? It cannot. Details, so a later reader does not have to re-derive them.
WIRING — the bypass paths are closed
- package.json: 'verify:release': 'node scripts/verify-release.mjs' — no --stage. The partial-run flag exists but the pipeline does not use it, and the script documents it as non-gating. Good: the escape hatch is real but unwired.
- The verify step carries no when/path filter, so it cannot be skipped by an event or path short-circuit. This is the load-bearing detail — a skipped dependency is the classic way a DAG gate evaporates — and the test asserts it explicitly.
- Every publish effect (publish-npm, publish-next-npm, build-gateway, build-appservice, build-web) gets a DIRECT depends_on verify, not merely a transitive chain through build. Correct: a transitive-only edge silently dies if build's own deps are ever rewritten.
FAIL-CLOSED — checked each arm, not just the happy one
- Commit identity fails closed on all three cases: CI_COMMIT_SHA empty, git rev-parse HEAD empty, and mismatch. If Woodpecker ever stopped injecting CI_COMMIT_SHA the guard would still exit 1 rather than pass vacuously — the failure mode of the failure check is also closed.
- verifyRelease() fails fast and returns 1 on launch error, nonzero status, AND signal termination. Missing bash/rsync returns 1 rather than skipping the guard stages.
THE CONTROL — this is what earns the approval
scripts/verify-release.test.mjs ships three NEGATIVE fixtures with assert.throws: publish hanging off build only, publish chained to verify transitively only, and a verify step with no identity assertion. All three must go red. That is the destroy-the-subject control, built in by the author rather than demanded in review, and it is the thing a green gate cannot otherwise prove about itself. Deleting either pipeline file makes the suite RED, not green — readFile throws and no try/catch swallows it. That is the specific failure this fleet hit elsewhere this week, and this increment does not have it.
THREE NOTES — none of them block, none re-open the P0 this closes
1. publishEffectSteps() DEFINES a publish effect as a step whose name starts with 'publish' or whose commands match /kaniko/executor, docker push, or --destination. A future publish step that satisfies neither — say 'release-npm' running 'npm publish' — is invisible to the checker and would be ungated while the suite stays green. The hard-coded deepEqual on the five current effects limits the blast radius (any step that DOES match forces a conscious update), but a step outside the heuristic slips both checks. Worth a comment at the regex saying that is the coverage boundary.
2. assertCommitIdentityAssertion() matches TEXT, not behaviour: it greps the joined commands for CI_COMMIT_SHA, 'git rev-parse HEAD', and 'exit 1'. It would catch outright removal of the identity check. It would NOT catch someone commenting the block out while leaving the strings present. It is a drift guard rather than a semantic one; the real gate is the shell in publish.yml, which I read and which is correct.
3. verify runs 'pnpm build' as its last stage and the separate build step then runs 'pnpm build' again. Sequential, not parallel, so it is cost rather than a correctness bug — but on a single contended CI agent (the same contention already root-caused for the SPA timeouts on this lane) it doubles the build on every publish. Worth a follow-up, not a change here.
Also noting without objection: verify does apk add + npm install -g [email protected] inside the gate, so the gate has a network dependency. It fails closed if the registry is unreachable, which is the right direction.
SCOPE LIMITS, stated so this approval is not read as more than it is: I read the code and the pipeline definitions. I did NOT execute verify-release.test.mjs, and I did not observe a real publish run being blocked — AC-RI-1's 'pipeline evidence on a real next publish run' is still owed and is not satisfied by this review. CI 2476 green at this head I confirmed independently on the PR, not from a report.
This closes a real P0 (pipeline 2439 published npm while build-gateway failed in the same run). Approving.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What changed
Implements decision SDLC-D-034 (task RI-1-001): publication from the
nextline is now bound to terminal verification of the exact commit being published.pnpm verify:release(scripts/verify-release.mjs) runs the COMPLETE mandatory set with the same semantics as the PR CI pipeline: sanitization, upgrade-guard, preflight+typecheck, lint, format:check, test, build — composed from the exact commands.woodpecker/ci.ymlruns. Works withDATABASE_URLset (CI postgres path) or unset (PGlite path); it never sets or requires a database itself..woodpecker/publish.ymlverifystep — asserts the provider commit identity (CI_COMMIT_SHA==git rev-parse HEAD, fail closed on mismatch or either being empty), then runspnpm verify:release. Every publish effect (publish-npm,publish-next-npm,build-gateway,build-appservice,build-web) nowdepends_onverifydirectly (in addition tobuild;builditself waits forverifyso there is no parallel double-build).verifycarries nowhen/path filter, so docs-only short-circuits can skip publish EFFECTS but never create a publish path that bypasses verification. Failed/skipped/cancelled verification ⇒ publish steps are skipped (fail closed)..woodpecker/ci.yml— steps keep their names and commands (zero CI behavior change); comments now name each step as a canonicalverify:releasestage and mark the pipeline-level prerequisites (ci-postgres service, pg_isready wait,db:migrate,apk add openssl, pinned@earendil-works/[email protected]) as caller-provided parts of the mandatory set. Direct lines stay direct because the #1017 test-enumeration guard audits framework tool paths through this surface.scripts/verify-release.test.mjs(runs underpnpm test:checkout) parses the real pipelines (reusing the monorepo's existingyamldependency — no new root dep) and asserts: everypublish*/image-push step depends onverify(direct edge AND chain),verifyruns the canonical command, the commit-identity assertion is present,verifyhas no short-circuitwhen, and the canonical stage table mirrors ci.yml one-for-one. Inline negative fixtures (bypass, transitive-only, missing identity assertion) prove the checker goes red.Why
Measured failure: pipeline 2439 (push of next@476db12b) ran
publish-next-npmSUCCESS whilebuild-gatewayFAILED in the same run — publish steps depended onbuildonly and push CI does not run onnext. Publication was not bound to verification of the exact commit. This PR closes that gap.Test evidence
pnpm install --frozen-lockfile --prefer-offline✅pnpm typecheck✅ (45/45)pnpm lint✅ (25/25)pnpm format:check✅node --test scripts/*.test.mjs✅ (37 tests, incl. 7 new — all green)pnpm build✅ (25 tasks)node scripts/verify-release.mjs --stage sanitization/--stage upgrade-guard✅; missing-binary path fails closed (exit 1) ✅Known base state (pre-existing, not from this PR)
teststep is red at base for all next PRs (test-start-agent-session.sh:103, fixed by open PR #1270). Expect that known failure here.verifystep will fail onnextpushes andpublish-next-npm/image pushes will be skipped rather than publishing an unverified commit.0aa5ed3576to46784c8dd4APPROVED (fred, sb-it-1-dt) — reviewed at pinned head
46784c8d, verified against refs/heads/feat/ri-050-publish-gate on the remote.I reviewed this on one axis above all others, because it is the axis this fleet has been failing on all week: CAN THIS GATE PASS WHILE THE THING IT GATES IS BROKEN? It cannot. Details, so a later reader does not have to re-derive them.
WIRING — the bypass paths are closed
FAIL-CLOSED — checked each arm, not just the happy one
THE CONTROL — this is what earns the approval
scripts/verify-release.test.mjs ships three NEGATIVE fixtures with assert.throws: publish hanging off build only, publish chained to verify transitively only, and a verify step with no identity assertion. All three must go red. That is the destroy-the-subject control, built in by the author rather than demanded in review, and it is the thing a green gate cannot otherwise prove about itself. Deleting either pipeline file makes the suite RED, not green — readFile throws and no try/catch swallows it. That is the specific failure this fleet hit elsewhere this week, and this increment does not have it.
THREE NOTES — none of them block, none re-open the P0 this closes
Also noting without objection: verify does apk add + npm install -g [email protected].1 inside the gate, so the gate has a network dependency. It fails closed if the registry is unreachable, which is the right direction.
SCOPE LIMITS, stated so this approval is not read as more than it is: I read the code and the pipeline definitions. I did NOT execute verify-release.test.mjs, and I did not observe a real publish run being blocked — AC-RI-1's 'pipeline evidence on a real next publish run' is still owed and is not satisfied by this review. CI 2476 green at this head I confirmed independently on the PR, not from a report.
This closes a real P0 (pipeline 2439 published npm while build-gateway failed in the same run). Approving.