Repository navigation
Isolate the PyPI publish job from post-publish verification - #241
Merged
Merged
Conversation
The publish job held job-wide id-token: write and contents: write, and after publishing it checked out the source and ran the check_* scripts, including a pip install --dry-run against PyPI, while OIDC was still mintable. A step is not a boundary; a job is. - publish: downloads the build artifact by id with digest enforcement, validates the producer digest and identity, extracts it and uploads it. Permissions are actions: read and id-token: write only. No checkout, no project code, no build or install. - verify (needs build, publish): contents: read, no id-token. Checks out the same commit without persisted credentials and runs the same two checks in the same order (exact PyPI JSON, then the project surface and supported installs). A mismatch fails the workflow. - github-release (needs build, verify): contents: write. Runs only the preinstalled GitHub CLI, with GH_REPO in place of a checkout, and is created only after verification passes, as before. tests/test_publish_workflow_isolation.py pins the contract: only the publish job can mint OIDC, it uses only the pinned download and PyPI publish actions and runs no python, pip, uv, twine, build or project script, verification follows publication without OIDC, and the release job runs no third-party action or project code. Fixes #240 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The central action policy preflight failed on b8917a3: - A privileged artifact consumer must open with the policy's fixed, credential-free checkout (fetch-depth 0, persist-credentials false, ref github.sha) before the digest-bound download. The publish job keeps that exact step; nothing in the job executes the checkout. - A privileged workflow_dispatch job must not run outside main. The github-release job (contents: write) now has the publish job's condition: a tag push, or a publish dispatch from main. The contract test pins both. The local run of the central qualification_policy.py validate now passes. Refs #240 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Member
Author
|
General interim review (Security unavailable): exact head The diff is 2 files (publish.yml +34/−1, plus a new isolation contract test).
Non-blocking follow-up: the publish job's Fixes #240. Merging by General. |
rmcdaniel
marked this pull request as ready for review
November 4, 2026 00:17
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.



Fixes #240.
Problem
The
publishjob held job-wideid-token: writeandcontents: write. After publishing, it checked out the source and rancheck_release_metadata.pyandcheck_pypi_project_surface.py, which includes apip install --dry-runagainst PyPI, while a PyPI OIDC token could still be minted. A step is not a boundary; a job is.Change (
.github/workflows/publish.yml)buildis unchanged.publishbuildactions: read,id-token: write(environmentpypi)fetch-depth: 0,persist-credentials: false,ref: github.sha) that nothing executes. Then pinneddownload-artifactby artifact id withdigest-mismatch: error, the existing producer digest and identity validation, thetarextraction, and pinnedpypa/gh-action-pypi-publish. No project code, no build or install.verifybuild,publishcontents: readpersist-credentials: false), then the same two checks in the same order: exact PyPI JSON (--pypi-version), then the canonical project surface and supported installs. Both keep--attempts 30 --interval-seconds 10, and a mismatch fails the workflow.github-releasebuild,verify; sameifaspublish(tag push, or publish dispatch frommain)contents: writegh release view/createscript, withGH_REPOin place of a checkout. Nouses:steps and no project code. As before, the release is created only after verification passes.Behaviour preserved:
publishis skipped, soverifyandgithub-releaseare skipped too.publishno longer hascontents: write. Nothing it still runs used that permission.main; this matches both. A local run ofqualification_policy.py validatepasses.Contract test (
tests/test_publish_workflow_isolation.py, 15 cases)contents: read, and onlypublishhasid-token: write.publishhas exactlyactions: readandid-token: write, in environmentpypi, and needs onlybuild.publishmay run:download-artifactand PyPI publish actions;run:scripts are the digest validation and the extraction.python,pip,uv,twine,hatch,poetry,tox,nox,make,python -m build/pip, ascripts/file,setup.pyorpyproject.toml. Positive controls show the pattern catches each install and build shape.pypienvironment appear in no other job.verifyhascontents: read, needspublish, and runs the two checks in order with their retry settings, from a checkout without persisted credentials.github-releasehascontents: write, needsverify, haspublish's condition, uses no action, and runs only theghscript.main's workflow, 4 of the 15 cases fail.Local checks (Python 3.13)
ruff check src/ tests/: clean.mypy src/durable_workflow/: no issues.pass.qualification_policy.py validatefromdurable-workflow/.githubmain: passes. The first head,b8917a36, failed the hosted preflight on the two rules above.actionlintandzizmoraren't installed here. The workflow was parse-checked with PyYAML; hosted CI and the central action-policy check validate it further.The publish workflow was not dispatched.
🤖 Generated with Claude Code