Skip to content

Automate release pipeline with towncrier-fragments version scheme#682

Open
RonnyPfannschmidt wants to merge 8 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:release-proposer
Open

Automate release pipeline with towncrier-fragments version scheme#682
RonnyPfannschmidt wants to merge 8 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:release-proposer

Conversation

@RonnyPfannschmidt
Copy link
Copy Markdown
Member

Automated release workflow

This replaces the manual release process with a fully automated CI pipeline.
The version is derived from changelog fragment types using the
towncrier-fragments scheme from setuptools-scm>=10.0:
feature → minor, bugfix → patch, removal → major.

How it works

  1. Contributors add changelog/<id>.<type>.rst fragments in PRs as usual.

  2. Every push to main triggers prepare-release.yml, which computes the
    next version, creates a release-X.Y.Z branch with the towncrier-built
    changelog, and opens (or updates) a PR.

  3. The test workflow runs on the release PR. After all checks pass,
    release-artifacts creates a draft GitHub release with the built
    wheel and sdist attached.

  4. A maintainer reviews the PR and publishes the draft release.

  5. Publishing triggers deploy.yml, which downloads the bit-identical
    artifacts from the draft, verifies PR checks, uploads to PyPI via
    trusted publishing, merges the PR, and cleans up the branch.

Locking semantics

  • Draft release = staging area, continuously replaced as main evolves.
  • Published release = lock. Once published, the pipeline skips that
    version entirely — the next push with new fragments starts a new cycle.

Known unsolved problem

When a new changelog fragment changes the computed version (e.g. a removal
fragment lands bumping from 1.7.0 to 2.0.0), the release branch name
changes from release-1.7.0 to release-2.0.0. The old branch/PR/draft
become stale. The workflow closes superseded PRs, but the old draft release
and branch are not automatically cleaned up. This needs a strategy — likely
deleting the old draft and branch when the version changes.

wahuneke and others added 3 commits May 27, 2026 11:57
Replace the manual release process with an automated CI pipeline:

- Configure setuptools_scm with the `towncrier-fragments` version scheme
  (from vcs-versioning, bundled with setuptools-scm>=10.0) to derive
  semver bumps from changelog fragment types automatically.

- Add `prepare-release.yml` workflow: triggers on every push to main,
  computes the next version from fragments, creates/updates a
  `release-X.Y.Z` branch with towncrier-built changelog, and opens a PR.

- Modify `test.yml`: set SETUPTOOLS_SCM_PRETEND_VERSION on release PRs
  so built packages have the exact release version. Add a
  `release-artifacts` job that creates/updates a draft GitHub release
  with the tested wheel+sdist after all checks pass.

- Replace `deploy.yml`: trigger on `release: published` instead of tag
  push. Downloads bit-identical assets from the draft release, verifies
  PR checks, uploads to PyPI via trusted publishing, merges the PR, and
  cleans up the release branch.

- Reclassify `632.removal.rst` as `632.feature.rst` (it's a deprecation
  warning, not a breaking removal).

- Update RELEASING.rst and scripts/release.py to document and support
  the new automated flow (with manual fallback preserved).

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
The draft release is a staging area that gets continuously replaced as
main evolves.  Only a published (non-draft) release acts as a lock:

- prepare-release: skips only when a published release exists for the
  version (meaning it is already on PyPI).  Draft releases are freely
  overwritten by force-updating the release branch.
- release-artifacts: creates or replaces the draft after every green
  test run.  Skips only when a published release exists.

To start a fresh release cycle after publishing, the next push to main
with new fragments will compute a new version automatically.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 10:56
The `release-` prefix match was too broad — branches like
`release-proposer` would set PRETEND_VERSION to `proposer`,
breaking the build. Now validate that the suffix matches
`^[0-9]+\.[0-9]+\.[0-9]+` before treating it as a version.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Automates the pluggy release process by deriving versions from Towncrier fragment types (via setuptools-scm’s towncrier-fragments scheme) and wiring GitHub Actions workflows to prepare release PRs, publish draft releases, and deploy to PyPI on release publication.

