reflex-release: lift unshippable dependency pins when materializing a release - #6889
Conversation
Greptile SummaryMaterialization now replaces unshippable sibling dependency floors with the earliest suitable tagged release and refreshes the lock file atomically.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/reflex-release/src/reflex_release/devpins.py | Adds dependency-bound discovery, tag-based replacement selection, requirement-preserving TOML rewrites, lock refresh, and rollback handling. |
| packages/reflex-release/src/reflex_release/commands.py | Integrates blocked-pin filtering into planning and stages repinned files during materialized release delivery. |
| packages/reflex-release/src/reflex_release/cli.py | Accepts and forwards materializationβs repinned-path output to release delivery commands. |
| packages/reflex-release/src/reflex_release/templates/workflows/dispatch_release.yml | Threads the materialize stepβs repinned output into both release delivery paths. |
| tests/units/reflex_release/test_devpins.py | Covers dependency resolution, requirement rewriting, lock handling, exemptions, and transactional rollback. |
| tests/units/reflex_release/test_commands.py | Covers plan-time holdbacks and committing materialized dependency changes. |
Reviews (5): Last reviewed commit: "Scope the pin rewrite to published requi..." | Re-trigger Greptile
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
All reported issues were addressed across 7 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
All four outstanding points addressed in 75e98b5. (The two shallow-clone threads @masenf already closed as by-design β agreed, and the README now says so where it explains that "published" means tagged.) Strict floor rewritten wrong (P1) β a real bug, good catch. I also added the invariant behind it: TOML escaping (P2) β real, and fail-safe rather than corrupting, but it aborts a release for a legal input. Confirmed the mechanism: for Non-atomic upgrade (P2) β real, though it needs a hand-run to bite. In CI a failed Over-broad staging (P2) β you're right that the comment overclaimed. The obstacle was that 279 passing (6 new); Generated by Claude Code |
There was a problem hiding this comment.
All reported issues were addressed across 7 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
fc0dc7b to
f113690
Compare
FarhanAliRaza
left a comment
There was a problem hiding this comment.
Ran this for real rather than reading it: built a sandbox repo with a root package, a sibling, and tags at 0.1.0 / 0.2.0a1 / 0.2.0 / 0.3.0, then drove plan -> materialize -> push-prerelease through a dozen shapes.
The behaviour matches the README table. >= 0.2.0.dev1 lifts to >= 0.2.0 for a final and >= 0.2.0a1 for a prerelease; > 0.2.0.dev1 becomes >= 0.2.0; extras and a python_version > "3.10" marker survive untouched; a prerelease floor on an outside dep is left alone while a dev floor on one is held back; an unsatisfiable floor drops an auto-selected package (with a ### Held back summary) and fails an explicit one; the two-package rollback from the last commit really does restore both pyprojects and leave the fragments on disk; and the commit carries exactly the changelog, the fragment deletion and pyproject.toml, with an unrelated dirty file left out.
Two things I'd fix before merging, plus a few caveats β inline.
Two more that didn't have a good line to hang on:
- The pin's target version and the release action can disagree.
>= 0.4.0.dev1on a sibling at0.3.0plusrelease-patchproduces0.3.1, which still doesn't satisfy the floor, so the dependent stays held back until someone picks a minor. Worth a line in the README next to "the next release lifts the pin by itself". - The rollback guard covers apply + lock but not the towncrier step after it. I made towncrier fail and
pyproject.tomlwas left lifted. CI throws the worktree away so this only bites local runs β noting it rather than asking for a change.
Tests, ruff and pyright are clean (299 passed).
A package that depends on a sibling it is waiting for pins the unreleased version β `widget-core >= 0.2.0.dev1` β so the workspace resolves while the sibling is still unpublished. That pin cannot be published, and `check-dev-pins` rejects it at build time, which leaves someone to remember to lift it by hand once the sibling is out. Materialization does it instead. Every floor the release cannot ship β a `*.dev` floor, and a prerelease floor on a sibling when the version being materialized is final β is rewritten to the earliest published version that satisfies the whole requirement, `uv.lock` is re-resolved, and both land in the release commit alongside the changelog bump, through the same review. "Published" means tagged: tags are created only after a successful upload, so the repository's own tags are its record of what is on PyPI. A published prerelease therefore satisfies a floor only when the release being materialized is itself a prerelease; a final version never floors its users on a sibling's alpha. A floor no published version satisfies has nowhere to go, so the package is held back at plan time rather than materialized into a version that could never be published: auto-selected packages are dropped from the batch (a lockstep group whole, since its members only release together) and listed in the run summary, while an explicit selection fails the dispatch. Two things are left alone: a floor on a lockstep sibling that `pin-exact` rewrites at build time anyway, and a prerelease floor on a dependency outside the repository, whose releases are not recorded here. A `*.dev` floor on an outside dependency still holds the package back β that pin is unpublishable whoever owns it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B13GVXzLdYzmqmMnaKWvyV
Fix a real rewrite bug. `widget-core > 0.2.0.dev1` admits 0.2.0, so 0.2.0 can be what it resolves to β but rewriting the operator verbatim produced `> 0.2.0`, excluding the very release the requirement had just been lifted onto, and under an upper bound that could leave it unsatisfiable. A strict floor over an unreleased version now becomes an inclusive floor over the release above it, and every rewrite is checked against the version it resolved to before being written, so a future gap in the operator handling fails loudly instead of publishing metadata that resolves to nothing. Handle TOML escaping. A requirement carrying a double-quoted marker is escaped in the file but comes back from the parser unescaped, so searching for the parsed value found nothing and aborted materialization. Both spellings a value can have β an escaped basic string and a literal string, which cannot escape anything β are now tried. Make the upgrade atomic. If `uv lock` failed, the rewritten pyproject files were already on disk beside the old lock file, and a re-run would find nothing left to lift, skip the lock refresh, and could commit exactly that pairing. The rewrites are rolled back when the lock cannot follow them. Stage only what materialization wrote. `_commit_materialized` promised as much but built its list from every released package's pyproject plus the lock file whenever one existed, so an unrelated uncommitted edit to one of those tracked files would ride along in the release commit. `materialize` now reports the paths it rewrote as a `repinned` output β it runs as a separate process from the delivery step, which cannot otherwise know β and only those are staged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B13GVXzLdYzmqmMnaKWvyV
The rollback guard started one line too late. `apply_pin_upgrades` rewrites package by package, so a requirement the second package cannot be given β one that appears twice, or a rewrite its resolved version would not satisfy β left the first package's pins lifted in the worktree while the command failed. Bring the rewrite itself inside the guard, so the whole batch and the lock file move together or not at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B13GVXzLdYzmqmMnaKWvyV
β¦ifted pins Four things the review turned up. The rewrite counted occurrences across the whole file, so a copy of the same requirement in `[dependency-groups]` β development-only, and deliberately ignored by `check_dev_pins` β made the count ambiguous and aborted an otherwise fine release. It now counts and rewrites only inside the `[project]` table and its subtables, which is exactly what `published_dependencies` reads. The same string in `dependencies` and in an optional-dependency group is two published requirements rather than an ambiguity, and both are lifted. A lifted pin could fail to reach the release commit. When the delivery step passes no `repinned` paths β a consumer whose workflow predates that output, and `cli-command` may be unpinned, so the tool can gain the output before the workflow does β the commit went out with the old pin and died at the publish-time gate, which is the failure this feature exists to remove, arriving later and with more to unwind. The commit now fails if a file materialization owns is still modified after staging, naming the re-sync. An exact floor (`== 0.2.0.dev1`) was told to release the dependency first and wait, which is a circle: no published version can ever equal an unpublished one. It is now reported as a dead end that has to be re-pinned by hand, and the closing advice is derived from what the blockers actually are. The lock file is reported only when the re-resolution moves it. In a uv workspace it never does for a sibling pin β the lock records members as `editable` with no specifier β and staging it regardless would have carried unrelated resolution churn into a release commit. The README now says that plainly instead of claiming nothing else is staged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B13GVXzLdYzmqmMnaKWvyV
f113690 to
15e2f8d
Compare
Merge brings in two reflex-release changes that alter the templates: dependency-pin lifting during materialize (#6889) and the skip-propagation fix in publish.yml and release_from_changelog.yml (#6950). `sync --check` flagged all three affected workflows as drifted, which is the mechanism working β a template change on main that never reached this repository's own workflows is exactly what it is there to catch. Regenerated; the uv and Python pins and the dispatch checkboxes are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXbekKtPyfbENVnZdTRxKX
β¦package (#6941) * Replace the release workflows with the bundled reflex-release The publish pipeline was a hand-maintained set of workflows over scripts/release.py and .github/scripts/*. reflex-release, which lives in this repository at packages/reflex-release, is that same pipeline packaged and generalized, so the workflows now come from its templates instead. Every reflex-specific behavior moves into [tool.reflex-release] in the repo-root pyproject.toml: the reflex/reflex-base lockstep pair (one version, exact pin rewritten at build time, reflex uploaded last), the internal packages that patch-release on every push, the America/Los_Angeles release timezone, and the packages exempt from the news-fragment check. The .pyi check on the reflex wheel becomes the pipeline's post_build.sh hook, and the dev-pin gate is now reflex-release's own check-dev-pins, which reports identically to scripts/check_min_deps.py --check-dev-pins. cli-command runs the copy in this repository straight from uv.lock rather than a version published to PyPI: uv run --frozen --package reflex-release reflex-release That pins the pipeline to one commit. The workflows are rendered from the templates of the commit that contains them, so `sync --check` β which changelog.yml now runs on every pull request β fails both on a workflow edited by hand and on a template change that was never regenerated. The two can no longer drift apart. What changes in behavior, beyond the move: - publish.yml splits validation, build and verification into separate unprivileged jobs and puts the SHA-256 manifest in front of the approver; the manifest is also attached to the GitHub release. - changelog.yml also runs on pull requests targeting the publishing branches, and its release-branch exemption for version headings now requires the pull request to be authored by github-actions[bot]. - Dispatch release takes a comma-separated package list instead of one checkbox per package, which no longer fits GitHub's workflow_dispatch input limit. - auto_release_internal.yml triggers on an internal package's src/ rather than its whole directory, matching what detection counts as its source, and diffs the whole pushed range instead of the last commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXbekKtPyfbENVnZdTRxKX * Pin the release toolchain in reflex-release, restore the checkboxes Four changes to the tool, and the regenerated workflows that follow from them. Pinned toolchain. New uv-version and python-version keys write the uv and Python the generated workflows install verbatim into every setup-uv step, in place of whatever that action resolves at run time. Both default to a version reflex-release itself pins, so upgrading the tool moves the release toolchain with it and `sync --check` reports that as drift until the workflows are regenerated β the same signal a template change already gives. A repository that wants its own cadence sets either key; "" leaves that version to the setup action. Both are interpolated into a quoted YAML scalar, so they are validated against a version-or-specifier pattern rather than trusted. The pins live in one rendered block instead of in each template, and a test asserts every setup-uv step in every template carries the placeholder that receives it: render() only fails on a placeholder it cannot substitute, so a step added without one would otherwise silently install an unpinned uv. Pinned build backend. reflex-release pins hatchling and uv-dynamic-versioning exactly. Build requirements are resolved fresh rather than locked, so a repository that vendors the tool β as this one now does, running it out of uv.lock β no longer has the backend that builds its release tooling move underneath it. Checkbox limit. Ten was wrong: workflow_dispatch takes twenty inputs, and this repository's own dispatch form has been running eighteen checkboxes. Raised to nineteen packages plus the release action, which brings the checkboxes back here β the comma-separated fallback was a regression, not a fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXbekKtPyfbENVnZdTRxKX * Regenerate the release workflows after merging main Merge brings in two reflex-release changes that alter the templates: dependency-pin lifting during materialize (#6889) and the skip-propagation fix in publish.yml and release_from_changelog.yml (#6950). `sync --check` flagged all three affected workflows as drifted, which is the mechanism working β a template change on main that never reached this repository's own workflows is exactly what it is there to catch. Regenerated; the uv and Python pins and the dispatch checkboxes are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXbekKtPyfbENVnZdTRxKX * Add never-publish-packages; append new Config fields Both from cubic's review of #6941. never-publish-packages, for the packages a repository builds but never releases. changelog-exempt-packages was the closest thing, and it only waives the news-fragment requirement β it left integrations-docs with a release checkbox and publishable by hand, which the checkbox form made visible. A listed package now gets no checkbox, is never auto-selected, is skipped by changelog detection even when it has a CHANGELOG.md, needs no fragment, and is refused by prepare-publish, so the one remaining way to reach it β typing it into the publish workflow β fails in the first unprivileged job rather than at verify-dist after a build. Being unreleasable it cannot also be a lockstep member, a custom-build package, latest-release-package or internal; each is rejected when the configuration loads. integrations-docs moves to the new key, which drops it from the Dispatch release form. Config's new fields move to the end of the dataclass. It is exported, so the generated __init__ has a positional contract: uv_version and python_version sat after cli_command, shifting every later argument for a caller that does not pass everything by keyword. A test pins the historical field order as a prefix so the next field added lands in the right place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXbekKtPyfbENVnZdTRxKX * Re-run the changelog check when pull request labels change Ports #6916 into the reflex-release changelog template, so the fix applies to every repository the tool scaffolds rather than to this one's copy of a workflow that is now generated. skip-changelog and changelog-version-edit waive parts of the check, so a verdict is only valid for the label set it was computed under. Three ways that broke: applying a label after the last push started no run at all, removing one left the green run that label produced standing with the gate silently open, and re-running a failed run replayed the original event payload β where the label does not exist yet β so the check kept failing until someone pushed again. So: labeled/unlabeled join the trigger types, and the labels are read back from the API into a step output the two guarded steps test, in a step that runs before the checkout because it needs nothing but `gh`. The job itself stays ungated: a job skipped by `if` reports its check as skipped, which branch protection counts as passing, so a cheap no-op on an unrelated label would overwrite a real failure with a green. Regenerated reflex's own changelog.yml from the template. The jq program was checked against the label sets it has to distinguish, running the generated step itself against a stubbed `gh`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXbekKtPyfbENVnZdTRxKX --------- Co-authored-by: Claude <noreply@anthropic.com>
All Submissions:
Type of change
The problem
A package that depends on a sibling it is waiting for pins the unreleased version β
widget-core >= 0.2.0.dev1β so the workspace resolves while the sibling is still unpublished. That pin cannot be published:*.devversions never reach PyPI, so the metadata would be uninstallable.check-dev-pinsrejects it at build time, which means someone has to remember to lift it once the sibling is out β and finds out at the publish gate, with the changelog already bumped, when they forget.What this does
Materialization lifts the pin. Every dependency floor the release cannot ship is rewritten to the earliest published version that satisfies the whole requirement,
uv.lockis re-resolved, and both land in the release commit alongside the changelog bump β through the same review.>= 0.2.0.dev10.2.0a1,0.2.0, β¦0.2.0, β¦>= 0.2.0a1>= 0.2.0A floor no published version satisfies has nowhere to go, so the package is held back at plan time rather than materialized into a version that could never be published: auto-selected packages are dropped from the batch (a lockstep group whole, since its members only release together) and listed in the run summary, while an explicit selection fails the dispatch. Release the depended-on package first and the next release lifts the pin by itself.
Design notes worth reviewing
*.devfloor on one holds the release back (it is unpublishable regardless, andcheck-dev-pinswould fail the build anyway); a prerelease floor on one is left alone, since its releases are not recorded here and pinning a beta is somebody's deliberate choice.>= 0.2.0.dev1to>= 0.2.0a1,check-dev-pinsis satisfied, and the subsequent final release ships a floor that resolves users onto an alpha. Happy to scope this back to dev floors only if you would rather.uv lockis the lock refresh, auto-detected from a rootuv.lockand skipped when there is none. Not configurable β the tool already assumes uv everywhere (cli-command,setup-uvin every generated job).pin-exactare skipped, sincepin-locksteprewrites those to== <version>at build time and nothing declared here is shipped.materializereports the paths it rewrote as arepinnedoutput β it runs as a separate process from the delivery step β and_commit_changelogs(now_commit_materialized) stages exactly those beside the changelogs, so unrelated worktree edits stay out of the release commit.dispatch_release.ymlchanges (the materialize step gains anid, the two delivery steps aREPINNED_JSONenv), so adopting repos need areflex-release sync; the PR drift check enforces it.Changes To Core Features:
uv run pytest tests/units/reflex_releaseβ 299 passed (34 new here): bound resolution across both prerelease modes, the whole-specifier-set constraint, requirement rewriting that preserves extras/markers/other specifiers and relaxes a strict floor, TOML basic- and literal-string rewriting, the lockstep and outside-dependency exemptions, rollback on a failed lock and on a mid-batch failure, and the plan-time hold-back in explicit, auto and lockstep-group forms.ruff check,ruff format --checkandpyrightclean.Rebased onto
mainnow that #6888 has merged; the two features compose cleanly (verified the rendered workflows carry both the post-release dispatch step and therepinnedthreading).