Skip to content

control: DO NOT MERGE — PF-013 positive controls for the PyPI publish rehearsal gate (#350) - #367

Closed
dean0x wants to merge 5 commits into
mainfrom
ci/release-surface-gate-control
Closed

control: DO NOT MERGE — PF-013 positive controls for the PyPI publish rehearsal gate (#350)#367
dean0x wants to merge 5 commits into
mainfrom
ci/release-surface-gate-control

Conversation

@dean0x

@dean0x dean0x commented Sep 6, 2026

Copy link
Copy Markdown
Owner

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.

…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
dean0x force-pushed the ci/release-surface-gate-control branch from 601c3c8 to fa62459 Compare September 6, 2026 22:34
@dean0x

dean0x commented Sep 6, 2026

Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant