Implement gated release pipeline with CI enforcement - #18
Conversation
New `release.yml`: tag-push (`v*`) builds wheels for Linux x86_64/aarch64, macOS x86_64/aarch64, and Windows x64 across CPython 3.11/3.12/3.13, plus an sdist. The `publish` job uploads to PyPI via OIDC trusted publishing and uses `skip-existing` so re-runs are idempotent. The `gate` job rejects any tag whose SHA isn't the current tip of `release`. Combined with branch protection requiring `ci` to pass on `release`, that turns "tip of release" into a hard guarantee that full CI (Tier 1 + Tier 2) ran on the exact SHA being released — so the release workflow doesn't re-run tests itself. Add a `release-source-gate` job to `ci.yml` that fails any PR targeting `release` whose head ref isn't `main`. GitHub branch protection has no "restrict source branch" setting, so we enforce it as a required check. `RELEASING.md` documents guarantees (branch protection rules, PyPI trusted publisher) and the per-release procedure, plus recovery from a lost tag race.
There was a problem hiding this comment.
Pull request overview
This PR introduces a gated release pipeline that constrains how the release branch is updated and how PyPI releases are built/published, plus documentation describing the new process and guarantees.
Changes:
- Adds a
release-source-gateCI job to only allow PRs intoreleasefrommain. - Introduces a
release.ymlworkflow to gate tags to the tip ofrelease, build wheels/sdist, and publish to PyPI via OIDC trusted publishing. - Adds
RELEASING.mddocumenting the end-to-end release procedure and troubleshooting steps.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
RELEASING.md |
Documents the enforced release flow, guarantees, and common failure modes. |
.github/workflows/release.yml |
Implements tag-to-release-tip gating, multi-OS wheel builds, sdist build, and PyPI publish via OIDC. |
.github/workflows/ci.yml |
Adds a CI enforcement job to restrict the source branch for PRs targeting release. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - name: PRs to release must come from main | ||
| run: | | ||
| if [ "$GITHUB_HEAD_REF" != "main" ]; then | ||
| echo "::error::PRs to release must come from main, got '$GITHUB_HEAD_REF'." |
There was a problem hiding this comment.
release-source-gate only checks $GITHUB_HEAD_REF == "main". A PR from a fork whose branch is also named main will pass this gate even though it did not originate from this repo’s main. If the intent is to ensure the source is this repository’s main, also validate the head repo (e.g., github.event.pull_request.head.repo.full_name == github.repository) in addition to the branch name.
| - name: PRs to release must come from main | |
| run: | | |
| if [ "$GITHUB_HEAD_REF" != "main" ]; then | |
| echo "::error::PRs to release must come from main, got '$GITHUB_HEAD_REF'." | |
| - name: PRs to release must come from this repo's main | |
| env: | |
| HEAD_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }} | |
| THIS_REPO: ${{ github.repository }} | |
| run: | | |
| if [ "$GITHUB_HEAD_REF" != "main" ] || [ "$HEAD_REPO_FULL_NAME" != "$THIS_REPO" ]; then | |
| echo "::error::PRs to release must come from ${THIS_REPO}'s main branch, got '${HEAD_REPO_FULL_NAME}:${GITHUB_HEAD_REF}'." |
| target: [x86_64, aarch64] | ||
| python-version: ['3.11', '3.12', '3.13'] | ||
| include: | ||
| - target: x86_64 | ||
| runner: macos-13 | ||
| - target: aarch64 | ||
| runner: macos-14 |
There was a problem hiding this comment.
The macos job matrix is malformed: include entries only specify target/runner, which adds extra matrix rows without python-version, and the main 6 combinations won’t have runner set. As a result runs-on: ${{ matrix.runner }} will be empty/undefined for most rows and the job will fail to start. Define runner for every matrix combination (e.g., compute it from matrix.target, or include python-version in each include entry).
| target: [x86_64, aarch64] | |
| python-version: ['3.11', '3.12', '3.13'] | |
| include: | |
| - target: x86_64 | |
| runner: macos-13 | |
| - target: aarch64 | |
| runner: macos-14 | |
| include: | |
| - target: x86_64 | |
| python-version: '3.11' | |
| runner: macos-13 | |
| - target: x86_64 | |
| python-version: '3.12' | |
| runner: macos-13 | |
| - target: x86_64 | |
| python-version: '3.13' | |
| runner: macos-13 | |
| - target: aarch64 | |
| python-version: '3.11' | |
| runner: macos-14 | |
| - target: aarch64 | |
| python-version: '3.12' | |
| runner: macos-14 | |
| - target: aarch64 | |
| python-version: '3.13' | |
| runner: macos-14 |
| `release`. The `release.yml` workflow gates on tag-equals-release-tip, so | ||
| the SHA being released has demonstrably passed full CI. |
There was a problem hiding this comment.
This intro says release.yml “gates on tag-equals-release-tip”, but the workflow/job name is gate and the rest of this doc refers to the gate job. Please align the wording so maintainers can easily map the documentation to the actual workflow/job names.
| `release`. The `release.yml` workflow gates on tag-equals-release-tip, so | |
| the SHA being released has demonstrably passed full CI. | |
| `release`. The `release.yml` workflow's `gate` job ensures the tag points | |
| to the tip of `release`, so the SHA being released has demonstrably passed | |
| full CI. |
This pull request introduces a robust and secure release process by adding new workflows and documentation to enforce stricter controls over how releases are created and published. The changes ensure that releases are only made from well-tested code, prevent accidental or unauthorized modifications to the release branch, and provide clear instructions for maintainers.
Key changes include:
Release Process Enforcement:
release-source-gatejob to.github/workflows/ci.ymlto ensure that pull requests to thereleasebranch can only come from themainbranch, blocking all other sources..github/workflows/release.ymlworkflow, which:releasebranch via a gating job.Documentation:
RELEASING.mdfile detailing the release process, the guarantees provided by the new checks and workflows, and troubleshooting steps for common issues.