Repository navigation
fix(packages): the upgrade marker outlives its snapshot and warns forever - #142
Merged
Merged
Conversation
…ever The "Detected an interrupted upgrade" warning could fire on every nodeup invocation, pointing at a file that no longer existed and telling the user to run a command that was guaranteed to fail. The marker (<DataDir>/upgrade-in-progress.json) is deliberately left behind when a migration fails, so the user can replay it — that is the #46 contract. But the snapshot it points at does not necessarily survive: v1.2.2's auto-reclaim (#130) deletes it once the migration has replayed the packages, and `nodeup packages cleanup` lets the user delete any snapshot by hand. When that happened the marker outlived its payload, and the marker lives outside the snapshots directory, so `nodeup packages cleanup` never touched it and the user had no way to dismiss it. The warning now validates its own advice: - snapshot still present → genuine interrupted upgrade. Warning and resume hint unchanged; the marker is NOT cleared. - snapshot gone → the marker is stale. nodeup says so and clears it instead of printing a hint that cannot work. Clearing is race-safe. The marker is CLAIMED with an atomic rename before it is inspected, so a concurrent `nodeup upgrade` that arms a fresh marker in the window can never have it deleted out from under it — the fresh marker is put back untouched. Compare-then-delete would have a TOCTOU window there; the claim closes it. `nodeup packages cleanup --stale-sentinel` clears a stale marker on demand. It refuses to clear one whose snapshot still exists: that represents a genuine interrupted migration, and discarding it would lose the only breadcrumb telling the user how to replay their globals. Also fixes a test-hygiene wart this change exposed. A CLI test that built a root command ran against the developer's real $HOME, so it probed the real marker — and could now delete it. Running the suite silently wiped the marker on this machine. The test now redirects the data dir, and a new AST-based guard test fails any future test that constructs a root command without doing so. Closes #141
|
✔️ 963aa31 - Conventional commits check succeeded. |
This was referenced Nov 2, 2026
dipto0321
added a commit
that referenced
this pull request
Nov 2, 2026
Cuts the v1.2.4 patch release carrying the stale upgrade-marker fix (#141, merged in #142). The npm wrapper's `binaryVersion` moves in lockstep with the package version: it is the tag the postinstall script downloads from, so a mismatch would 404 every install. The CHANGELOG's [Unreleased] section becomes [1.2.4]; the release checklist's wrapper-verification steps now take the version as a variable so they don't need editing for every release. Refs #141 Co-authored-by: dipto0321 <dipto@local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The "Detected an interrupted upgrade" warning could fire on every
nodeupinvocation, pointing at a file that no longer existed andtelling the user to run a command that was guaranteed to fail.
The marker lived at
<DataDir>/upgrade-in-progress.json— outsidethe snapshots directory — so
nodeup packages cleanupnever touchedit and the user had no way to dismiss it.
Root cause
Two gaps that combine into an unresolvable state:
The marker outlives its snapshot. The marker is deliberately
left behind when a migration fails (the bug(packages): restore aborts on first failed install; migration report never written #46 contract — the user
must be able to replay it). But the snapshot it points at doesn't
necessarily survive: v1.2.2's auto-reclaim (fix(packages): stop the snapshot leak — auto-reclaim +
nodeup packages cleanup#130) deletes it oncethe migration has replayed the packages, and
nodeup packages cleanuplets the user delete any snapshot by hand.The warning never checked whether its own advice was
actionable. It printed the resume hint unconditionally, so it
could never notice that the file it pointed at was gone.
The fix
genuine interrupted upgrade, warning and hint unchanged, marker not
cleared. Snapshot gone → the marker is stale; nodeup says so and
clears it instead of printing a hint that cannot work.
rename before it's inspected, so a concurrent
nodeup upgradethatarms a fresh marker in the window can never have it deleted out from
under it — the fresh marker is put back untouched. Compare-then-
delete would have a TOCTOU window there; the claim closes it.
nodeup packages cleanup --stale-sentinelclears a stale markeron demand. It refuses to clear one whose snapshot still exists:
that represents a genuine interrupted migration, and discarding it
would lose the only breadcrumb telling the user how to replay their
global npm packages.
Test hygiene this change exposed
A CLI test that built a root command ran against the developer's real
$HOME, so it probed the real marker — and, after this change, coulddelete it. Running the suite silently wiped the marker on the
reporter's machine.
The test now redirects the data dir, and a new AST-based guard test
fails any future test that constructs a root command without doing so.
(A textual check would have been satisfied by the word
redirectSentinelDataDirappearing in a comment, so it parses the AST.)Linked issues
Closes #141
Refs #46
Refs #128
Test plan
make cigreen (vet + golangci-lint +go test -race ./...)internal/packages/stale_sentinel_test.goandinternal/cli/stale_sentinel_test.gocover the stale case, thelive case, the empty-path case, the directory case, the
compare-and-delete semantics, and a deterministic race test that
uses a seam to land the concurrent write exactly between our read
and our claim.
the stale marker with its missing snapshot, confirmed the warning
no longer fires and the marker is cleared, then confirmed the
live case still warns with the resume hint and does not clear.
removed from
version_test.go.