Skip to content

ci: split build from publish, gate OIDC with environment#160

Merged
gavinsharp merged 6 commits into
mainfrom
gavinsharp/split-build-publish
Jun 26, 2026
Merged

ci: split build from publish, gate OIDC with environment#160
gavinsharp merged 6 commits into
mainfrom
gavinsharp/split-build-publish

Conversation

@gavinsharp

@gavinsharp gavinsharp commented May 27, 2026

Copy link
Copy Markdown
Contributor

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:

compile -> test -> tag -> build-artifact -> publish
  • build-artifact (new, no secrets): poetry build, then upload dist/ via actions/upload-artifact.
  • publish (rewritten): no checkout, no setup-python, no Poetry. Downloads the artifact, then runs pypa/gh-action-pypi-publish (upload-only). actions/download-artifact@v8 verifies the downloaded bundle against GitHub's recorded digest by default (digest-mismatch: error).
  • 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.
  • concurrency group keyed on the ref; cancels superseded runs on PR branches but never on main (a mid-flight cancel would orphan the tag from the publish).
  • Shared PYTHON_VERSION / POETRY_VERSION env vars replace the version strings previously duplicated across jobs (and compile now pins Python like the others).
  • All new uses: are SHA-pinned with trailing version comments (actions/upload-artifact@v7.0.1, actions/download-artifact@v8.0.1).
  • .github/workflows/ci.yml is in .fernignore, so Fern regen will not clobber this.

A bespoke SHA256SUMS build/verify step between the jobs was considered and dropped: it only defends against a GitHub-side artifact-store compromise, which is out of scope, and download-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:

  • Create the pypi-production GitHub Environment in repo settings, with a deployment-branch rule restricting it to refs/heads/main.
  • Update the PyPI Trusted Publisher binding for this repo to require environment name pypi-production.
  • Confirm branch protection is enabled on main (the refs/heads/main restriction 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

  • Merge to main with no version bump: tag job sets should_publish=false, build-artifact and publish are skipped.
  • Bump version in pyproject.toml, merge to main: tag creates the release, build-artifact produces and uploads python-dist, publish downloads 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-artifactpublish: Poetry builds and uploads dist/ as a short-lived artifact; publish only downloads that artifact (with digest verification) and runs gh-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-production on publish for Trusted Publisher / branch rules, and concurrency that cancels in-progress runs on non-main refs but not on main (to avoid orphaning a tag from a half-finished upload). Centralizes PYTHON_VERSION / POETRY_VERSION across jobs and bumps verify-openapi-spec / sync-fern-artifacts reusable 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.

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml
@gavinsharp gavinsharp force-pushed the gavinsharp/split-build-publish branch from e4fbcb2 to 66a97da Compare June 15, 2026 19:21
Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml
gavinsharp and others added 5 commits June 25, 2026 09:50
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>
@gavinsharp gavinsharp force-pushed the gavinsharp/split-build-publish branch from 0fa0b58 to 7069bfe Compare June 25, 2026 13:50
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread .github/workflows/ci.yml
@gavinsharp gavinsharp merged commit 8766449 into main Jun 26, 2026
7 checks passed
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.

2 participants