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
3// The pipeline object (2026-08-20 release pipelines v2 spec §2): a
4// root-signed per-app ordered channel list with per-channel bake and
5// approval rules, replacing the implicit edge/beta/stable ladder assumption.
6// The properties tested here: validation catches the reserved/duplicate/
7// malformed-channel cases before anything is signed; the accessors read the
8// object the way later tasks will (Entry, Index, ChannelNamed,
9// MaxApprovals); the object round-trips through the same root-only signed
10// wrapper discipline as production.go, including monotonic SetAt and
11// refusal of a copied object at the wrong app.
12
13import (
14 "context"
15 "crypto/ed25519"
16 "strings"
17 "testing"
18 "time"
19)
20
21func TestPipelineValidation(t *testing.T) {
22 good := []PipelineChannel{{Name: "edge"}, {Name: "production", Bake: "24h",
23 Rules: ChannelRules{Passkey: true, PromoteApprovals: 2, ChangeApprovals: 2}}}
24 for _, tc := range []struct {
25 name string
26 channels []PipelineChannel
27 wantErr string // "" = valid
28 }{
29 {"default single edge", DefaultPipelineChannels(), ""},
30 {"two channels with rules", good, ""},
31 {"empty list", nil, "at least one channel"},
32 {"reserved canary", []PipelineChannel{{Name: "canary"}}, "reserved"},
33 {"slash in name", []PipelineChannel{{Name: "canary/x"}}, "may not contain"},
34 {"duplicate names", []PipelineChannel{{Name: "edge"}, {Name: "edge"}}, "duplicate"},
35 {"bad duration", []PipelineChannel{{Name: "edge", Bake: "soon"}}, "bake"},
36 {"negative bake", []PipelineChannel{{Name: "edge", Bake: "-1h"}}, "bake"},
37 {"negative approvals", []PipelineChannel{{Name: "edge",
38 Rules: ChannelRules{PromoteApprovals: -1}}}, "approvals"},
39 } {
40 p := &Pipeline{Schema: 1, Account: "bae", App: "web", Channels: tc.channels, SetAt: time.Now()}
41 err := validatePipeline(p, "bae", "web")
42 if tc.wantErr == "" && err != nil {
43 t.Errorf("%s: unexpected error %v", tc.name, err)
44 }
45 if tc.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) {
46 t.Errorf("%s: err = %v, want mention of %q", tc.name, err, tc.wantErr)
47 }
48 }
49}
50
51func TestPipelineAccessors(t *testing.T) {
52 p := &Pipeline{Schema: 1, Channels: []PipelineChannel{
53 {Name: "edge"}, {Name: "staging", Bake: "1h"}, {Name: "production", Bake: "24h"}}}
54 if p.Entry() != "edge" {
55 t.Errorf("Entry() = %q, want edge", p.Entry())
56 }
57 if i := p.Index("staging"); i != 1 {
58 t.Errorf("Index(staging) = %d, want 1", i)
59 }
60 if i := p.Index("canary/x"); i != -1 {
61 t.Errorf("Index(canary/x) = %d, want -1 (canary is never a pipeline channel)", i)
62 }
63 if i := p.Index("nope"); i != -1 {
64 t.Errorf("Index(nope) = %d, want -1", i)
65 }
66 c, ok := p.ChannelNamed("production")
67 if !ok || c.Bake != "24h" {
68 t.Errorf("ChannelNamed(production) = %+v, %v", c, ok)
69 }
70 if d, err := c.BakeWindow(); err != nil || d != 24*time.Hour {
71 t.Errorf("BakeWindow() = %v, %v", d, err)
72 }
73 var zero PipelineChannel
74 if d, err := zero.BakeWindow(); err != nil || d != 0 {
75 t.Errorf("zero BakeWindow() = %v, %v; want 0, nil", d, err)
76 }
77}
78
79func TestSetGetPipelineRoundTrip(t *testing.T) {
80 // Mirror TestSetProductionMonotonicSetAtOnRapidFlip's harness:
81 // dir store + root key. Missing object reads as (nil, nil).
82 s, priv, pub := pipelineTestHarness(t)
83 ctx := context.Background()
84 if p, err := GetPipeline(ctx, s, pub, "bae", "web"); p != nil || err != nil {
85 t.Fatalf("missing pipeline = %v, %v; want nil, nil", p, err)
86 }
87 set, err := SetPipeline(ctx, s, priv, "bae", "web", DefaultPipelineChannels(), "owner@example.test")
88 if err != nil {
89 t.Fatal(err)
90 }
91 got, err := GetPipeline(ctx, s, pub, "bae", "web")
92 if err != nil || got == nil || got.Entry() != "edge" || got.Actor != "owner@example.test" {
93 t.Fatalf("round trip = %+v, %v", got, err)
94 }
95 // Monotonic SetAt: an immediate rewrite must stamp strictly later.
96 set2, err := SetPipeline(ctx, s, priv, "bae", "web",
97 []PipelineChannel{{Name: "edge"}, {Name: "production", Bake: "24h"}}, "owner@example.test")
98 if err != nil {
99 t.Fatal(err)
100 }
101 if !set2.SetAt.After(set.SetAt) {
102 t.Fatalf("SetAt not monotonic: %v then %v", set.SetAt, set2.SetAt)
103 }
104}
105
106func TestGetPipelineRefusesTamperAndWrongApp(t *testing.T) {
107 s, priv, pub := pipelineTestHarness(t)
108 ctx := context.Background()
109 if _, err := SetPipeline(ctx, s, priv, "bae", "web", DefaultPipelineChannels(), ""); err != nil {
110 t.Fatal(err)
111 }
112 // Copy the object to another app's key: signed payload names bae/web,
113 // so it must refuse at bae/other (same trap production.go documents).
114 raw, _, err := s.Get(ctx, PipelineKey("bae", "web"))
115 if err != nil {
116 t.Fatal(err)
117 }
118 if _, err := s.Put(ctx, PipelineKey("bae", "other"), raw, ""); err != nil {
119 t.Fatal(err)
120 }
121 if _, err := GetPipeline(ctx, s, pub, "bae", "other"); err == nil {
122 t.Fatal("copied pipeline verified at the wrong app; want refusal")
123 }
124}
125
126func TestPipelinePromotionAllowed(t *testing.T) {
127 p := &Pipeline{Schema: 1, Channels: []PipelineChannel{
128 {Name: "edge"}, {Name: "staging"}, {Name: "production"}}}
129 for _, tc := range []struct {
130 name, from, to string
131 wantErr string // substring; "" = allowed
132 }{
133 {"canary always", "production", "canary/x", ""},
134 {"fresh build enters at entry", "", "edge", ""},
135 {"nothing else enters at entry", "staging", "edge", "-hotfix"},
136 {"edge to staging", "edge", "staging", ""},
137 {"staging to production", "staging", "production", ""},
138 {"skip refused", "edge", "production", "staging"},
139 {"unknown target names the list", "edge", "beta", "edge, staging, production"},
140 } {
141 err := PipelinePromotionAllowed(p, tc.from, tc.to)
142 if tc.wantErr == "" && err != nil {
143 t.Errorf("%s: unexpected refusal %v", tc.name, err)
144 }
145 if tc.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) {
146 t.Errorf("%s: err = %v, want mention of %q", tc.name, err, tc.wantErr)
147 }
148 }
149}
150
151func TestPipelineSingleChannelPromoteNamesSetup(t *testing.T) {
152 p := &Pipeline{Schema: 1, Channels: DefaultPipelineChannels()}
153 err := PipelinePromotionAllowed(p, "edge", "production")
154 if err == nil || !strings.Contains(err.Error(), "carlos pipeline") {
155 t.Fatalf("one-channel refusal = %v; want it to point at carlos pipeline setup (spec §5)", err)
156 }
157}
158
159// pipelineTestHarness builds a fresh store and root key pair, copying
160// exactly how production_test.go's tests build theirs (NewDirStore over a
161// temp dir, testKeys for the signing pair) — there is no in-memory store in
162// this package; NewDirStore(t.TempDir()) is the existing convention.
163func pipelineTestHarness(t *testing.T) (Store, ed25519.PrivateKey, ed25519.PublicKey) {
164 t.Helper()
165 s := NewDirStore(t.TempDir())
166 priv, pub := testKeys(t)
167 return s, priv, pub
168}
169
170// VersionStage is VersionRung for a pipeline, and PromoteOnLadder must
171// consult it — order gate, entry, hotfix bypass, and canary staying
172// unconditional — the moment an app has one set (task 3, spec §3/§5).
173func TestVersionStageAndPipelinePromote(t *testing.T) {
174 s, priv, pub := pipelineTestHarness(t)
175 ctx := context.Background()
176 chans := []PipelineChannel{{Name: "edge"}, {Name: "production"}}
177 if _, err := SetPipeline(ctx, s, priv, "bae", "web", chans, ""); err != nil {
178 t.Fatal(err)
179 }
180 shipRelease(t, s, "bae", "web", "v1", "linux-arm64", []byte("bin v1"))
181 // Fresh build promotes onto the entry channel.
182 if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "edge", "v1", PromoteOptions{}); err != nil {
183 t.Fatalf("promote v1 onto entry: %v", err)
184 }
185 pipe, err := GetPipeline(ctx, s, pub, "bae", "web")
186 if err != nil || pipe == nil {
187 t.Fatalf("GetPipeline: %+v, %v", pipe, err)
188 }
189 if st, err := VersionStage(ctx, s, pub, "bae", "web", "v1", pipe); err != nil || st != "edge" {
190 t.Fatalf("VersionStage = %q, %v; want edge", st, err)
191 }
192 // edge -> production allowed; a second fresh promote onto production refused.
193 if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v1", PromoteOptions{}); err != nil {
194 t.Fatalf("edge->production: %v", err)
195 }
196 shipRelease(t, s, "bae", "web", "v2", "linux-arm64", []byte("bin v2"))
197 if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v2", PromoteOptions{}); err == nil {
198 t.Fatal("v2 (on no channel) promoted straight onto production; want order refusal")
199 }
200 // Hotfix bypasses order.
201 if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v2", PromoteOptions{Hotfix: true}); err != nil {
202 t.Fatalf("hotfix: %v", err)
203 }
204 // Rung names outside the pipeline are refused for a pipelined app.
205 if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "stable", "v2", PromoteOptions{}); err == nil {
206 t.Fatal("stable promoted on a pipelined app whose pipeline has no stable")
207 }
208 // Canary stays unconditional.
209 if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "canary/x", "v2", PromoteOptions{}); err != nil {
210 t.Fatalf("canary on pipelined app: %v", err)
211 }
212}
213
214// A grant names frozen rungs only (grant.go), so "production" is
215// uncoverable: the scoped signer must refuse (spec §6), and -hotfix — which
216// bypasses only the ORDER gate — can never widen a delegation's reach.
217func TestScopedKeyCannotSignPipelineChannel(t *testing.T) {
218 s, priv, _ := pipelineTestHarness(t)
219 ctx := context.Background()
220 if _, err := SetPipeline(ctx, s, priv, "bae", "web",
221 []PipelineChannel{{Name: "edge"}, {Name: "production"}}, ""); err != nil {
222 t.Fatal(err)
223 }
224 rootPub := priv.Public().(ed25519.PublicKey)
225 key, blob := mintGrant(t, priv, "builder", []string{RungEdge}, time.Hour)
226 sg, err := ScopedSigner(key, blob, rootPub)
227 if err != nil {
228 t.Fatalf("ScopedSigner: %v", err)
229 }
230 shipRelease(t, s, "bae", "web", "v1", "linux-arm64", []byte("bin v1"))
231 if _, err := sg.PromoteOnLadder(ctx, s, "bae", "web", "production", "v1", PromoteOptions{Hotfix: true}); err == nil {
232 t.Fatal("scoped key signed a pipeline channel pointer; want refusal even with --hotfix")
233 }
234}
235
236// TestPipelinePromoteNamesTheChannelAsTo: PromoteResult.To was Rung(channel)
237// with no fallback, so a promotion onto an app-defined channel reported ""
238// — an empty "to" in the API's answer and, worse, an empty To in the
239// Promotion record written into the pointer's own audit trail, which is the
240// permanent record of where a build went. From already carried the channel
241// name on this path (VersionStage answers in channel names), so the trail
242// read "edge -> " for the app's whole history. The fallback is the channel's
243// own name; frozen rungs keep answering with the rung.
244func TestPipelinePromoteNamesTheChannelAsTo(t *testing.T) {
245 s, priv, pub := pipelineTestHarness(t)
246 ctx := context.Background()
247 if _, err := SetPipeline(ctx, s, priv, "bae", "web",
248 []PipelineChannel{{Name: "edge"}, {Name: "production"}}, ""); err != nil {
249 t.Fatal(err)
250 }
251 shipRelease(t, s, "bae", "web", "v1", "linux-arm64", []byte("bin v1"))
252 if _, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "edge", "v1", PromoteOptions{}); err != nil {
253 t.Fatalf("promote to edge: %v", err)
254 }
255 res, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v1", PromoteOptions{Actor: "paul@keymail.dev"})
256 if err != nil {
257 t.Fatalf("promote to production: %v", err)
258 }
259 if res.To != "production" {
260 t.Errorf("PromoteResult.To = %q, want %q — an app-defined channel names itself", res.To, "production")
261 }
262 if res.From != "edge" {
263 t.Errorf("PromoteResult.From = %q, want %q", res.From, "edge")
264 }
265 p, err := Channel(ctx, s, pub, "bae", "web", "production")
266 if err != nil {
267 t.Fatalf("read pointer: %v", err)
268 }
269 if len(p.Audit) == 0 {
270 t.Fatalf("pointer carries no audit trail: %+v", p)
271 }
272 if got := p.Audit[len(p.Audit)-1]; got.To != "production" || got.From != "edge" {
273 t.Errorf("promotion record = %+v, want from edge to production", got)
274 }
275
276 // The repeat (a no-op) names the channel on BOTH sides: the build is on
277 // production, which is exactly what makes this a repeat, so reporting ""
278 // for where it sits would be the same empty answer one rung down.
279 again, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "production", "v1", PromoteOptions{})
280 if err != nil {
281 t.Fatalf("repeat promote: %v", err)
282 }
283 if !again.NoOp {
284 t.Errorf("repeat promote = %+v, want NoOp", again)
285 }
286 if again.From != "production" || again.To != "production" {
287 t.Errorf("repeat promote from/to = %q/%q, want production/production", again.From, again.To)
288 }
289}
290
291// The frozen rungs are untouched: a legacy promote still reports the RUNG,
292// which is not always the channel name (canary/x reports "canary").
293func TestLegacyPromoteStillReportsTheRung(t *testing.T) {
294 s, priv, _ := pipelineTestHarness(t)
295 ctx := context.Background()
296 shipRelease(t, s, "bae", "web", "v1", "linux-arm64", []byte("bin v1"))
297 res, err := PromoteOnLadder(ctx, s, priv, "bae", "web", "canary/x", "v1", PromoteOptions{})
298 if err != nil {
299 t.Fatalf("promote to canary/x: %v", err)
300 }
301 if res.To != RungCanary {
302 t.Errorf("PromoteResult.To = %q, want the rung %q", res.To, RungCanary)
303 }
304 // A different build for edge: v1 is on canary now, and the legacy ladder
305 // only lets a build with no rung yet enter at edge.
306 shipRelease(t, s, "bae", "web", "v2", "linux-arm64", []byte("bin v2"))
307 res, err = PromoteOnLadder(ctx, s, priv, "bae", "web", "edge", "v2", PromoteOptions{})
308 if err != nil {
309 t.Fatalf("promote to edge: %v", err)
310 }
311 if res.To != RungEdge {
312 t.Errorf("PromoteResult.To = %q, want the rung %q", res.To, RungEdge)
313 }
314}
315