ci: split build from publish, gate OIDC with environment#160
Merged
Conversation
3 tasks
9bdec86 to
c5af6ab
Compare
e4fbcb2 to
66a97da
Compare
Refactors ci.yml so the credential-bearing job runs only an upload
command against a pre-built, checksum-verified artifact. No
dependency installer or build toolchain executes in the same job as
the OIDC token.
Topology:
compile -> test -> tag -> build-artifact -> publish
- build-artifact (new, no secrets): poetry build, sha256sum the
resulting wheel + sdist, upload dist/ + SHA256SUMS via
actions/upload-artifact.
- publish (rewritten): no checkout, no setup-python, no poetry.
Downloads the artifact, verifies SHA256SUMS, then runs
pypa/gh-action-pypi-publish (an upload-only action).
- Workflow-root permissions: {} with per-job grants; publish gets
id-token: write + contents: read and nothing else.
- environment: pypi-production on publish, so the PyPI Trusted
Publisher binding and the environment's deployment rules can pin
OIDC minting to refs/heads/main.
- All new uses: pinned to commit SHA with a trailing version comment.
Follow-ups outside this PR (registry/GitHub side):
- Create the pypi-production environment in GitHub repo settings
with a deployment-branch rule restricting it to refs/heads/main.
- Update the PyPI Trusted Publisher binding to require environment
name pypi-production.
- Confirm branch protection on main so the ref restriction is
meaningful.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses PR review: bundling SHA256SUMS inside the python-dist artifact gave no integrity protection against the artifact-store compromise the comment claimed to guard against — an attacker who can tamper with the bundle rewrites the checksums to match. Move the checksums to the build-artifact job's output (sha256sums), which lives in the workflow run's output metadata — a separate subsystem from the artifact blob store. publish reads them via needs.build-artifact.outputs and verifies the downloaded wheel/sdist against them, so a swapped bundle is caught even if its store-side recorded digest were rewritten too. This layers on top of download-artifact v8's built-in verification, which already checks the downloaded bundle against GitHub's recorded digest (digest-mismatch: error by default). - build-artifact: SHA256SUMS no longer uploaded; emitted as a multi-line job output. Upload is now path: dist/ only. - publish: download into dist/ (the artifact root changed now that SHA256SUMS is gone), verify against the job output via env. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Guarding against a GitHub-side artifact-store compromise is overkill and out of step with the sibling SDK workflows. download-artifact@v8 already verifies the downloaded bundle against GitHub's recorded digest by default (digest-mismatch: error), which covers the realistic threat (transit corruption / tampering). The core win of this PR — the credential-bearing publish job runs no build toolchain — is unchanged. Removes the Compute checksums / Verify checksums steps and the sha256sums job output added earlier. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0fa0b58 to
7069bfe
Compare
Keeps the sdk-shared-actions references in lockstep with the verify-openapi-spec bump in ci.yml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5a2edb3. Configure here.
rossmpowell
approved these changes
Jun 26, 2026
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.

Summary
Implements step 2 of the publish-hardening plan (Python SDK). Splits the credential-bearing job from the build toolchain so no dependency installer ever runs in the same job as the PyPI OIDC token.
New topology:
poetry build, then uploaddist/viaactions/upload-artifact.setup-python, no Poetry. Downloads the artifact, then runspypa/gh-action-pypi-publish(upload-only).actions/download-artifact@v8verifies the downloaded bundle against GitHub's recorded digest by default (digest-mismatch: error).permissions: {}with per-job grants.publishgetsid-token: write+contents: readand nothing else.environment: pypi-productiononpublish— so the PyPI Trusted Publisher binding and the environment's deployment rules can pin OIDC minting torefs/heads/main.concurrencygroup keyed on the ref; cancels superseded runs on PR branches but never onmain(a mid-flight cancel would orphan the tag from the publish).PYTHON_VERSION/POETRY_VERSIONenv vars replace the version strings previously duplicated across jobs (andcompilenow pins Python like the others).uses:are SHA-pinned with trailing version comments (actions/upload-artifact@v7.0.1,actions/download-artifact@v8.0.1)..github/workflows/ci.ymlis in.fernignore, so Fern regen will not clobber this.A bespoke
SHA256SUMSbuild/verify step between the jobs was considered and dropped: it only defends against a GitHub-side artifact-store compromise, which is out of scope, anddownload-artifact@v8's built-in digest check already covers the realistic threat (transit corruption / tampering).Required follow-up (outside this PR)
These are GitHub/PyPI configuration changes that must land for the new bindings to be enforced:
pypi-productionGitHub Environment in repo settings, with a deployment-branch rule restricting it torefs/heads/main.pypi-production.main(therefs/heads/mainrestriction is only as strong as who can push to main).Until the environment is created, GitHub will auto-create it with no protection rules, so this PR is safe to merge before the manual steps land — the security improvements just don't fully take effect until then.
Test plan
tagjob setsshould_publish=false,build-artifactandpublishare skipped.pyproject.toml, merge to main:tagcreates the release,build-artifactproduces and uploadspython-dist,publishdownloads it and publishes to PyPI.🤖 Generated with Claude Code
Note
Medium Risk
Changes the release/publish pipeline and PyPI OIDC wiring; misconfiguration or a failed artifact handoff could block or break publishes until GitHub environment and PyPI Trusted Publisher settings are aligned.
Overview
Hardens the main-branch PyPI release path by splitting what used to be a single publish job into
build-artifact→publish: Poetry builds and uploadsdist/as a short-lived artifact; publish only downloads that artifact (with digest verification) and runsgh-action-pypi-publish, with no checkout, Poetry, or dependency install in the OIDC-bearing job.Adds workflow-root
permissions: {}with per-job grants,environment: pypi-productionon publish for Trusted Publisher / branch rules, and concurrency that cancels in-progress runs on non-mainrefs but not onmain(to avoid orphaning a tag from a half-finished upload). CentralizesPYTHON_VERSION/POETRY_VERSIONacross jobs and bumpsverify-openapi-spec/sync-fern-artifactsreusable workflows to 1.0.3.Reviewed by Cursor Bugbot for commit 5a2edb3. Bugbot is set up for automated code reviews on this repo. Configure here.