carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// The channel ladder — the platform's frozen vocabulary for build maturity
2// (pipelines spec §3).
3//
4// canary/<name> a specific change, shipped from a worktree or a branch.
5// ↓ No gate in and no gate out: canary is where work happens.
6// edge merged to main. Nothing is promoted ONTO edge — merging is
7// ↓ what puts a build there.
8// beta promoted from edge on sign-off.
9// ↓
10// stable promoted from beta on sign-off, after the bake window.
11//
12// Ordering is enforced from edge upward only, and `carlos promote -hotfix` is
13// the one bypass — an explicit, recorded one, because a bypass that is
14// invisible is indistinguishable from a broken gate.
15//
16// This file is vocabulary and rules, nothing else: it touches no store, no
17// pointer and no route. In particular, promotion moves a signed channel
18// pointer and NEVER changes a route's environment — the ladder proves nothing
19// if beta and stable can be wired to different glue.
20package release
21
22import (
23 "fmt"
24 "strings"
25)
26
27// The rungs. Frozen: a channel name outside this vocabulary (plus canary's
28// nested names) is not a rung, and promotion onto it is not a promotion.
29const (
30 RungCanary = "canary"
31 RungEdge = "edge"
32 RungBeta = "beta"
33 RungStable = "stable"
34)
35
36// rungs is the ladder, bottom first — the order RungIndex reports.
37var rungs = []string{RungCanary, RungEdge, RungBeta, RungStable}
38
39// RungIndex is a rung's height: 0 canary, 1 edge, 2 beta, 3 stable, -1 for
40// anything that is not a rung name (including a channel name like
41// "canary/x" — that is Rung's job).
42func RungIndex(rung string) int {
43 for i, r := range rungs {
44 if r == rung {
45 return i
46 }
47 }
48 return -1
49}
50
51// Rung reports which rung a CHANNEL name belongs to: "stable" -> "stable",
52// "canary/keymail" -> "canary". Only canary nests (a canary is a named
53// change, so it needs a name); anything else is a single segment, and a name
54// outside the vocabulary belongs to no rung at all.
55func Rung(channel string) (string, bool) {
56 if !ValidChannel(channel) {
57 return "", false
58 }
59 name, _, nested := strings.Cut(channel, "/")
60 if nested && name != RungCanary {
61 return "", false
62 }
63 if RungIndex(name) < 0 {
64 return "", false
65 }
66 return name, true
67}
68
69// PromotionAllowed reports whether a build may move onto rung `to`, given the
70// rung it currently occupies (`from` — the highest, if several; "" when it is
71// on none). nil means go ahead; every refusal names -hotfix, because the
72// bypass has to be as discoverable as the gate.
73//
74// Legal: edge -> beta, beta -> stable, and a build that is on no rung yet
75// entering at edge. Canary is unconditional in both directions of the
76// argument list — production runs on canary/* channels, and nothing about
77// maturity may stop a canary shipping.
78func PromotionAllowed(from, to string) error {
79 if RungIndex(to) < 0 {
80 return fmt.Errorf("%q is not a rung (%s) — use --hotfix to point a channel outside the ladder",
81 to, strings.Join(rungs, ", "))
82 }
83 switch {
84 case to == RungCanary:
85 // No gate in: a canary is proven BEFORE it is a candidate.
86 return nil
87 case to == RungEdge && from == "":
88 // A fresh build entering the ladder — the merge that produced it is
89 // the gate, and this is what an automatic edge promotion does.
90 return nil
91 case to == RungBeta && from == RungEdge:
92 return nil
93 case to == RungStable && from == RungBeta:
94 return nil
95 }
96 if to == RungEdge {
97 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",
98 describeRung(from))
99 }
100 return fmt.Errorf("%s is entered from %s only, and this build is on %s — use --hotfix to bypass the ladder",
101 to, rungs[RungIndex(to)-1], describeRung(from))
102}
103
104// describeRung names a rung for a human, including the "on nothing yet" case.
105func describeRung(rung string) string {
106 if rung == "" {
107 return "no rung yet"
108 }
109 return rung
110}
111