Skip to content

fix(cli): aube update --latest preserves higher-than-latest prerelease pins#445

Merged
jdx merged 2 commits intomainfrom
claude/update-prerelease-recursive
May 1, 2026
Merged

fix(cli): aube update --latest preserves higher-than-latest prerelease pins#445
jdx merged 2 commits intomainfrom
claude/update-prerelease-recursive

Conversation

@jdx
Copy link
Copy Markdown
Contributor

@jdx jdx commented May 1, 2026

Summary

Fixes a real bug uncovered while porting pnpm/test/update.ts: aube update --latest was silently downgrading user-pinned prereleases when the registry's latest dist-tag was older than the pin. Pnpm guards against this via #7436; aube didn't, so a manifest like "@pkg/has-prerelease": "3.0.0-rc.0" got rewritten to "2.0.0" if the registry's latest was 2.0.0.

This PR fixes the regression and ports the three pnpm tests it was blocking. Bumps pnpm/test/update.ts coverage from 13/22 → 16/22.

The fix

In crates/aube/src/commands/update.rs, before building resolver_manifest, pre-fetch packuments for every direct dep that's about to get its spec rewritten to latest. Parse the original pin and the registry's dist-tags.latest as semver. Skip the rewrite for any dep where the parsed pin is numerically newer.

The resolver then sees the original spec, resolves the original version, the lockfile keeps it, and the manifest rewrite path leaves it alone (because resolved == original).

Only applied to bulk update --latest (no positional args). When the user explicitly names a package — aube update --latest <pkg> — they're opting in to whatever latest says, even when that downgrades a prerelease (matches pnpm's pnpm update <pkg>@latest behavior, which is also tested at update.ts:659 and already ported).

Packuments come from the same cache the resolver uses moments later, so the only cost when nothing's stale is a cheap revalidation (5-min TTL + ETag).

Ports

