Skip to content

fix(packages): the interrupted-upgrade sentinel outlives its snapshot and warns forever about a dead file #141

Closed

Description

@dipto0321

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:

  1. warnInterruptedUpgrade becomes self-validating. Before
    printing the resume hint, check that the sentinel's SnapshotPath
    still exists. If it doesn't, the sentinel is stale — say so, and
    clear it, rather than printing a hint that cannot work.

  2. 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 cleanup should 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

Refs #47
Refs #128

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions