Drive releases from CHANGELOG.md with towncrier (ENG-10965)

Merged
MA
2mo ago
+1831-798

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]:

KeyWorkflowRole
custom-buildbuild_release_artifacts.ymlThe 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-workflowdeploy-docs-stg.ymlDispatched 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.yml has no push trigger, 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 (the release-post action) and runs in the build workflow's version-gate job, 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 to news/496.feature.md.
  • verify_ci_workflow.py drops 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

  1. PyPI trusted publishing repointed from release.yml to publish.yml (environment pypi). A publisher still naming release.yml will reject the upload.
  2. Required reviewers on the pypi environment. publish.yml fails closed without them. allow-self-review is left at its default (true), so the reviewer list is the control; flipping it to false additionally requires "Prevent self-review" on the environment.
  3. Allow GitHub Actions to create and approve pull requests (Settings → Actions → General), so a release action can open its PR.
  4. Create the skip-changelog and changelog-version-edit labels.
  5. This PR needs the changelog-version-edit label: 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.