Changes:

  • Add scripts/compute_version.py and update pyproject.toml to compute/derive versions from Towncrier fragments using towncrier-fragments.
  • Replace tag-push deployment with a release-publication-driven pipeline (prepare-release.yml, release-artifacts job in test.yml, and updated deploy.yml).
  • Update release docs and expand tracing tests to cover PluginManager hook-call tracing output.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
testing/test_tracer.py Adds tests validating PluginManager tracing output for hook calls (single and multiple impls).
scripts/release.py Updates legacy local release script to support auto-derived versions and align with the new CI pipeline.
scripts/compute_version.py New helper to compute the next release version using setuptools-scm + Towncrier fragments.
RELEASING.rst Documents the automated release flow and keeps a manual fallback section.
pyproject.toml Bumps setuptools-scm requirement and enables version_scheme = "towncrier-fragments".
changelog/632.feature.rst Adds a changelog entry for hookspec self/@staticmethod deprecation warning behavior.
.github/workflows/test.yml Injects release-branch version into package build and adds a job to update a draft GitHub release from CI artifacts.
.github/workflows/prepare-release.yml New workflow to compute version on main, build changelog, force-update release-X.Y.Z branch, and create/update the release PR.
.github/workflows/deploy.yml Switches deployment trigger to “release published”, verifies PR checks, publishes to PyPI, merges PR, and deletes the release branch.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/release.py
Comment on lines +26 to +33
def compute_version_auto() -> str:
"""Derive the next release version from changelog fragments."""
from scripts.compute_version import compute_version

version = compute_version()
if version is None:
raise RuntimeError("No changelog fragments found — cannot determine version.")
return version
Comment on lines 35 to 38
- uses: hynek/build-and-inspect-python-package@fe0a0fb1925ca263d076ca4f2c13e93a6e92a33e # v2.17.0
env:
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PLUGGY: ${{ steps.release.outputs.version || '' }}

Comment thread .github/workflows/prepare-release.yml Outdated
Comment on lines +114 to +115
for pr in $(gh pr list --head 'release-' --json number,headRefName \
--jq '.[] | select(.headRefName != "'"$BRANCH"'") | .number'); do
RonnyPfannschmidt and others added 4 commits May 27, 2026 13:08
- Rename `changelog/` to `changelog.d/` to match the convention
  expected by the `towncrier-fragments` version scheme in
  vcs-versioning. This makes `python -m setuptools_scm --strip-dev`
  correctly detect fragment types and compute the release version.

- Remove `scripts/compute_version.py` — the setuptools_scm CLI
  with `--strip-dev` replaces it entirely.

- Update `prepare-release.yml` and `scripts/release.py` to use the CLI.

- Add `setuptools-scm[toml]>=10.0` to the tox release env deps.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
- test.yml: Set SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PLUGGY via
  GITHUB_ENV only when a valid release version is detected, avoiding
  an empty-string env var that would break setuptools-scm on
  non-release branches.

- prepare-release.yml: Fix stale release PR cleanup — `gh pr list
  --head` does exact matching, so list all PRs and filter by
  `startswith("release-")` in jq instead.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Use the typed Octokit REST API via actions/github-script@v9 for all
GitHub API interactions in the release workflows. This eliminates
fragile shell+jq piping (including the --head prefix-matching bug)
and makes the logic more readable and maintainable.

Shell scripts are kept only for git operations, Python tooling, and
simple asset downloads where they remain the better fit.

Converted steps:
- prepare-release.yml: release-published check, branch up-to-date
  check, PR create/update/close-stale
- test.yml: draft release create/update with asset management
- deploy.yml: PR lookup + CI status verification, PR merge + branch
  cleanup

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Release PRs should validate downstream compatibility before
publishing, so trigger the downstream workflow unconditionally
when the head branch starts with `release-`.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
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.

3 participants