v0.263.0: a failed update puts the app back by itself (09 decision 15, R-637)
gates / gates (push) Successful in 26s
gates / gates (push) Successful in 26s
The guarded update gains a folder copy of the app's named volumes, taken after the pull where the app stops anyway (decision 19, chosen by the 2026-09-23 bake-off). On a failed health check the box undoes: every copy validated by its finished-marker first, volumes refilled, definition and pin from the job's own pre-update copies, the old version checked with the OLD .felhom.yml probe. It holds only if the undo fails, and the hold sentence says so and what state the data is in. Bind-mounted folders are never touched. - R-637 built; R-638/R-640/R-641 do not arise with a folder copy; R-639 (pre-update copies incl. .felhom.yml kept until the undo is over). - journal phases copying/undoing with power-cut recovery. - app.yaml last_update_undone + one line on the app page (hu/en). - R-642: start/restart never answer "completed". - Removal deletes kept undo copies. MinAgent unchanged (0.131.0). Nine red-proofs in REPORT.md. 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:
@@ -340,18 +340,7 @@ func (m *Manager) RestoreHoldFor(stack string) (bool, string) {
|
||||
// Slice 4: one storage, two reasons. An update hold names the copy it can be restored from; a
|
||||
// restore hold names nothing, because the restore it refers to already consumed the copy.
|
||||
if h.Reason == settings.HoldReasonUpdateFailed {
|
||||
copyDate := m.note("note.reconstitute.copy_latest")
|
||||
if h.CopyDate != "" {
|
||||
copyDate = fmtHoldTime(h.CopyDate)
|
||||
}
|
||||
// R-475: name the tier when the hold recorded one; an older hold keeps its own sentence.
|
||||
if label := UpdateTierLabel(h.CopyTier); label != "" && h.CopyDate != "" {
|
||||
if h.CopyHolds != "" { // R-479: name what the copy holds
|
||||
return true, fmt.Sprintf(UpdateHoldFmt, stack, fmtHoldTime(h.At), label, copyDate, h.CopyHolds)
|
||||
}
|
||||
return true, fmt.Sprintf(UpdateHoldTierFmt, stack, fmtHoldTime(h.At), label, copyDate)
|
||||
}
|
||||
return true, fmt.Sprintf(UpdateHoldLegacyFmt, stack, fmtHoldTime(h.At), copyDate)
|
||||
return true, m.undoHoldPrefix(h.UndoState) + m.updateHoldSentence(stack, h)
|
||||
}
|
||||
when := h.At
|
||||
if t, err := time.Parse(time.RFC3339, h.At); err == nil {
|
||||
@@ -360,6 +349,36 @@ func (m *Manager) RestoreHoldFor(stack string) (bool, string) {
|
||||
return true, m.note("note.reconstitute.held", stack, when)
|
||||
}
|
||||
|
||||
// undoHoldPrefix (v0.263.0) opens the hold sentence when the box already TRIED to undo the update and
|
||||
// that failed too: what was tried, then what state the data is in. "" when no undo was attempted, so
|
||||
// every hold written before v0.263.0 reads exactly as it did.
|
||||
func (m *Manager) undoHoldPrefix(state string) string {
|
||||
switch state {
|
||||
case "untouched", "half", "not_started":
|
||||
return m.note("hold.update.undo_failed") + " " + m.note("hold.update.undo_state."+state) + " "
|
||||
case "":
|
||||
return ""
|
||||
}
|
||||
m.logger.Printf("[WARN] [backup] unknown undo state %q on a hold — rendering the plain prefix", state)
|
||||
return m.note("hold.update.undo_failed") + " "
|
||||
}
|
||||
|
||||
// updateHoldSentence is the update hold's own sentence (slice 4, R-475, R-479), unchanged.
|
||||
func (m *Manager) updateHoldSentence(stack string, h settings.RestoreHold) string {
|
||||
copyDate := m.note("note.reconstitute.copy_latest")
|
||||
if h.CopyDate != "" {
|
||||
copyDate = fmtHoldTime(h.CopyDate)
|
||||
}
|
||||
// R-475: name the tier when the hold recorded one; an older hold keeps its own sentence.
|
||||
if label := UpdateTierLabel(h.CopyTier); label != "" && h.CopyDate != "" {
|
||||
if h.CopyHolds != "" { // R-479: name what the copy holds
|
||||
return fmt.Sprintf(UpdateHoldFmt, stack, fmtHoldTime(h.At), label, copyDate, h.CopyHolds)
|
||||
}
|
||||
return fmt.Sprintf(UpdateHoldTierFmt, stack, fmtHoldTime(h.At), label, copyDate)
|
||||
}
|
||||
return fmt.Sprintf(UpdateHoldLegacyFmt, stack, fmtHoldTime(h.At), copyDate)
|
||||
}
|
||||
|
||||
// holdAppAfterFailedRollback records the R-379/R-380 hold and makes sure nothing restarts the app
|
||||
// behind our back.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user