reflex-release: lift unshippable dependency pins when materializing a release - #6889
reflex-release: lift unshippable dependency pins when materializing a release#6889masenf wants to merge 3 commits into
Conversation
Greptile SummaryMaterialization now replaces unshippable dependency floors with the earliest suitable tagged sibling release and refreshes
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/reflex-release/src/reflex_release/devpins.py | Implements tagged-version resolution, requirement rewriting, lock refresh, and rollback without leaving an eligible follow-up issue. |
| packages/reflex-release/src/reflex_release/commands.py | Integrates pin eligibility into planning and stages materialized dependency changes alongside changelogs. |
| packages/reflex-release/src/reflex_release/cli.py | Propagates the materialization output into release delivery commands. |
| packages/reflex-release/src/reflex_release/templates/workflows/dispatch_release.yml | Wires materialization outputs into both prerelease and release-PR delivery paths. |
| tests/units/reflex_release/test_devpins.py | Covers requirement resolution, rewriting, lock refresh, rollback, and blocking behavior. |
| tests/units/reflex_release/test_commands.py | Covers planning holdbacks and release-commit handling for rewritten files. |
Reviews (4): Last reviewed commit: "Roll back a partly-applied pin batch, no..." | 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
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
fc0dc7b to
f113690
Compare
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).