// Canary TTL — the pipelines spec's second guard rail (§5 "Canaries // expire"). Every canary/* channel carries a default 7-day lease, measured // from the pointer's own PromotedAt: the pointer already carries that // field, so this needs no new storage, and re-promoting a canary refreshes // the lease, which is the right semantic — an actively-updated canary never // expires, however old the branch it started from. // // Expiry is visibility, not enforcement, matching §5's "prod-wired" guard // rail right above it: nothing here stops a route serving, nothing deletes // a pointer, and promotion to or from an expired canary stays allowed. The // hazard §5 names is not a canary in production — it's one that outlives // the branch it was testing and quietly becomes permanent infrastructure // nobody remembers deploying. Every caller that needs to know computes it // fresh from the pointer already in hand; nothing persists an "expired" bit // anywhere. package release import ( "fmt" "strings" "time" ) // CanaryTTL is a canary/* channel's default lease length before it is // flagged expired. Spec §5: "default 7 days". const CanaryTTL = 7 * 24 * time.Hour // CanaryExpired reports whether p is a canary/* channel pointer whose lease // (PromotedAt + CanaryTTL) has passed as of now. Always false for a channel // outside canary/* — edge, beta and stable carry no TTL — and false for a // nil pointer or a zero PromotedAt (an unpromoted or not-yet-read pointer): // unknown age is never treated as proof of age. Exactly CanaryTTL old is // NOT yet expired; the TTL names the lease's length. func CanaryExpired(p *ChannelPointer, now time.Time) bool { if p == nil || !strings.HasPrefix(p.Channel, RungCanary+"/") { return false } if p.PromotedAt.IsZero() { return false } return now.Sub(p.PromotedAt) > CanaryTTL } // CanaryAge is how long PAST CanaryTTL an expired canary pointer is — the // number both operator-facing surfaces (the adopt pass's nag and the // console's badge) render, NOT the pointer's total age. Total age // (now.Sub(p.PromotedAt)) is always exactly CanaryAge + CanaryTTL, and // reporting IT as "how long has this been expired" overstates the lapse by // one full lease length — a canary that missed its lease by an hour would // read as a seven-day-old problem (2026-08-03 review, IMPORTANT 2). // // Meaningful only when CanaryExpired(p, now) is already true; call that // first. A caller that skips the check gets a negative or otherwise // nonsensical duration back for a fresh or non-canary pointer, the same as // any bare Sub on the wrong side of zero. func CanaryAge(p *ChannelPointer, now time.Time) time.Duration { return now.Sub(p.PromotedAt) - CanaryTTL } // FormatCanaryAge renders a CanaryAge duration for a human. It is the ONE // formatter both the adopt pass's nag (internal/release/adopt.go) and the // console's badge (internal/console/ladder.go) call, so the two // operator-facing surfaces of this feature can never drift into disagreeing // about what a duration means (2026-08-03 review, IMPORTANT 2) — the way // they briefly did when the console rounded-and-bucketed a duration the nag // printed raw and un-subtracted. // // "<1h" for anything under a full hour past the TTL — never "0h", which // reads as "not actually expired" to whoever sees it first (2026-08-03 // review, MINOR 7). "Nh" rounded to the nearest hour from one hour up to a // day. "Nd" TRUNCATED (not rounded) at a day and beyond: nobody needs // minute-level precision on a guard rail whose whole point is that nobody // was watching the clock, and losing up to 23 hours off the day count is // the deliberate coarseness that buys — nothing here mistakes a 47-hour // lapse for two days. func FormatCanaryAge(d time.Duration) string { if d < time.Hour { return "<1h" } d = d.Round(time.Hour) if d < 24*time.Hour { return fmt.Sprintf("%dh", int(d.Hours())) } return fmt.Sprintf("%dd", int(d.Hours())/24) }