package console import ( "context" "log" "time" "github.com/carlosframework/platform/internal/release" ) // bumpSerial announces "the bucket has news" (carlos-deploy spec §3) after a // write a box loop consumes. Log-only on failure and clamped to 2s: a lost // bump costs latency (the hourly floor still converges), and no user-facing // write may fail or stall because its announcement did. // // Every call site sits strictly AFTER the handler's own successful write, next // to its audit row — never before a gate, and never in place of one. The // serial carries no authority at all (it is the one unsigned object in the // bucket); the worst a spurious bump can do is make a box run its ordinary // signature-verified pass set a little early. // // context.WithoutCancel is the whole point of the ctx handling here: the // caller's context is the REQUEST's, and by the time a bump runs the write it // announces has already landed and is irreversible. A client that hangs up in // that window (a CI runner killed mid-promote, a closed laptop, a proxy // timeout) would otherwise cancel the announcement for a change that is // permanently in the bucket — the fleet then waits out the hourly floor for a // deploy that already happened, with nothing in any log to say why. The 2s // clamp still applies, measured from here: detached from cancellation, not // from time. func (s *Server) bumpSerial(ctx context.Context) { if s.cfg.Releases == nil { return // no bucket on this console: nothing to announce into } ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second) defer cancel() if err := release.BumpSerial(ctx, s.cfg.Releases, time.Now()); err != nil { log.Printf("console: serial bump: %v (boxes fall back to the hourly pass)", err) } }