Drive releases from CHANGELOG.md with towncrier (ENG-10965)
Closes ENG-10965.
Releases were cut by hand: write a dated ## [X.Y.Z] heading, push a v* tag, and release.yml built the wheel matrix and published. The tag was the trigger, so a failed publish left a tag to delete and re-cut, the changelog was a checklist item nothing enforced, and every contributor edited the same file.
This adopts reflex-release (0.1.0a2) with towncrier, so CHANGELOG.md is the trigger: a version heading with no matching git tag is what publishes that version, and the tag is pushed only after PyPI accepts the artifacts. A failed release is retried by pushing a fix on top of the changelog bump — nothing to delete, nothing to re-cut.
news fragment ──▶ Dispatch release ──▶ CHANGELOG.md bump ──▶ merge
(towncrier) (PR) │
▼
GitHub release ◀── tag ◀── upload ◀── approval ◀── build matrix
What contributors do differently
Every change under python/, src/ or js/ adds a news fragment instead of editing CHANGELOG.md:
make news NAME=1234.feature.md # breaking, deprecation, feature, bugfix,
# performance, docs, misc
make news-check # the same check the PR runs
changelog.yml requires one, rejects hand-written version headings, and runs sync --check. The skip-changelog label waives the fragment for changes that genuinely are not user-facing.
The integration surface
Four workflows come from the tool and are regenerated verbatim by reflex-release sync. This repository owns two, both wired in through [tool.reflex-release]:
| Key | Workflow | Role |
|---|---|---|
custom-build | build_release_artifacts.yml | The release matrix. uv build on one runner cannot produce eleven cross-compiled platform wheels, a runtime-verified PyEmscripten wheel and an sdist, so publish.yml calls this in place of its own build job. |
post-release-workflow | deploy-docs-stg.yml | Dispatched per published tag, once the upload, tag and GitHub release exist. |
The matrix, every per-artifact gate and the wheel-size budget are unchanged. What is new: each build job tags its own checkout (no tag exists at build time), expect-artifacts names all thirteen files a release must contain — so a leg that silently uploads nothing stops the release rather than shipping a version some users cannot install — and the whole matrix runs before the approval gate, where collect verifies every artifact declares the released version and checksums the set the reviewer approves.
deploy-docs-stg.yml's own push: tags trigger cannot see a tag pushed with GITHUB_TOKEN, which fires no on-push workflow; the post-release dispatch closes that gap. Its tag/package/version inputs are the dispatch contract.
Also in here
publish.ymlhas nopushtrigger, so a hand-cut tag can no longer publish without a changelog entry, the version gate, or an approval.- The tag-shape gate accepts
.postN(therelease-postaction) and runs in the build workflow'sversion-gatejob, ahead of the cross-compile legs. - Existing changelog headings are converted to the towncrier format the release parser reads back, and
## [Unreleased]becomes the towncrier marker. The pending entry for #496 moves tonews/496.feature.md. verify_ci_workflow.pydrops its release-workflow validation, and the action-pin policy now applies to the workflows this repository authors. The release pipeline's invariants are tested where the tool lives, and re-asserting them here went stale the moment the tool changed (net −1660 lines from that removal).
Before merging — repository settings
- PyPI trusted publishing repointed from
release.ymltopublish.yml(environmentpypi). A publisher still namingrelease.ymlwill reject the upload. - Required reviewers on the
pypienvironment.publish.ymlfails closed without them.allow-self-reviewis left at its default (true), so the reviewer list is the control; flipping it tofalseadditionally requires "Prevent self-review" on the environment. - Allow GitHub Actions to create and approve pull requests (Settings → Actions → General), so a release action can open its PR.
- Create the
skip-changelogandchangelog-version-editlabels. - This PR needs the
changelog-version-editlabel: converting the six existing headings makes every version look new to the heading guard, which is exactly what that label exists for.
Verification
reflex-release sync --check (all four up to date), detect (correct no-op at the tagged 0.0.6), changelog-check, verify_ci_workflow, abi_smoke, check-sdist (news/ excluded, CHANGELOG.md shipped), Ruff, and all three pre-commit hooks pass. Test suite: 4305 passed, with two pre-existing tests/test_shared_glhost.py failures that reproduce without these changes.
The plan → materialize → detect → extract-notes loop was exercised locally end to end (it produced a correct ## v0.0.7 section from a fragment and then reported "will publish v0.0.7"), then reverted.
Generated by Claude Code
Summary by CodeRabbit
-
New Features
- Improved automatic placement for unanchored Cartesian legends when location is set to “best,” including responsive browser adjustment.
- Introduced changelog-driven release automation for building, approving, publishing, tagging, and documenting package releases.
- Added support for post-release version tags.
-
Bug Fixes
- Prevented value and drop-off labels from overlapping in short funnel charts.
- Improved validation of release versions and generated release workflows.
-
Documentation
- Expanded contributor and release-process guidance, including changelog fragments and release commands.
- Updated changelog and API documentation links and release instructions.