pnpm test (line) aube @test
update to latest without downgrading already defined prerelease (#7436) (615) aube update --latest: keeps a higher-than-latest prerelease pin
update to latest recursive workspace (outdated, updated, prerelease, outdated) (728) aube update -r --latest: prerelease workspace mix preserves higher pins
update to latest recursive workspace (prerelease, outdated) (807) aube update -r --latest: prerelease + outdated workspace mix

pnpm test 615 also runs update -r --latest against the same single-project fixture; aube's -r requires a workspace root, so the recursive half is covered by the workspace ports above (728 and 807) rather than re-running it.

Doc cleanup

While at it, pnpm/test/recursive/update.ts — listed in PNPM_TEST_IMPORT.md as Phase 2 work with "5 tests, 2 dist-tag uses" — turns out to be 1 test.skip upstream now (pnpm itself doesn't currently pass it: per-project packageConfigs.savePrefix/saveExact doesn't reach recursive update --latest). Updated the doc to reflect this — nothing portable today, re-evaluate when pnpm un-skips.

Test plan

  • AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats test/pnpm_update.bats — 17/17 pass.
  • AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats test/update.bats — 10/10 still pass (no regression).
  • cargo test --workspace — all green.
  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings

🤖 Generated with Claude Code


Note

Medium Risk
Updates update --latest behavior to conditionally skip rewriting certain manifest pins based on live registry dist-tags.latest, adding concurrent packument fetches; this could impact update correctness/performance and introduces new network-failure edge cases.

Overview
Fixes a regression where bulk aube update --latest could downgrade user-pinned prereleases when the registry’s dist-tags.latest is numerically lower.

Before rewriting specifiers to latest, the command now pre-fetches packuments for targeted direct deps, detects exact-version pins (including npm: alias forms), and skips the latest rewrite when the pinned version parses higher than dist-tags.latest (bulk updates only; explicit update --latest <pkg> keeps the downgrade behavior).

Adds three new pnpm_update.bats tests covering prerelease preservation in single-project and recursive workspace updates, and updates PNPM_TEST_IMPORT.md to reflect the increased ported coverage and the corrected pnpm/test/recursive/update.ts status.

Reviewed by Cursor Bugbot for commit 2c90361. Bugbot is set up for automated code reviews on this repo. Configure here.

…e pins

Mirrors pnpm's #7436 regression guard: a manifest pin like
`"@pkg/has-prerelease": "3.0.0-rc.0"` shouldn't be silently
downgraded to `"2.0.0"` by `aube update --latest` just because the
registry's `latest` dist-tag is older than the user's pin.

Implementation: before building `resolver_manifest`, pre-fetch
packuments for every direct dep that's about to get its spec rewritten
to `latest`, parse the original pin and the registry's
`dist-tags.latest` as semver, and skip the rewrite for any dep where
the parsed pin is numerically newer. The resolver then sees the
original spec, resolves the original version, the lockfile keeps it,
and the manifest rewrite path leaves it alone (because resolved ==
original).

Only applied to bulk `update --latest` (no positional args). When the
user explicitly names a package — `aube update --latest <pkg>` —
they're opting in to whatever `latest` says, even when that downgrades
a prerelease (matches pnpm's `pnpm update <pkg>@latest` behavior).

Packuments come from the same cache the resolver uses moments later,
so the only cost when nothing's stale is a cheap revalidation (5-min
TTL + ETag).

Ports landed:
- pnpm/test/update.ts:615 ('update to latest without downgrading
  already defined prerelease (#7436)') — bulk single-project variant.
  pnpm also runs `update -r --latest` against the same single-project
  fixture; aube's `-r` requires a workspace root, so the recursive
  half is covered by the workspace ports below.
- pnpm/test/update.ts:728 ('update to latest recursive workspace
  (outdated, updated, prerelease, outdated)') — 4-project mix.
- pnpm/test/update.ts:807 ('update to latest recursive workspace
  (prerelease, outdated)') — 2-project mix.

16/22 of pnpm/test/update.ts ported (was 13/22). Drops the
prerelease-preservation divergence from PNPM_TEST_IMPORT.md.

Also notes upstream that `pnpm/test/recursive/update.ts` shrank to a
single `test.skip` upstream — pnpm itself doesn't currently pass the
only test in the file (per-project `packageConfigs.savePrefix` /
`saveExact` doesn't reach `recursive update --latest`). Nothing
portable today; re-evaluate when pnpm un-skips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR fixes a real regression in aube update --latest: when a user pinned a prerelease (e.g. 3.0.0-rc.0) and the registry's latest tag pointed to an older stable release (e.g. 2.0.0), the bulk update silently downgraded the manifest. The fix pre-fetches packuments for all exact-pinned direct deps, compares them to dist-tags.latest, and skips the specifier rewrite for any pin that is semver-newer. Previously flagged issues (silent swallowing of fetch errors and the undocumented return-slice contract of exact_pin_version) are both addressed in this revision. Three pnpm parity tests and accurate doc updates round out the change.

Confidence Score: 5/5

Safe to merge; the core logic is correct, both prior review concerns are resolved, and the new tests pass.

No P0 or P1 findings. The single P2 (task-panic path lacks a tracing::warn) is a minor observability gap, not a correctness issue. The fix is well-scoped to bulk update --latest with no positional args, the semver comparison is correct, and the test coverage directly exercises the pnpm parity cases.

No files require special attention beyond the minor P2 in update.rs.

Important Files Changed

Filename Overview
crates/aube/src/commands/update.rs Adds preserve_pin BTreeSet built from async packument pre-fetches; skips rewriting exact-version pins that are newer than dist-tags.latest. Also adds exact_pin_version helper. Previously flagged issues (silent error swallowing, undocumented return-slice contract) are both addressed. One minor unhandled path: JoinError from a panicking task is silently discarded.
test/pnpm_update.bats Adds three bats tests porting pnpm/test/update.ts:615, 728, and 807. Test structure is consistent with the existing suite; workspace tests correctly omit a root package.json matching the pattern of earlier workspace tests in the file.
test/PNPM_TEST_IMPORT.md Accurately updates test coverage count from 13/22 to 16/22, records the new ports, and correctly notes that pnpm/test/recursive/update.ts has a single test.skip upstream with no portable work today.

Fix All in Claude Code

Reviews (2): Last reviewed commit: "fix(cli): warn instead of silently faili..." | Re-trigger Greptile

Comment thread crates/aube/src/commands/update.rs
Comment thread crates/aube/src/commands/update.rs
…n guard

Two greptile P2 callouts on update.rs:

1. The pre-fetch loop was silently swallowing packument fetch + parse
   errors (`.ok()?`), so a transient registry blip during the
   prerelease-preservation guard would silently fall through to the
   rewrite path and downgrade the user's prerelease pin — exactly the
   bug this guard exists to prevent. Replace the swallow with a
   `tracing::warn!` for both the fetch failure and a non-semver
   `latest` dist-tag, then continue with the resolver path (which has
   its own retry/cache semantics).

2. Document `exact_pin_version`'s slice contract: the returned `&str`
   is the bare version (already stripped of `npm:`, the `<name>@`
   prefix, the optional `=`, and surrounding whitespace), suitable
   for `Version::parse` but NOT a round-trip back to the original
   specifier. Today's only caller hands it straight to `parse`, but
   future callers shouldn't be surprised by the shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx merged commit c260e3c into main May 1, 2026
18 checks passed
@jdx jdx deleted the claude/update-prerelease-recursive branch May 1, 2026 02:03
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Benchmark changes

Versions:

  • aube: 1.5.1 -> 1.5.2
  • pnpm: 11.0.2 -> 11.0.3

Public ratios: warm installs vs Bun 4x -> 4x; warm installs vs pnpm 5x -> 9x.

Benchmark aube bun pnpm
Fresh install (warm cache) 1021ms -> 276ms (-73%) 4134ms -> 1114ms (-73%) 4717ms -> 2414ms (-49%)
CI install (warm cache, GVS disabled) 2920ms -> 497ms (-83%) 3396ms -> 1104ms (-67%) 4864ms -> 2332ms (-52%)
CI install (cold cache, GVS disabled) 10801ms -> 4680ms (-57%) 10012ms -> 4677ms (-53%) 9722ms -> 5114ms (-47%)

2c90361 vs 60ff453 | aube/bun/pnpm | 3 scenarios | 3 runs | 500mbit/50ms | generated by Codex.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant