| index a86ea7060..ee3dd26fe 100644 |
| --- a/internal/release/adopt.go |
| +++ b/internal/release/adopt.go |
| @@ -13,6 +13,7 @@ import ( |
| "io" |
| "os" |
| "runtime" |
| + "strings" |
| "syscall" |
| "time" |
| |
| @@ -338,12 +339,18 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551 |
| // here, the same cost property the production-flag reads below hold. |
| // |
| // A read/verify ERROR is kept, and kept apart from verified-absent |
| - // (nil, nil): the errored app falls to the legacy path below — |
| - // DefaultHolds plus the production-flag machinery, the same fail-closed |
| - // posture an old binary that cannot parse pipeline.json at all takes |
| - // (spec §3's old-binary edge; its mitigation, a nonzero-bake pipeline |
| - // also setting production: true, is what makes legacy the closed |
| - // direction). The error is NEVER folded into the ratchet as a nil: nil |
| + // (nil, nil): the errored app's windows fall back to DefaultHolds |
| + // FLOORED by the box's own ratchet memory (the appHolds loop below) — |
| + // every channel the memory carries enforces max(its DefaultHolds |
| + // resolution, the remembered window), so corrupting the object can |
| + // never make this box looser than it already was (spec §3: corruption |
| + // must never loosen; plain DefaultHolds would hand a tightened rung |
| + // name like edge its ZERO legacy window back). A channel with no memory |
| + // keeps the fail-closed DefaultHolds resolution — the old-binary |
| + // posture (spec §3's old-binary edge), whose production: true co-write |
| + // remains the mitigation for a MEMORY-LESS box only. The production- |
| + // flag machinery still applies to an errored app exactly as to any |
| + // legacy app. The error is NEVER folded into the ratchet as a nil: nil |
| // means verified-absent only (ObservePipeline's contract), and a |
| // corrupt object must not masquerade as a deletion. |
| type pipeState struct { |
| @@ -405,22 +412,56 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551 |
| } |
| |
| // One enforced-window map per app for this pass. A verified pipeline |
| - // resolves through the box's ratchet memory (HoldsForApp); everything |
| - // else — no pipeline, or a read this pass could not trust — keeps |
| - // DefaultHolds, today's behaviour byte-for-byte. canary/* is stitched |
| - // back in at zero: it lives outside the pipeline (spec §2), so |
| - // HoldsForApp's map cannot carry it, and HoldFor's fallback for a name |
| - // missing from the map is the 72h fail-closed default — which would |
| - // silently freeze every canary deploy of a pipelined app. Every OTHER |
| - // channel outside the pipeline does take that 72h (spec §3: a channel |
| - // with no pipeline entry keeps the fail-closed default), including rung |
| - // names like beta whose DefaultHolds entry no longer applies. |
| + // resolves through the box's ratchet memory (HoldsForApp); an ERRORED |
| + // read keeps DefaultHolds floored by that same memory (never looser |
| + // than what this box already enforced — see the pipes comment above); |
| + // an app with no pipeline at all keeps DefaultHolds, today's behaviour |
| + // byte-for-byte. canary/* is stitched back in at zero on the verified |
| + // path: it lives outside the pipeline (spec §2), so HoldsForApp's map |
| + // cannot carry it, and HoldFor's fallback for a name missing from the |
| + // map is the 72h fail-closed default — which would silently freeze |
| + // every canary deploy of a pipelined app. Every OTHER channel outside |
| + // the pipeline does take that 72h (spec §3: a channel with no pipeline |
| + // entry keeps the fail-closed default), including rung names like beta |
| + // whose DefaultHolds entry no longer applies. |
| appHolds := map[acctApp]map[string]time.Duration{} |
| for ak, ps := range pipes { |
| - if ps.err == nil && ps.p != nil { |
| + switch { |
| + case ps.err == nil && ps.p != nil: |
| h := bake.HoldsForApp(ak.acct, ak.app, ps.p) |
| h["canary/*"] = 0 |
| appHolds[ak] = h |
| + case ps.err != nil: |
| + // The memory floor. Overrides overlay a COPY of DefaultHolds, |
| + // so a remembered channel is only ever RAISED above its legacy |
| + // resolution, an unremembered one resolves exactly as legacy |
| + // (HoldFor's 72h unknown-name fallback included), and canary/* |
| + // keeps its zero: no pipeline can name canary, so no ratchet |
| + // memory exists to raise it. With nothing remembered above |
| + // legacy, no map is built and holdsFor falls to DefaultHolds |
| + // itself. The prefix match is exact by construction: neither an |
| + // account, an app name nor a non-canary channel can contain |
| + // clockSep or "/" (channelClockKey's own injectivity argument). |
| + var h map[string]time.Duration |
| + prefix := holdKeyPrefix(ak.acct, ak.app) |
| + for key, mem := range bake.Holds { |
| + name := strings.TrimPrefix(key, prefix) |
| + if name == key || name == "" { |
| + continue |
| + } |
| + if mem.Window > HoldFor(DefaultHolds, name) { |
| + if h == nil { |
| + h = make(map[string]time.Duration, len(DefaultHolds)+1) |
| + for ch, d := range DefaultHolds { |
| + h[ch] = d |
| + } |
| + } |
| + h[name] = mem.Window |
| + } |
| + } |
| + if h != nil { |
| + appHolds[ak] = h |
| + } |
| } |
| } |
| holdsFor := func(ak acctApp) map[string]time.Duration { |
| @@ -561,7 +602,8 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551 |
| // pipeline's alone (spec §3) — never ignore, and a zero-window |
| // channel holds nothing anyway. The flag stays meaningful for |
| // legacy apps, and for THIS app on a pass whose pipeline read |
| - // errored, which falls to exactly that legacy machinery. |
| + // errored, which takes exactly that legacy verdict path (its |
| + // WINDOWS are memory-floored above; the verdict is unchanged). |
| if ps := pipes[ak]; ps.err == nil && ps.p != nil { |
| return false |
| } |
| @@ -694,7 +736,9 @@ func RouteAccount(rt registry.Route) string { |
| // |
| // holds is the enforced per-channel window map the caller resolved for this |
| // route's app: a verified pipeline's ratcheted HoldsForApp map (canary/* |
| -// stitched back in at zero), else DefaultHolds — never consulted app-blind. |
| +// stitched back in at zero), DefaultHolds floored by the ratchet memory for |
| +// an app whose pipeline read errored, else DefaultHolds — never consulted |
| +// app-blind. |
| // |
| // ignore is the app-level bake verdict from ProductionBake — the LEGACY |
| // production flag's doing, never a publisher field, and constant false for |