// The channel ladder — the platform's frozen vocabulary for build maturity // (pipelines spec §3). // // canary/ a specific change, shipped from a worktree or a branch. // ↓ No gate in and no gate out: canary is where work happens. // edge merged to main. Nothing is promoted ONTO edge — merging is // ↓ what puts a build there. // beta promoted from edge on sign-off. // ↓ // stable promoted from beta on sign-off, after the bake window. // // Ordering is enforced from edge upward only, and `carlos promote -hotfix` is // the one bypass — an explicit, recorded one, because a bypass that is // invisible is indistinguishable from a broken gate. // // This file is vocabulary and rules, nothing else: it touches no store, no // pointer and no route. In particular, promotion moves a signed channel // pointer and NEVER changes a route's environment — the ladder proves nothing // if beta and stable can be wired to different glue. package release import ( "fmt" "strings" ) // The rungs. Frozen: a channel name outside this vocabulary (plus canary's // nested names) is not a rung, and promotion onto it is not a promotion. const ( RungCanary = "canary" RungEdge = "edge" RungBeta = "beta" RungStable = "stable" ) // rungs is the ladder, bottom first — the order RungIndex reports. var rungs = []string{RungCanary, RungEdge, RungBeta, RungStable} // RungIndex is a rung's height: 0 canary, 1 edge, 2 beta, 3 stable, -1 for // anything that is not a rung name (including a channel name like // "canary/x" — that is Rung's job). func RungIndex(rung string) int { for i, r := range rungs { if r == rung { return i } } return -1 } // Rung reports which rung a CHANNEL name belongs to: "stable" -> "stable", // "canary/keymail" -> "canary". Only canary nests (a canary is a named // change, so it needs a name); anything else is a single segment, and a name // outside the vocabulary belongs to no rung at all. func Rung(channel string) (string, bool) { if !ValidChannel(channel) { return "", false } name, _, nested := strings.Cut(channel, "/") if nested && name != RungCanary { return "", false } if RungIndex(name) < 0 { return "", false } return name, true } // PromotionAllowed reports whether a build may move onto rung `to`, given the // rung it currently occupies (`from` — the highest, if several; "" when it is // on none). nil means go ahead; every refusal names -hotfix, because the // bypass has to be as discoverable as the gate. // // Legal: edge -> beta, beta -> stable, and a build that is on no rung yet // entering at edge. Canary is unconditional in both directions of the // argument list — production runs on canary/* channels, and nothing about // maturity may stop a canary shipping. func PromotionAllowed(from, to string) error { if RungIndex(to) < 0 { return fmt.Errorf("%q is not a rung (%s) — use --hotfix to point a channel outside the ladder", to, strings.Join(rungs, ", ")) } switch { case to == RungCanary: // No gate in: a canary is proven BEFORE it is a candidate. return nil case to == RungEdge && from == "": // A fresh build entering the ladder — the merge that produced it is // the gate, and this is what an automatic edge promotion does. return nil case to == RungBeta && from == RungEdge: return nil case to == RungStable && from == RungBeta: return nil } if to == RungEdge { return fmt.Errorf("nothing is promoted onto edge — merging to main is what puts a build there, and this one is on %s; use --hotfix to override", describeRung(from)) } return fmt.Errorf("%s is entered from %s only, and this build is on %s — use --hotfix to bypass the ladder", to, rungs[RungIndex(to)-1], describeRung(from)) } // describeRung names a rung for a human, including the "on nothing yet" case. func describeRung(rung string) string { if rung == "" { return "no rung yet" } return rung }