| 1 | // Canary TTL — the pipelines spec's second guard rail (§5 "Canaries |
| 2 | // expire"). Every canary/* channel carries a default 7-day lease, measured |
| 3 | // from the pointer's own PromotedAt: the pointer already carries that |
| 4 | // field, so this needs no new storage, and re-promoting a canary refreshes |
| 5 | // the lease, which is the right semantic — an actively-updated canary never |
| 6 | // expires, however old the branch it started from. |
| 7 | // |
| 8 | // Expiry is visibility, not enforcement, matching §5's "prod-wired" guard |
| 9 | // rail right above it: nothing here stops a route serving, nothing deletes |
| 10 | // a pointer, and promotion to or from an expired canary stays allowed. The |
| 11 | // hazard §5 names is not a canary in production — it's one that outlives |
| 12 | // the branch it was testing and quietly becomes permanent infrastructure |
| 13 | // nobody remembers deploying. Every caller that needs to know computes it |
| 14 | // fresh from the pointer already in hand; nothing persists an "expired" bit |
| 15 | // anywhere. |
| 16 | package release |
| 17 | |
| 18 | import ( |
| 19 | "fmt" |
| 20 | "strings" |
| 21 | "time" |
| 22 | ) |
| 23 | |
| 24 | // CanaryTTL is a canary/* channel's default lease length before it is |
| 25 | // flagged expired. Spec §5: "default 7 days". |
| 26 | const CanaryTTL = 7 * 24 * time.Hour |
| 27 | |
| 28 | // CanaryExpired reports whether p is a canary/* channel pointer whose lease |
| 29 | // (PromotedAt + CanaryTTL) has passed as of now. Always false for a channel |
| 30 | // outside canary/* — edge, beta and stable carry no TTL — and false for a |
| 31 | // nil pointer or a zero PromotedAt (an unpromoted or not-yet-read pointer): |
| 32 | // unknown age is never treated as proof of age. Exactly CanaryTTL old is |
| 33 | // NOT yet expired; the TTL names the lease's length. |
| 34 | func CanaryExpired(p *ChannelPointer, now time.Time) bool { |
| 35 | if p == nil || !strings.HasPrefix(p.Channel, RungCanary+"/") { |
| 36 | return false |
| 37 | } |
| 38 | if p.PromotedAt.IsZero() { |
| 39 | return false |
| 40 | } |
| 41 | return now.Sub(p.PromotedAt) > CanaryTTL |
| 42 | } |
| 43 | |
| 44 | // CanaryAge is how long PAST CanaryTTL an expired canary pointer is — the |
| 45 | // number both operator-facing surfaces (the adopt pass's nag and the |
| 46 | // console's badge) render, NOT the pointer's total age. Total age |
| 47 | // (now.Sub(p.PromotedAt)) is always exactly CanaryAge + CanaryTTL, and |
| 48 | // reporting IT as "how long has this been expired" overstates the lapse by |
| 49 | // one full lease length — a canary that missed its lease by an hour would |
| 50 | // read as a seven-day-old problem (2026-08-03 review, IMPORTANT 2). |
| 51 | // |
| 52 | // Meaningful only when CanaryExpired(p, now) is already true; call that |
| 53 | // first. A caller that skips the check gets a negative or otherwise |
| 54 | // nonsensical duration back for a fresh or non-canary pointer, the same as |
| 55 | // any bare Sub on the wrong side of zero. |
| 56 | func CanaryAge(p *ChannelPointer, now time.Time) time.Duration { |
| 57 | return now.Sub(p.PromotedAt) - CanaryTTL |
| 58 | } |
| 59 | |
| 60 | // FormatCanaryAge renders a CanaryAge duration for a human. It is the ONE |
| 61 | // formatter both the adopt pass's nag (internal/release/adopt.go) and the |
| 62 | // console's badge (internal/console/ladder.go) call, so the two |
| 63 | // operator-facing surfaces of this feature can never drift into disagreeing |
| 64 | // about what a duration means (2026-08-03 review, IMPORTANT 2) — the way |
| 65 | // they briefly did when the console rounded-and-bucketed a duration the nag |
| 66 | // printed raw and un-subtracted. |
| 67 | // |
| 68 | // "<1h" for anything under a full hour past the TTL — never "0h", which |
| 69 | // reads as "not actually expired" to whoever sees it first (2026-08-03 |
| 70 | // review, MINOR 7). "Nh" rounded to the nearest hour from one hour up to a |
| 71 | // day. "Nd" TRUNCATED (not rounded) at a day and beyond: nobody needs |
| 72 | // minute-level precision on a guard rail whose whole point is that nobody |
| 73 | // was watching the clock, and losing up to 23 hours off the day count is |
| 74 | // the deliberate coarseness that buys — nothing here mistakes a 47-hour |
| 75 | // lapse for two days. |
| 76 | func FormatCanaryAge(d time.Duration) string { |
| 77 | if d < time.Hour { |
| 78 | return "<1h" |
| 79 | } |
| 80 | d = d.Round(time.Hour) |
| 81 | if d < 24*time.Hour { |
| 82 | return fmt.Sprintf("%dh", int(d.Hours())) |
| 83 | } |
| 84 | return fmt.Sprintf("%dd", int(d.Hours())/24) |
| 85 | } |
| 86 | |