Repository navigation
Description
Problem
nodeup prints a permanent, unfixable "Detected an interrupted
upgrade" warning on every invocation, even though there is nothing
left to resume:
$ nodeup packages cleanup
⚠ Detected an interrupted upgrade (snapshot: .../snapshots/fnm-26.10.0.json, started: 2026-10-22T10:28:49+06:00).
⚠ To resume: `nodeup packages restore --from .../snapshots/fnm-26.10.0.json`
• No snapshot files found.
Following the hint fails, because the snapshot it points at no longer
exists:
$ nodeup packages restore --from .../snapshots/fnm-26.10.0.json
Error: restore failed: read snapshot ...: no such file or directory
And the warning never goes away on its own — nodeup packages cleanup
only touches <DataDir>/snapshots/ and <DataDir>/reports/, never the
sentinel at <DataDir>/upgrade-in-progress.json. The user is stuck
with a warning that points at a dead file and tells them to run a
command that cannot succeed.
Root cause
Two independent gaps that combine into an unresolvable state.
1. The sentinel outlives its snapshot
internal/cli/upgrade.go arms the sentinel
(packages.WriteSentinel) before the install step and clears it in the
deferred cleanup — but only when restoreSucceeded is true. On the
failure path the sentinel deliberately survives, because it is the
"resume this migration" breadcrumb.
That contract assumes the snapshot the sentinel points at also
survives. It does not: since v1.2.2 (#130) the upgrade flow
auto-reclaims the snapshot files it wrote once the migration has
replayed them (snapshotsConsumed → reclaimSnapshots), and
nodeup packages cleanup lets the user delete any snapshot by hand.
So a user who (a) had an upgrade that failed to restore, and then
(b) deleted the snapshot — via packages cleanup, via the auto-reclaim,
or via manually removing the file — ends up with a sentinel whose
SnapshotPath points at nothing. The warning then fires forever, and
the recovery command it suggests is guaranteed to fail.
This is exactly the state on the reporter's machine: the snapshot
directory is empty, but upgrade-in-progress.json is still there
from 2026-10-22.
2. The warning doesn't check whether its own advice is actionable
internal/cli/root.go's warnInterruptedUpgrade prints the resume
hint unconditionally whenever a sentinel file exists. It never verifies
that the referenced snapshot is still on disk, so it can never notice
that its own advice is dead.
Fix
Two parts:
-
warnInterruptedUpgradebecomes self-validating. Before
printing the resume hint, check that the sentinel'sSnapshotPath
still exists. If it doesn't, the sentinel is stale — say so, and
clear it, rather than printing a hint that cannot work. -
The stale-sentinel case gets a real recovery path. A user
whose snapshot is gone but who still wants the warning to go away
needs a way to dismiss it.nodeup packages cleanupshould also
offer to clear a stale sentinel.
Design notes
- A sentinel whose snapshot still exists keeps the current
behaviour exactly — that is a genuine interrupted upgrade and the
resume hint is correct. - The stale case must be loud enough that the user understands why
the warning disappeared (and that it was safe), but it must NOT
pretend an upgrade was interrupted when it wasn't. - Clearing is best-effort. A failure to delete the sentinel must not
break the user's actual command.
Acceptance criteria
-
nodeup <anything>with a stale sentinel (snapshot missing) does
not print the "Detected an interrupted upgrade" warning -
nodeup <anything>with a live sentinel (snapshot present) still
prints the warning and the resume hint - The stale sentinel is removed from disk so the warning does not
come back -
nodeup packages cleanupcan clear a stale sentinel on request - Tests cover both the stale and the live case
- No regression to the bug(packages): restore aborts on first failed install; migration report never written #46 / bug(packages): interrupted-upgrade sentinel lifecycle broken (deleted on failure, never cleared after manual restore) #47 sentinel-lifecycle contract
(a failed restore must still leave the sentinel armed)
Activity
chore(release): bump to v1.2.4 and refresh the version references (#144)