fix(packages): keep upgrade sentinel on restore failure; clear on manual restore success#81
Merged
Merged
Conversation
…ual restore success The upgrade-in-progress sentinel had a broken lifecycle in both directions (issue #46). On the `nodeup upgrade` side, the deferred `RemoveSentinel()` fired unconditionally once the sentinel had been armed, even when the post-install package restore had failed (restore failures were logged as warnings and the function returned normally). The user was left with installed Node versions, unmigrated global packages, and no "resume breadcrumb" for the manual `nodeup packages restore --from <path>` workflow that `PersistentPreRunE` advertises. The defer now gates on `restoreSucceeded`: only a successful restore clears the sentinel, a failed restore keeps it so the user can recover via the documented replay command. On the `nodeup packages restore` side — the exact command PersistentPreRunE's hint tells the user to run after an interrupted upgrade — a successful restore now calls `packages.RemoveSentinel()`. Without this, a user who followed the printed instructions to recover from a crash kept seeing "interrupted upgrade" warnings on every subsequent `nodeup` invocation forever. Sentinel-clearing failure there is logged, not returned: the user's primary goal (restored packages) is already achieved and a stale sentinel is cosmetic. Also corrects the CHANGELOG claim that the next run "prompts the user to replay" the migration — the implementation only prints a two-line stderr hint naming the snapshot path and the exact `nodeup packages restore --from <path>` command to copy. No interactive prompt is shown. Closes #46. Co-Authored-By: puku-ai-2.8 <noreply@puku.sh>
|
✔️ 6ff67ff - Conventional commits check succeeded. |
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 upgrade-in-progress sentinel had a broken lifecycle in both directions (issue #46). On the
nodeup upgradeside, the deferredRemoveSentinel()fired unconditionally once armed — even when the post-install package restore had failed (the restore step only logs a warning and returns normally). The user was left with installed Node versions, unmigrated global packages, and a deleted "resume breadcrumb," so the documented replay flow was a dead end.The fix gates the deferred cleanup on a new
restoreSucceededflag that only flips true whenpackages.RestoreFromSnapshotreturns nil. A failed restore keeps the sentinel on disk so a follow-upnodeup packages restore --from <sentinel path>(the commandPersistentPreRunE's hint already prints) can still find and replay it.The companion half of the bug: a successful manual
nodeup packages restorewas also failing to clear the sentinel. A user who followed the printed recovery instructions to completion kept seeing "interrupted upgrade" warnings on every subsequentnodeupinvocation forever. Both branches ofrunRestore(the--from <path>form and the<manager> <version>form) now callpackages.RemoveSentinel()after success. A removal failure is logged, not returned: the user's primary goal (restored packages) is achieved and a stale sentinel is cosmetic.CHANGELOG wording fix: the previous "prompts the user to replay" claim is replaced with a faithful description of the actual UX (two-line stderr hint naming the snapshot path and the exact
nodeup packages restore --from <path>command to copy). No interactive prompt is shipped.Closes #46.
Linked issues
Type of change
fix— bug fix (PATCH bump)Checklist
fix(scope): subject)make cilocally and it passes (fmt, vet, test pass locally;make lintfails on the v2→v1 schema mismatch tracked in build(lint): .golangci.yml v1 schema incompatible with golangci-lint v2 (brew installs v2, make lint fails to even parse config) #62 — pre-existing onmain, independent of this fix; CI runs golangci-lint v1.64.8 per.github/workflows/ci.ymland parses the config fine)internal/packages/sentinel_test.go::TestRemoveSentinel_OnlyOnRestoreSuccessregression pin: write sentinel → assert orphaned → RemoveSentinel → assert cleared; documents the contract that the cli/upgrade.go defer depends on)docs/, inline godoc)Scope notes
runUpgradedefer pattern now reads as:sentinelArmedstill flips onWriteSentinelsuccess (the only way to ever touch the sentinel-armed flag).restoreSucceededflips inside theif !skipMigrate && len(toInstall) > 0block, only on thenilbranch ofRestoreFromSnapshot. A nil sentinel-armed + nil restoreSucceeded (the dry-run path) leaves the file untouched; a successful migration clears it; a failed migration leaves it for the user to resolve.nodeup upgradeis intentionally not added here — that would be a separate UX feature (an actual interactive prompt + auto-replay), and the issue explicitly accepts the wording fix as one of the two acceptable resolutions. Out of scope.runUpgrade(via a stub manager) and asserts sentinel state on both the success and failure paths. It would require factoring the restore logic out of runUpgrade (currently inlined as a 6-line block), which is a bigger refactor than this fix wants. The packages-level test pin + the post-fix manual verification path (nodeup upgrade→ kill mid-restore → check sentinel exists →nodeup packages restore --from <path>→ check sentinel gone) cover the contract.internal/cli/root.go'swarnInterruptedUpgrade— itsfmt.Fprintf(os.Stderr, ...)two-line hint format is correct; the CHANGELOG claim was the wrong half of the bug, not the implementation.Out of scope (separate issues)
internal/cli/packages.gostill usescontext.Background()at line ~141 forRestore— fix(cli): context.Background() used instead of cmd.Context() in packages.go (contextcheck violation) #49.nodeup upgrade's restore loop does not have HTTP timeout/retry (manifest fetch) — bug(node): no HTTP timeout/retry/context on nodejs.org manifest fetch; cache write not atomic #48.--cleanup+--yesconflict — bug(cli): --yes silently overrides --no-cleanup, auto-deletes with zero confirmation #57.packages restore/diffvia unsanitized manager name — fix(packages): path traversal via unsanitized manager name in packages restore/diff #51.asdf ASDF_DIR vs ASDF_DATA_DIRinconsistency — fix(detector): asdf uses ASDF_DIR vs ASDF_DATA_DIR inconsistently between docs, detector, and system-node classifier #50.Screenshots / output
User-visible behavior change:
Before:
nodeup upgrade; one of thenpm install -gcalls errors out; restore logsWarning: restore failed: …; function returns success; deferredRemoveSentinel()fires; nextnodeup upgradefinds no sentinel; user has no documented path to retry the migration despite the snapshot file still being on disk.nodeup upgrade; crashes mid-upgrade; user follows the printed hint:nodeup packages restore --from /path/to/snapshot.json; packages restore completes successfully; user runs any othernodeupcommand; seesDetected an interrupted upgrade…warning again, repeating forever.After:
nodeup upgrade; restore fails; nextnodeupinvocation printsDetected an interrupted upgrade (snapshot: …)and the suggestednodeup packages restore --from …command, allowing recovery.nodeupinvocations are silent.Verification path: in either scenario, capture
ls $(nodeup-config --datadir 2>/dev/null || echo ~/.local/share/nodeup)/upgrade-in-progress.jsonbefore/after restore to confirm file presence/absence matches the new contract.