control: DO NOT MERGE — PF-013 positive controls for the PyPI publish rehearsal gate (#350) - #367
Closed
dean0x wants to merge 5 commits into
Closed
control: DO NOT MERGE — PF-013 positive controls for the PyPI publish rehearsal gate (#350)#367dean0x wants to merge 5 commits into
dean0x wants to merge 5 commits into
Conversation
…I publish; add opt-in TestPyPI leg (#342, #350) - pull_request trigger on 5 paths: release.yml, .github/actions/**, crates/mds-napi/**, crates/mds-python/**, scripts/verify-napi-names.mjs - rehearse-publish-python job: dry-run OIDC exchange, unguarded (PF-039), blocks publish-crates - publish-testpypi job: opt-in via workflow_dispatch testpypi:true, in TIER_B_EXPECTED_SKIPPED - version-gate: GHCR manifest probe for pypa/gh-action-pypi-publish pin (PF-040); step-level if: guard on CI-history step so version-gate runs (not skips) on PRs (ADR-013 amendment) - verify-pr-checks.mjs: RELEASE_SURFACE/RELEASE_SURFACE_CONTEXTS/matchesReleaseSurface exports; D-PR6 block requires Rehearse PyPI publish (no upload) on release-surface PRs; bounded PR-files pagination with graceful degradation - 13 new spec tests (S5-S13 in release-auth-probe.spec.mjs, D-PR5f + D-PR6a-g in verify-pr-checks.spec.mjs); Snyk: security/snyk (dean0x) is the scan of record (Snyk MCP unavailable this session)
The D-PR6 changed-files fetch was an inline block scope with four mutable
let variables (filesFailed, filesPagError, filesPage, collected) and a
confusing name collision — the loop data variable was also named `page`,
shadowing the loop iterator `filesPage`.
Extract fetchChangedFiles() following the same Result-return pattern as
fetchCheckRuns() and fetchStatuses(): returns { ok: true, files } on
success, { ok: 'skip', notice } on API error (graceful degradation), and
{ ok: false, exitCode: 2, message } on pagination overflow or non-vacuity
mismatch. main() dispatches on filesResult.ok with explicit equality checks.
No behaviour change. 172/0 pass/fail.
…l closed on the PR-files API P0 (Functionality/Security, PF-039): rehearse-publish-python invoked `pypa/gh-action-pypi-publish@v1.14.2` with `dry-run: true`. That input does not exist in v1.14.2 — the runner logged "Unexpected input(s) 'dry-run'", the action ignored it and performed a REAL upload to https://upload.pypi.org/legacy/ from a pull_request (run 34060146952). It failed only because the workspace version (0.4.2) was already on PyPI; on a version-bump PR — which touches crates/mds-python/Cargo.toml and therefore matches the release-surface paths filter — the upload would have SUCCEEDED, publishing an unreleased version from an unmerged branch. pypi.org still lists 0.4.2 only; nothing landed. The action has no no-upload mode, so the rehearsal now REPRODUCES what it does instead of calling it: pin shape, GHCR manifest, docker pull, and `twine check` run from the pinned image with `--network none --entrypoint twine`. Each gate carries a positive control (PF-013): the v0.4.1 annotated-tag-object SHA must be rejected, a ref that cannot exist must not resolve or pull, and a corrupt wheel must fail twine. The job is denied id-token, so it holds no credential to upload with even if a step is re-introduced. The duplicate GHCR probe in version-gate is removed — publish-crates needs the rehearsal, so a bad pin still aborts before the irreversible crates.io write (PF-023, PF-040). P1 (Error Handling, PF-013): fetchChangedFiles returned `{ ok: 'skip' }` on an API error and main() then skipped the release-surface presence check — a transient 500 would let a release-surface PR pass the merge gate with no release check-runs. It now fails closed (exit 2), matching fetchCheckRuns/fetchStatuses/ fetchRequiredContexts; the tri-state is gone. Only the exported pure function's `changedFiles === undefined` caller-path still skips, with a notice. P1 (Functionality, ADR-013 amendment): the credential and PyPI OIDC probes are skipped at STEP level on pull_request. Fork and Dependabot PRs receive no repository secrets and no id-token, so both probes hard-failed version-gate — on exactly the Dependabot-bump PR class #350 exists to catch (PF-039 amendment 2026-09-05), and a version-gate that cannot reach success hard-fails the mandatory pre-merge verifier, whose natural workaround is bypassing it (PF-017). P1 (Tests): S14 pins the invocation to publish-python/publish-testpypi only (with a planted positive control and a commented-out counter-control), S15 the absent id-token, S16 PIN_REF/uses drift, S17 the four gates and their controls. S8 follows the probe to the rehearsal. S10 now compares on.pull_request.paths to RELEASE_SURFACE as SETS — the invariant the production comment already claimed but nothing enforced. S13 gains a positive control. D-PR6h drives the fail-closed path end-to-end through main() (verified failing against the old behaviour), D-PR6i pins the prefix/exact-match boundaries, D-PR6j pins all-must-pass for duplicate names. S18 pins every RELEASE_SURFACE_CONTEXTS name to a real job name: in release.yml — a renamed job would otherwise make the verifier demand a check-run that can never appear, hard-failing every release-surface PR (ADR-013 amendment, PF-017). npm run test:gates: 183 tests, 183 pass, 0 fail (was 172).
… probes, fail closed on fork/Dependabot PRs, decouple the rehearsal from the napi matrix, harden gate controls (#342, #350) M1: Remove step-level if: from credential and PyPI OIDC probe steps in version-gate (both now run on every event, including pull_request). Remove the notice step that announced the skipping. M2: Add IS_FORK env + fail-closed block before the npm-token guard. Fork and Dependabot pull_request events receive no repository secrets and no id-token: write; the probe exits 1 with an actionable error pointing to `gh workflow run release.yml --ref <branch>` as the remedy. Removes the now-incorrect NOTE suffix on the NPM_TOKEN error. (Applying ADR-013) M3: rehearse-publish-python: needs [build-python] only (decoupled from the napi matrix); job-level if conditioned on build-python success. M4+M5+M6: publish-testpypi: needs [build-python, rehearse-publish-python]; if guarded by workflow_dispatch + inputs.testpypi + both upstreams; new "Assert all 8 Python distributions are present" step before publish. M7: Gate 1 — add dc37677b2e1c63e2034f94d8a5b11f265b73ba33 (v1.14.2 commit SHA) as second positive control. Update error message to explain why GHCR has images for commit shas AND release tags but never annotated tag objects. Fix publish-python comment (no longer a blanket "GHCR only publishes images for tagged releases" claim). M8: publish-python: add rehearse-publish-python to needs and if condition. M9/S6: rehearse-publish-python job-level if now tested as a non-tag-guarded condition on build-python success (was: asserting if is null). M10a/b/c: Three new ADR-013 graph tests — cargo-publish jobs transitively need rehearse-publish-python; publish-crates does NOT transitively need publish-testpypi; guarded job names exactly equal TIER_B_EXPECTED_SKIPPED. M11: Gate 2 — GHCR probe now writes body to /tmp/ghcr-body.json; positive control checks HTTP 404 AND MANIFEST_UNKNOWN body text (not just non-200). M12: Gate 3 — positive control uses grep for "manifest unknown" rather than bare exit code; docker pull retries up to 3 times with sleep 10 between attempts. M13: Extend file header comment to describe the paths: list as the release surface. M14: RELEASING.md — update probes description (run on all events, fail closed on forks); TestPyPI publisher expiry note; update "What happens after tagging" already-applied items cross-checked. M15: CHANGELOG.md [Unreleased] entry updated to reflect aligned behaviour. M16: rename D-PR6 presence-check to D-PR7 (D-PR6 stays for Exit codes). M17: D-PR5a test updated to "five publish names" (was "four"). P: Fix empty GitHub expression ${{ }} in Gate 3 bash comment — GitHub's expression pre-processor is not comment-aware; it raises "An expression was expected" (run 34061583304, 0 jobs). Rewrite comment to use plain prose. Spec S19 guards against regression. Applying PF-013 (positive controls), PF-039 (unguarded rehearsal), PF-040 (release-tag pin), ADR-013 (three-place rule: guarded jobs, TIER_B_EXPECTED_SKIPPED, verifier required contexts).
…99 to prove the GHCR manifest probe fails closed (#350, PF-013)
dean0x
force-pushed
the
ci/release-surface-gate-control
branch
from
September 6, 2026 22:34
601c3c8 to
fa62459
Compare
Owner
Author
|
Controls complete: B failed at the shape gate (S16 in version-gate's test:gates caught the commit-sha format before the rehearsal ran — release.yml version-gate includes the same spec suite as ci.yml; rehearsal skipped rather than failed), A failed at the GHCR probe after its positive control passed (Gate 2/4, GHCR 404 for v9.99.99); publish-crates skipped in both. Closing unmerged; run ids in PR #366. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Temporary draft PR. Exercises the rehearsal job's gates with deliberately bad pins. Control B = commit-sha pin (shape gate must fail). Control A = @v9.99.99 (GHCR probe must fail). Closed unmerged when done. Not related to any issue; do not merge.