Features
- New package:
reflex-releaseextracts Reflex's changelog-driven release pipeline — the towncrier news-fragment workflow, the changelog-as-source-of-truth publish detection, and the human-gated PyPI upload — into a reusable tool other repositories can adopt withuvx reflex-release init. It scaffolds the GitHub Actions workflows into the consuming repository (required for PyPI trusted publishing, which validates the workflow's own repository) and keeps them in step withreflex-release sync --check. Repository shape lives in a[tool.reflex-release]table: single package or monorepo, branch policy, lockstep groups that release together at one version, and internal packages that release without a changelog. Every upload waits on an approval from thepypienvironment's required reviewers, and the artifacts it covers are named by SHA-256 in the run summary and in a manifest attached to the GitHub release. Seepackages/reflex-release/README.mdfor setup, configuration and the security model. (#6868) reflex-releasecan now delegate a package's build to a workflow the consuming repository owns, for packages whose artifacts cannot come from a singleuv build— a matrix of platform-specific wheels, say. A[[tool.reflex-release.custom-build]]entry names the packages and the workflow file, and the generatedpublish.ymlcalls it in place of its own build job, passing the package, version, tag, build directory and the artifact-name prefix to upload under. Everything either side of the build is unchanged: the whole matrix runs before the approval gate, in the same unprivileged trust boundary (contents: read, no secrets, no OIDC — a called workflow cannot hold more than its caller grants), and every file it produces is verified against the release, hashed into the manifest the approver sees, and published only after that approval. A failed leg fails the release rather than uploading a partial set, andexpect-artifactscan additionally require the built set to match a list of filename patterns so a leg that silently produced nothing is caught too.reflex-release syncfails when a configured build workflow is missing or declares noworkflow_calltrigger, so a renamed file is a red pull request instead of a failed release. (#6891)- Add
post-release-workflowto[tool.reflex-release]: the named workflow is dispatched once per published tag, on the tag itself, after the upload, the tag and the GitHub release exist — withtag,packageandversionasworkflow_dispatchinputs. The dispatch step and theactions: writegrant it needs are only scaffolded intopublish.yml(and the workflows that call it) when the setting is present, so a repository that runs nothing after a release keeps the narrower permissions. - Materializing a changelog now names leftover orphan news fragments (
+something.feature.md) after the pull request whose commit added them, so their entries get the usual#-link instead of shipping unlinked. - Materialization now lifts dependency pins a release cannot ship. A
*.devfloor — and, for a final version, a prerelease floor on a sibling package — is rewritten to the earliest published version that satisfies the requirement,uv.lockis re-resolved, and both land in the release commit alongside the changelog bump. "Published" means tagged, which is this pipeline's record of what reached PyPI, so a prerelease satisfies a floor only when the version being materialized is itself a prerelease. A floor no published version satisfies has nowhere to go, so the package is held back at plan time — dropped from an auto-selection (a lockstep group whole), an error for an explicit one — rather than materialized into a version that could never be published. (#6889) - Add
uv-versionandpython-versionto[tool.reflex-release]: every generated workflow now installs uv and Python at those exact versions instead of whateversetup-uvresolves at run time. Both default to versions this tool pins, so upgrading reflex-release moves the release toolchain with it andsync --checkreports that as drift until the workflows are regenerated — the same signal a template change gives. Set either key to keep your own cadence, or to""to leave that version to the setup action. (#6941) - Add
never-publish-packagesto[tool.reflex-release], for the packages a repository builds but never releases — an application, a docs bundle, a fixture. A listed package gets no Dispatch release checkbox, is never auto-selected, is skipped by changelog detection even if it has aCHANGELOG.md, needs no news fragment, and is refused byprepare-publish, so a manual publish dispatch fails in the first unprivileged job instead of atverify-dist. It cannot also be a lockstep member, acustom-buildpackage,latest-release-packageor internal; each combination is rejected when the configuration loads.changelog-exempt-packageswas the closest thing before, and it only waives the news-fragment requirement. (#6941)
Bug Fixes
- A release using a
[[tool.reflex-release.custom-build]]entry no longer publishes nothing while reporting success. Exactly one of the built-inbuildjob and a custom-build job runs for any given package, so the other is always skipped — and GitHub evaluates the implicitsuccess()a job gets when itsifnames no status function over the whole transitive dependency closure, not just the directneeds. The skipped build therefore reachedpublishstraight through thecollectwritten to absorb it, andtag-and-releasebehind it: every build succeeded, the artifacts were verified and checksummed, and then the upload silently never happened. Both jobs now carry an explicitneeds.<job>.result == 'success'guard.release_from_changelog.yml'sreportjob, the canonical failure signal for a partial release, was blind to the same shape because it accepted any skipped leg; it now accepts a skipped leg only when detection found nothing for that leg to publish, so a release that publishes nothing — or holds a lockstep package back — is red. - The generated
changelog.ymlnow re-runs when a pull request's labels change, and reads the labels back from the API instead of the event payload.skip-changelogandchangelog-version-editwaive parts of the check, so a verdict is only valid for the label set it was computed under: applying a label after the last push previously started no run at all, removing one left the green run that label produced standing, and re-running a failed run replayed the original payload — where the new label does not exist — so the check kept failing until someone pushed again. The job itself stays ungated so every run computes the real verdict, because a job skipped byifreports a check that branch protection counts as passing. (#6941) - A release using a
[[tool.reflex-release.custom-build]]entry no longer publishes nothing while reporting success. Exactly one of the built-inbuildjob and a custom-build job runs for any given package, so the other is always skipped — and GitHub evaluates the implicitsuccess()a job gets when itsifnames no status function over the whole transitive dependency closure, not just the directneeds. The skipped build therefore reachedpublishstraight through thecollectwritten to absorb it, andtag-and-releasebehind it: every build succeeded, the artifacts were verified and checksummed, and then the upload silently never happened. Both jobs now carry an explicitneeds.<job>.result == 'success'guard.release_from_changelog.yml'sreportjob, the canonical failure signal for a partial release, was blind to the same shape because it accepted any skipped leg; it now accepts a skipped leg only when detection found nothing for that leg to publish, so a release that publishes nothing — or holds a lockstep package back — is red. (#6950)
Miscellaneous
- Every
run:step in the generated release workflows now declaresshell: bash, so adefaults.run.shelladded to one of those files — or a runner whose default shell is not bash — cannot change how a release-critical script is interpreted, or silently drop the-e/-o pipefaila failing step relies on. (#6926) - Property docstrings are now noun phrases rather than "Get the ..." / "Return the ..." (ruff 0.16's new
D421). - Append new
Configfields after the existing ones.Configis exported, so its generated__init__has a positional contract that inserting a field in the middle would break for any caller that does not pass every argument by keyword; a test now pins the historical field order as a prefix. (#6941) - Pin
hatchlinganduv-dynamic-versioningexactly in this package's[build-system] requires. Build requirements are resolved fresh rather than locked, so a repository that vendors the tool — running it from its own lockfile rather than PyPI — no longer has the backend that builds its release tooling move underneath it. (#6941) - Raise the package-checkbox limit on the Dispatch release form from ten to twenty, matching GitHub's actual
workflow_dispatchinput limit. Repositories with between nine and nineteen releasable packages (or lockstep groups) keep the checkboxes underdispatch-package-inputs = "auto"instead of falling back to the comma-separated field. (#6941)