carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "crypto/ed25519"
6 "errors"
7 "fmt"
8 "os"
9 "strings"
10 "testing"
11)
12
13func TestRungOfChannel(t *testing.T) {
14 cases := []struct {
15 channel string
16 rung string
17 ok bool
18 }{
19 {"stable", RungStable, true},
20 {"beta", RungBeta, true},
21 {"edge", RungEdge, true},
22 {"canary", RungCanary, true},
23 {"canary/keymail", RungCanary, true},
24 {"canary/platform-adoption", RungCanary, true},
25 {"canary/rehearsal", RungCanary, true},
26 {"canary/x", RungCanary, true},
27 // Only canary nests, and only names in the frozen vocabulary are rungs.
28 {"beta/x", "", false},
29 {"stable/x", "", false},
30 {"dev", "", false},
31 {"wip", "", false},
32 {"", "", false},
33 {"canary/", "", false},
34 {"canary/a/b", "", false},
35 }
36 for _, c := range cases {
37 got, ok := Rung(c.channel)
38 if got != c.rung || ok != c.ok {
39 t.Errorf("Rung(%q) = (%q, %v), want (%q, %v)", c.channel, got, ok, c.rung, c.ok)
40 }
41 }
42}
43
44func TestRungIndexOrdersTheLadder(t *testing.T) {
45 if RungIndex(RungCanary) != 0 || RungIndex(RungEdge) != 1 ||
46 RungIndex(RungBeta) != 2 || RungIndex(RungStable) != 3 {
47 t.Fatalf("ladder order wrong: canary=%d edge=%d beta=%d stable=%d",
48 RungIndex(RungCanary), RungIndex(RungEdge), RungIndex(RungBeta), RungIndex(RungStable))
49 }
50 for _, unknown := range []string{"", "dev", "prod", "canary/x"} {
51 if got := RungIndex(unknown); got != -1 {
52 t.Errorf("RungIndex(%q) = %d, want -1", unknown, got)
53 }
54 }
55}
56
57func TestPromotionAllowedLegalTransitions(t *testing.T) {
58 legal := []struct{ from, to string }{
59 // The two ordered steps, which is the whole ladder above edge.
60 {RungEdge, RungBeta},
61 {RungBeta, RungStable},
62 // A fresh build enters the ladder at edge (merging put it there).
63 {"", RungEdge},
64 // Canary has no gate in: production runs on canary/* channels today
65 // and shipping to one is never refused, whatever the build is on.
66 {"", RungCanary},
67 {RungCanary, RungCanary},
68 {RungEdge, RungCanary},
69 {RungBeta, RungCanary},
70 {RungStable, RungCanary},
71 }
72 for _, c := range legal {
73 if err := PromotionAllowed(c.from, c.to); err != nil {
74 t.Errorf("PromotionAllowed(%q, %q) = %v, want nil", c.from, c.to, err)
75 }
76 }
77}
78
79func TestPromotionAllowedRefusesSkipsAndNamesHotfix(t *testing.T) {
80 refused := []struct{ from, to string }{
81 {RungEdge, RungStable}, // the skip the ladder exists to stop
82 {"", RungBeta}, // straight onto beta, having been nowhere
83 {"", RungStable}, // straight onto stable, having been nowhere
84 {RungCanary, RungEdge}, // nothing is promoted from canary to edge
85 {RungCanary, RungBeta}, // ... nor past it
86 {RungCanary, RungStable}, //
87 {RungStable, RungBeta}, // no going back down the ladder
88 {RungBeta, RungEdge}, //
89 {RungEdge, RungEdge}, // same-rung re-promotion of another build
90 {RungBeta, RungBeta}, //
91 {RungStable, RungStable}, //
92 {RungEdge, "dev"}, // not a rung at all
93 {RungEdge, ""}, //
94 }
95 for _, c := range refused {
96 err := PromotionAllowed(c.from, c.to)
97 if err == nil {
98 t.Errorf("PromotionAllowed(%q, %q) = nil, want a refusal", c.from, c.to)
99 continue
100 }
101 if !strings.Contains(err.Error(), "-hotfix") {
102 t.Errorf("PromotionAllowed(%q, %q) = %q, want the refusal to name --hotfix", c.from, c.to, err)
103 }
104 }
105}
106
107// --- Stable promotion cuts the version tag (release versioning spec) ---
108
109// shipLabelledIteration ships a real one-artifact release carrying the human
110// half of a manifest, so a tag cut from it has a non-empty changelog. It goes
111// through Ship (not a hand-written manifest) so the artifact sha256s are the
112// real thing and validateManifest is exercised.
113func shipLabelledIteration(t *testing.T, s Store, acct, app, version, label, notes string) {
114 t.Helper()
115 m := Manifest{
116 App: app, Version: version, Kind: "binary",
117 Commit: "c-" + version, Label: label, Notes: notes,
118 Artifacts: map[string]Artifact{"linux-arm64": {}},
119 }
120 files := map[string][]byte{"linux-arm64": []byte("build " + version)}
121 if err := Ship(context.Background(), s, acct, m, files); err != nil {
122 t.Fatalf("Ship %s/%s: %v", app, version, err)
123 }
124}
125
126// climbToBeta walks a version up the ladder the way an operator does, so the
127// stable promote under test meets a real edge->beta->stable history.
128func climbToBeta(t *testing.T, ctx context.Context, s Store, priv ed25519.PrivateKey, acct, app, version string) {
129 t.Helper()
130 for _, ch := range []string{"edge", "beta"} {
131 if _, err := PromoteOnLadder(ctx, s, priv, acct, app, ch, version, PromoteOptions{}); err != nil {
132 t.Fatalf("promote %s to %s: %v", version, ch, err)
133 }
134 }
135}
136
137// A promotion to stable cuts the iteration's tag, reports it, and stays
138// re-runnable: the repeat is a no-op that still names the tag.
139func TestStablePromoteCutsTag(t *testing.T) {
140 s := NewDirStore(t.TempDir())
141 ctx := context.Background()
142 priv, _ := testKeys(t)
143
144 shipLabelledIteration(t, s, "bab", "km", "0.5.0-1", "fixing modals", "- focus trap fixed")
145 shipLabelledIteration(t, s, "bab", "km", "0.5.0-2", "polish", "- rounded corners")
146 climbToBeta(t, ctx, s, priv, "bab", "km", "0.5.0-2")
147
148 res, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "0.5.0-2",
149 PromoteOptions{Actor: "paul@example.com"})
150 if err != nil {
151 t.Fatal(err)
152 }
153 if res.Tag != "0.5.0" || res.TagWarning != "" {
154 t.Fatalf("res.Tag=%q TagWarning=%q; want 0.5.0, empty", res.Tag, res.TagWarning)
155 }
156 tag, err := GetTag(ctx, s, "bab", "km", "0.5.0")
157 if err != nil || tag.Release != "0.5.0-2" {
158 t.Fatalf("tag after promote: %+v, %v", tag, err)
159 }
160 if tag.Actor != "paul@example.com" || tag.Commit != "c-0.5.0-2" {
161 t.Fatalf("tag fields: %+v", tag)
162 }
163 // The changelog is compiled from BOTH iterations, oldest first — proof the
164 // hook ran with the whole line in view, not just the promoted build.
165 if !strings.Contains(tag.Changelog, "0.5.0-1") || !strings.Contains(tag.Changelog, "rounded corners") {
166 t.Fatalf("changelog does not carry the line: %q", tag.Changelog)
167 }
168
169 // Repeat promote: NoOp, and the tag is still reported.
170 res2, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "0.5.0-2",
171 PromoteOptions{Actor: "paul@example.com"})
172 if err != nil || !res2.NoOp || res2.Tag != "0.5.0" {
173 t.Fatalf("repeat: %+v, %v", res2, err)
174 }
175 if res2.TagWarning != "" {
176 t.Fatalf("repeat warned: %q", res2.TagWarning)
177 }
178}
179
180// The no-op path is a HEAL, not just a report: a promotion whose tag write
181// failed the first time is repaired by re-running the same command.
182//
183// Mutation-mindedness: drop the stableTag call from the NoOp early return and
184// this is the only test that notices — and the operator would have no way back
185// to a tag, since the pointer never moves again.
186func TestRepeatStablePromoteHealsAMissingTag(t *testing.T) {
187 s := NewDirStore(t.TempDir())
188 ctx := context.Background()
189 priv, _ := testKeys(t)
190
191 shipLabelledIteration(t, s, "bab", "km", "0.6.0-1", "first", "- one")
192 climbToBeta(t, ctx, s, priv, "bab", "km", "0.6.0-1")
193 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "0.6.0-1", PromoteOptions{}); err != nil {
194 t.Fatal(err)
195 }
196 // Simulate the tag write having failed on the first attempt.
197 if err := s.Delete(ctx, TagKey("bab", "km", "0.6.0")); err != nil {
198 t.Fatal(err)
199 }
200
201 res, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "0.6.0-1",
202 PromoteOptions{Actor: "paul@example.com"})
203 if err != nil || !res.NoOp {
204 t.Fatalf("repeat: %+v, %v", res, err)
205 }
206 if res.Tag != "0.6.0" || res.TagWarning != "" {
207 t.Fatalf("no-op did not heal the tag: Tag=%q warning=%q", res.Tag, res.TagWarning)
208 }
209 if tag, err := GetTag(ctx, s, "bab", "km", "0.6.0"); err != nil || tag.Release != "0.6.0-1" {
210 t.Fatalf("tag after heal: %+v, %v", tag, err)
211 }
212}
213
214// A sha-named build reaching stable cuts nothing — no invented history — and
215// says so, so the operator knows why the release has no name.
216func TestStablePromoteShaVersionNoTag(t *testing.T) {
217 s := NewDirStore(t.TempDir())
218 ctx := context.Background()
219 priv, pub := testKeys(t)
220
221 shipRelease(t, s, "bab", "km", "de2766f8", "linux-arm64", []byte("build"))
222 climbToBeta(t, ctx, s, priv, "bab", "km", "de2766f8")
223
224 res, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "de2766f8", PromoteOptions{})
225 if err != nil {
226 t.Fatal(err)
227 }
228 if res.Tag != "" {
229 t.Fatalf("sha promote cut tag %q", res.Tag)
230 }
231 if res.TagWarning == "" {
232 t.Fatal("sha promote to stable should warn that no tag was cut")
233 }
234 if !strings.Contains(res.TagWarning, "de2766f8") {
235 t.Fatalf("warning does not name the version: %q", res.TagWarning)
236 }
237 // The promotion itself is real, warning or not.
238 p, err := Channel(ctx, s, pub, "bab", "km", "stable")
239 if err != nil || p.Version != "de2766f8" {
240 t.Fatalf("stable pointer = %+v, %v", p, err)
241 }
242}
243
244// Nothing below stable touches tags: edge and beta are staging rungs, and a
245// build that never reaches stable never earns a name.
246func TestBetaPromoteNoTag(t *testing.T) {
247 s := NewDirStore(t.TempDir())
248 ctx := context.Background()
249 priv, _ := testKeys(t)
250
251 shipLabelledIteration(t, s, "bab", "km", "0.5.0-1", "first", "- one")
252 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "edge", "0.5.0-1", PromoteOptions{}); err != nil {
253 t.Fatal(err)
254 }
255
256 res, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "beta", "0.5.0-1", PromoteOptions{})
257 if err != nil || res.Tag != "" || res.TagWarning != "" {
258 t.Fatalf("beta promote touched tags: %+v, %v", res, err)
259 }
260 if _, err := GetTag(ctx, s, "bab", "km", "0.5.0"); !errors.Is(err, os.ErrNotExist) {
261 t.Fatalf("beta promote wrote a tag: %v", err)
262 }
263 // A canary promotion of the same iteration is equally tag-free.
264 res, err = PromoteOnLadder(ctx, s, priv, "bab", "km", "canary/x", "0.5.0-1", PromoteOptions{})
265 if err != nil || res.Tag != "" || res.TagWarning != "" {
266 t.Fatalf("canary promote touched tags: %+v, %v", res, err)
267 }
268}
269
270// tagFailStore fails every read under an app's tags/ prefix with an error that
271// is NOT os.ErrNotExist — the shape of a bucket that is up but unhappy.
272type tagFailStore struct {
273 Store
274 fail error
275}
276
277func (s tagFailStore) Get(ctx context.Context, key string) ([]byte, string, error) {
278 if strings.Contains(key, "/tags/") {
279 return nil, "", s.fail
280 }
281 return s.Store.Get(ctx, key)
282}
283
284// The promotion has already happened when the tag is cut, so a tag failure can
285// only ever be a warning. Returning an error here would tell the operator the
286// deploy failed when the fleet is already moving to the new build.
287//
288// Mutation-mindedness: make stableTag return its error and have PromoteOnLadder
289// propagate it, and this is the test that fails — with a stable pointer that
290// moved under a "promote failed" message.
291func TestStablePromoteTagFailureIsAWarningNotAnError(t *testing.T) {
292 dir := NewDirStore(t.TempDir())
293 s := tagFailStore{Store: dir, fail: fmt.Errorf("bucket said no")}
294 ctx := context.Background()
295 priv, pub := testKeys(t)
296
297 shipLabelledIteration(t, s, "bab", "km", "0.5.0-1", "first", "- one")
298 climbToBeta(t, ctx, s, priv, "bab", "km", "0.5.0-1")
299
300 res, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "0.5.0-1",
301 PromoteOptions{Actor: "paul@example.com"})
302 if err != nil {
303 t.Fatalf("a failed tag write failed the promotion: %v", err)
304 }
305 if res.Tag != "" {
306 t.Fatalf("res.Tag = %q after a failed tag read", res.Tag)
307 }
308 if !strings.Contains(res.TagWarning, "bucket said no") {
309 t.Fatalf("warning does not carry the cause: %q", res.TagWarning)
310 }
311 // And the pointer really moved.
312 p, err := Channel(ctx, s, pub, "bab", "km", "stable")
313 if err != nil || p.Version != "0.5.0-1" {
314 t.Fatalf("stable pointer = %+v, %v", p, err)
315 }
316}
317
318// A second iteration under a target already tagged by an earlier one must not
319// claim the earlier tag as its own. 0.5.0 got tagged at 0.5.0-1; when 0.5.0-2
320// later reaches stable, EnsureTag finds that existing tag (it names the same
321// target) and hands it back unchanged — the promotion is real, but it did not
322// cut or move the tag, so res.Tag must stay empty and the operator must be
323// told which iteration the tag actually names.
324//
325// Mutation-mindedness: report res.Tag = tag.Version regardless of tag.Release
326// and this is the test that catches it — a false "tagged 0.5.0" for a
327// promotion the tag does not describe.
328func TestStablePromoteAfterEarlierTagWarnsInsteadOfClaimingIt(t *testing.T) {
329 s := NewDirStore(t.TempDir())
330 ctx := context.Background()
331 priv, pub := testKeys(t)
332
333 shipLabelledIteration(t, s, "bab", "km", "0.5.0-1", "first", "- one")
334 climbToBeta(t, ctx, s, priv, "bab", "km", "0.5.0-1")
335 if _, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "0.5.0-1", PromoteOptions{}); err != nil {
336 t.Fatal(err)
337 }
338
339 shipLabelledIteration(t, s, "bab", "km", "0.5.0-2", "second", "- two")
340 climbToBeta(t, ctx, s, priv, "bab", "km", "0.5.0-2")
341
342 res, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "0.5.0-2", PromoteOptions{})
343 if err != nil {
344 t.Fatal(err)
345 }
346 if res.Tag != "" {
347 t.Fatalf("res.Tag = %q; a tag belonging to a different iteration must not be claimed", res.Tag)
348 }
349 if !strings.Contains(res.TagWarning, "already tagged at 0.5.0-1") {
350 t.Fatalf("TagWarning = %q; want it to name the iteration the tag actually carries", res.TagWarning)
351 }
352 // The promotion itself is real regardless of the tag warning.
353 p, err := Channel(ctx, s, pub, "bab", "km", "stable")
354 if err != nil || p.Version != "0.5.0-2" {
355 t.Fatalf("stable pointer = %+v, %v", p, err)
356 }
357}
358
359// A hotfix straight to stable — no rungs climbed — still cuts the version's
360// tag: the ladder gate is bypassed, but stable promotion's tag-cutting is not
361// gated on how the version got there.
362func TestHotfixToStableCutsTag(t *testing.T) {
363 s := NewDirStore(t.TempDir())
364 ctx := context.Background()
365 priv, _ := testKeys(t)
366
367 shipLabelledIteration(t, s, "bab", "km", "0.7.0-1", "urgent fix", "- patched the leak")
368
369 res, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "0.7.0-1",
370 PromoteOptions{Hotfix: true, Actor: "paul@example.com"})
371 if err != nil {
372 t.Fatal(err)
373 }
374 if !res.Hotfix {
375 t.Fatalf("res.Hotfix = false for a hotfix promotion")
376 }
377 if res.Tag != "0.7.0" {
378 t.Fatalf("res.Tag = %q, TagWarning = %q; want 0.7.0 cut even via hotfix", res.Tag, res.TagWarning)
379 }
380 tag, err := GetTag(ctx, s, "bab", "km", "0.7.0")
381 if err != nil || tag.Release != "0.7.0-1" {
382 t.Fatalf("tag after hotfix promote: %+v, %v", tag, err)
383 }
384}
385