v0.237.0: the Update button takes a backup first, and tells the truth (update arc slice 4 — R-448, R-443, R-439)
gates / gates (push) Successful in 13s
gates / gates (push) Successful in 13s
POST /api/stacks/{name}/update is now a guarded job answering 202:
cheap refusals (hold — R-439, busy, migration, deploying, memory via the
deploy's own memoryVerdict, a fixed 2 GB disk floor, and no restorable
Tier-2 copy) → backup-first when the proven copy is older than
update.backup_max_age (24h) → safety dump BEFORE the pin moves → pin →
pull (failure puts the pin back) → up → health (.felhom.yml check or 60 s
settle, update.health_timeout 5m). Not healthy → the app is stopped and
HELD (RestoreHold reason update_failed, same store and gate as R-379) and
the page names the backup to restore from; the pin stays. Success is only
ever update_phase=done after health (R-443). UpdateStack is deleted.
The restorable-unit predicate is EXTRACTED to backup.Tier2UnitRestorePoint
and shared with the backups page (row pinned unchanged). The copy is aged
by the last successful Tier-2 copy, not the manifest created_at — measured
on demo-hp that created_at moves only on definition changes.
Crash safety: update-journal.json before each phase; RecoverUpdates before
the boot sweep, ResumeInterruptedUpdates after the guards are wired.
Three unattended start paths ignored a hold and now honour it: the
drive-return gate (restart + boot recreate) and the nightly volume dump.
The nightly capture and Tier-2 run skip held apps so the restore point
survives. No automatic rollback — measured per-app; route back = restore.
Tests A–H across stacks/backup/api/web/cmd; six red-proofs seen to fail.
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -407,7 +407,9 @@ func TestGroupE_RestartStackReachesTheRecorder(t *testing.T) {
|
||||
func TestGroupE_EveryBringUpPathCallsTheRecorder(t *testing.T) {
|
||||
callers := map[string]bool{} // enclosing func name -> calls recordInstalledImages
|
||||
fset := token.NewFileSet()
|
||||
for _, src := range []string{"manager.go", "deploy.go"} {
|
||||
// update.go since v0.237.0: the guarded update replaced UpdateStack, and it records in
|
||||
// verifyAndConclude — only after the app's health is known (slice 4).
|
||||
for _, src := range []string{"manager.go", "deploy.go", "update.go"} {
|
||||
f, err := parser.ParseFile(fset, src, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -433,7 +435,7 @@ func TestGroupE_EveryBringUpPathCallsTheRecorder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"StartStack", "RestartStack", "UpdateStack", "runComposeDeploy"} {
|
||||
for _, want := range []string{"StartStack", "RestartStack", "verifyAndConclude", "runComposeDeploy"} {
|
||||
if !callers[want] {
|
||||
t.Errorf("%s does not call recordInstalledImages — a bring-up path that records nothing leaves a stale record standing", want)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user