controller v0.268.0: the undo finds volumes by definition; a held app names only a whole copy; one press = one tested step (R-658, R-659, R-660, R-651; 09 §6.4 part 5)
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
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:
@@ -292,6 +292,9 @@ func fillHoldReason(g UpdateGuards, st *Stack) {
|
||||
if g != nil && st.Deployed {
|
||||
if h, why := g.HoldFor(st.Name); h {
|
||||
st.HoldReason, held = why, true
|
||||
if nw, ok := g.(holdWholeCopy); ok {
|
||||
st.HoldNoWholeCopy = nw.HoldNoWholeCopy(st.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
// R-480: an update that ended HELD carries the hold's sentence as its UpdateError. Once that hold
|
||||
@@ -304,6 +307,11 @@ func fillHoldReason(g UpdateGuards, st *Stack) {
|
||||
}
|
||||
}
|
||||
|
||||
// holdWholeCopy is the OPTIONAL half of UpdateGuards that says a hold names no copy (R-659).
|
||||
type holdWholeCopy interface {
|
||||
HoldNoWholeCopy(name string) bool
|
||||
}
|
||||
|
||||
// UpdateRefusal is a refusal taken before anything moved. Reason is a stable key for logs and tests;
|
||||
// Message is the customer sentence.
|
||||
type UpdateRefusal struct {
|
||||
@@ -700,6 +708,19 @@ func (m *Manager) runGuardedUpdate(ctx context.Context, name string) {
|
||||
fail("update.error.no_guards", "no UpdateGuards wired")
|
||||
return
|
||||
}
|
||||
// v0.268.0 — THE LADDER (`09` §3 decision 14): which definition this ONE press pins. Decided
|
||||
// first, before anything moves, so a step the catalog promises and does not carry refuses here
|
||||
// rather than jumping past it. An unpinned app is left to today's behaviour (advancePinTo no-ops).
|
||||
stepSrc := m.CatalogTemplatePath(name, "docker-compose.yml")
|
||||
if cfg := LoadAppConfig(dir); cfg != nil && len(cfg.PinnedImages) > 0 {
|
||||
step, serr := nextLadderStep(filepath.Dir(stepSrc), cfg.PinnedImages)
|
||||
if serr != nil {
|
||||
fail("update.error.pin_failed", "update ladder: "+serr.Error())
|
||||
return
|
||||
}
|
||||
stepSrc = step.Source
|
||||
m.logger.Printf("[INFO] [stacks] update %s: ladder — %s", name, step.Why)
|
||||
}
|
||||
// R-475: the precondition is a copy on ANY tier, chosen in the order 2, 1, 3, and the age rule
|
||||
// applies to whichever tier is chosen. The first FRESH copy wins — not merely the first copy — so a
|
||||
// stale second-drive mirror never forces a backup while the app's own unit is minutes old.
|
||||
@@ -743,7 +764,7 @@ func (m *Manager) runGuardedUpdate(ctx context.Context, name string) {
|
||||
|
||||
// v0.263.0 — the undo's copy is PLANNED here, before anything moves: its size against the disk
|
||||
// floor (decision 19's limit). The copy itself is taken after the pull, where the app stops anyway.
|
||||
undoVols, perr := m.planUndoCopies(name)
|
||||
undoVols, perr := m.planUndoCopies(name, dir)
|
||||
if perr != nil {
|
||||
if se, ok := perr.(*undoSpaceError); ok {
|
||||
fail("err.stacks.update_undo_space", "undo copy: "+perr.Error(), se.need, se.free, updateDiskFloorGiB)
|
||||
@@ -792,7 +813,7 @@ func (m *Manager) runGuardedUpdate(ctx context.Context, name string) {
|
||||
fail("update.error.journal_failed", "journal write failed")
|
||||
return
|
||||
}
|
||||
if err := m.advancePinToCatalog(name, dir); err != nil {
|
||||
if err := m.advancePinTo(name, dir, stepSrc); err != nil {
|
||||
m.pinBack(name, dir, entry)
|
||||
fail("update.error.pin_failed", "advancing the pin: "+err.Error())
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user