R-645: the night backup skips an app whose pinned version is not the one it runs (09 decision 142)

Every night leg (DB dump, volume dump, recovery-unit capture, Tier-2 mirror) now leaves alone an app
whose app.yaml pin (pinned_images) differs from its running record (installed_images) - the state a
failed update leaves behind. A hold lifted by hand (--clear-restore-hold + restart) no longer lets the
capture write the just-failed definition over the good unit. Unknown (no pin, no record, a service not
observed) never skips. The log says it per leg; the backups page shows one amber line, hu + en
(backup.status.version_skip). Seam: backup.Manager.SetVersionCheck <- stacks.Manager.PinNotRunning.

Tests: TestR645_HandLiftedHoldKeepsTheGoodUnit (whole night run + Tier 2, unit tree fingerprint),
TestR645_VersionSkipSentence, TestR645_PinNotRunning_*, TestR645_BackupRowSaysTheNightBackupSkipsIt,
TestR645_VersionCheckIsWiredAtStartup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-06 11:30:01 +02:00
parent 13bda270c3
commit 2d63714eca
15 changed files with 356 additions and 1 deletions
+5
View File
@@ -464,6 +464,11 @@ func (m *Manager) RunAllTier2() {
m.logger.Printf("[WARN] [backup] Tier 2 skipped for %s — the app is HELD; its copy is the restore point and is preserved", stack.Name)
continue
}
// R-645: the mirror of an app not running its pinned version stays as it is.
if why, skip := m.versionSkip(stack.Name); skip {
m.logger.Printf("[WARN] [backup] Tier 2 skipped for %s — it is not running its pinned version (%s); the last good copy is kept (R-645)", stack.Name, why)
continue
}
runOne := m.perAppTier2
if runOne == nil {
runOne = m.RunTier2