carlos / platform Public

ship: --program-change is recorded whenever it is given

Ruling G1. CheckShipProgram stamped ProgramChange only when the release differed from the newest one, so ship A, ship B1 with the flag, ship B2 with the flag left B2 without the consent, and a box still on A (it missed B1) refused B2. The flag now reaches the manifest for every Go binary release; the comparison only decides whether a ship without it refuses.

Also the Task 1 contract tests from the final-review triage: agreeing multi-artifact releases pass, static releases are neither judged nor stamped, PreviousProgram orders by built_at over listing order, and skips manifests that fail validation or name another app or version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 6fb5cfe1a47c1bb73bcc86c450407f8e1f15157d parent db54838
3 files changed, +193 −7
  • internal/release/program.go +10 −7
  • internal/release/program_test.go +136 −0
  • internal/release/programhistory_test.go +47 −0
diff --git a/internal/release/program.go b/internal/release/program.go
index 5f274fbfe..45121f3b6 100644
--- a/internal/release/program.go
+++ b/internal/release/program.go
@@ -127,7 +127,13 @@ func (e *ProgramMismatchError) Error() string {
}
// CheckShipProgram is the one ship-side rule both ship doors call before
-// release.Ship. It stamps m.Program (and m.ProgramChange when allowed).
+// release.Ship. It stamps m.Program, and m.ProgramChange from allowChange.
+//
+// The flag is recorded whenever it was given, not only when this ship differs
+// from the newest release: a box decides by its own route's history, and a box
+// that missed the release which changed the program (ship A, ship B1 with the
+// flag, a box still on A) must find the consent on B2 too. The comparison with
+// the previous release only decides whether a ship WITHOUT the flag refuses.
func CheckShipProgram(ctx context.Context, s Store, acct string, m *Manifest, files map[string][]byte, allowChange bool) error {
if m.Kind != "binary" {
return nil
@@ -137,16 +143,13 @@ func CheckShipProgram(ctx context.Context, s Store, acct string, m *Manifest, fi
return err
}
m.Program = prog
+ m.ProgramChange = allowChange
prev, prevVer, err := PreviousProgram(ctx, s, acct, m.App)
if err != nil {
return fmt.Errorf("reading %s's previous releases: %w", m.App, err)
}
- if prev == "" || prev == prog {
+ if prev == "" || prev == prog || allowChange {
return nil
}
- if !allowChange {
- return &ProgramMismatchError{App: m.App, Got: prog, Want: prev, WantVersion: prevVer}
- }
- m.ProgramChange = true
- return nil
+ return &ProgramMismatchError{App: m.App, Got: prog, Want: prev, WantVersion: prevVer}
}
diff --git a/internal/release/program_test.go b/internal/release/program_test.go
index c39cfe04d..23f688095 100644
--- a/internal/release/program_test.go
+++ b/internal/release/program_test.go
@@ -2,12 +2,14 @@ package release
import (
"context"
+ "encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
+ "time"
)
// tinyProgram builds a stripped linux/arm64 Go binary whose main package is
@@ -96,6 +98,140 @@ func TestCheckShipProgram(t *testing.T) {
}
}
+// sameProgram is b with one byte appended: different bytes (a different
+// sha256, so a different release) whose build info still names the same
+// program.
+func sameProgram(b []byte) []byte {
+ return append(b[:len(b):len(b)], 0)
+}
+
+// Ruling G1: --program-change is recorded whenever it is given, even when the
+// newest release already has the same program. Ship A, then B1 with the flag,
+// then B2 with the flag: B2 must carry the consent, because a box that missed
+// B1 is still on A and decides by its own history.
+func TestCheckShipProgramRecordsTheFlagWhenThePredecessorAgrees(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ a, b1 := tinyProgram(t, "example.com/console"), tinyProgram(t, "example.com/carlos")
+ b2 := sameProgram(b1)
+ if err := shipWithProgram(t, s, "va", a, false); err != nil {
+ t.Fatal(err)
+ }
+ if err := shipWithProgram(t, s, "vb1", b1, true); err != nil {
+ t.Fatal(err)
+ }
+ if err := shipWithProgram(t, s, "vb2", b2, true); err != nil {
+ t.Fatal(err)
+ }
+ m, err := GetManifest(context.Background(), s, "bab", "console", "vb2")
+ if err != nil || m.Program != "example.com/carlos" || !m.ProgramChange {
+ t.Fatalf("vb2 manifest = %+v, %v; want program_change true though vb1 already names the program", m, err)
+ }
+ // And without the flag a same-program ship records no change.
+ if err := shipWithProgram(t, s, "vb3", sameProgram(b2), false); err != nil {
+ t.Fatal(err)
+ }
+ if m, err := GetManifest(context.Background(), s, "bab", "console", "vb3"); err != nil || m.ProgramChange {
+ t.Fatalf("vb3 manifest = %+v, %v; want no program_change", m, err)
+ }
+}
+
+// Every Go artifact of a release agreeing on one program passes, and the
+// manifest names it.
+func TestCheckShipProgramAgreeingArtifactsPass(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ arm := tinyProgram(t, "example.com/console")
+ files := map[string][]byte{"linux-arm64": arm, "linux-amd64": sameProgram(arm)}
+ m := Manifest{Schema: 1, App: "console", Version: "v1", Kind: "binary",
+ Artifacts: map[string]Artifact{"linux-arm64": {}, "linux-amd64": {}}}
+ if err := CheckShipProgram(context.Background(), s, "bab", &m, files, false); err != nil {
+ t.Fatalf("agreeing artifacts: %v", err)
+ }
+ if m.Program != "example.com/console" {
+ t.Fatalf("program = %q, want example.com/console", m.Program)
+ }
+}
+
+// A static release is never judged or stamped, whatever its bytes are.
+func TestCheckShipProgramSkipsStatic(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ if err := shipWithProgram(t, s, "v1", tinyProgram(t, "example.com/console"), false); err != nil {
+ t.Fatal(err)
+ }
+ files := map[string][]byte{"site.tar.gz": tinyProgram(t, "example.com/carlos")}
+ m := Manifest{Schema: 1, App: "console", Version: "v2", Kind: "static",
+ Artifacts: map[string]Artifact{"site.tar.gz": {}}}
+ if err := CheckShipProgram(context.Background(), s, "bab", &m, files, false); err != nil {
+ t.Fatalf("static release judged: %v", err)
+ }
+ if m.Program != "" || m.ProgramChange {
+ t.Fatalf("static manifest stamped: program %q change %v", m.Program, m.ProgramChange)
+ }
+}
+
+// putManifest writes a raw manifest for app console under account bab.
+func putManifest(t *testing.T, s Store, ver string, m Manifest) {
+ t.Helper()
+ raw, err := json.Marshal(m)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(context.Background(), ManifestKey("bab", "console", ver), raw, ""); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// PreviousProgram picks by built_at, not by the order the store lists
+// versions in. "z-new" was built last, but its manifest object is the older
+// one (a re-upload, a copy between buckets), so the store's own ranking —
+// newest object first, ties by version — lists "a-old" first.
+func TestPreviousProgramOrdersByBuiltAt(t *testing.T) {
+ root := t.TempDir()
+ s := NewDirStore(root)
+ art := map[string]Artifact{adoptPlatform: {File: adoptPlatform, SHA256: strings.Repeat("a", 64), Size: 1}}
+ putManifest(t, s, "z-new", Manifest{Schema: 1, App: "console", Version: "z-new", Kind: "binary",
+ BuiltAt: "2026-06-01T00:00:00Z", Artifacts: art, Program: "example.com/new"})
+ putManifest(t, s, "a-old", Manifest{Schema: 1, App: "console", Version: "a-old", Kind: "binary",
+ BuiltAt: "2026-01-01T00:00:00Z", Artifacts: art, Program: "example.com/old"})
+ older, newer := time.Now().Add(-time.Hour), time.Now()
+ for ver, mod := range map[string]time.Time{"z-new": older, "a-old": newer} {
+ p := filepath.Join(root, filepath.FromSlash(ManifestKey("bab", "console", ver)))
+ if err := os.Chtimes(p, mod, mod); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if vers, _, err := VersionCandidates(context.Background(), s, "bab", "console", 20); err != nil || len(vers) != 2 || vers[0] != "a-old" {
+ t.Fatalf("listing = %v, %v; the test needs a-old listed first", vers, err)
+ }
+ got, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
+ if err != nil || got != "example.com/new" || ver != "z-new" {
+ t.Fatalf("PreviousProgram = %q %q %v, want example.com/new from z-new", got, ver, err)
+ }
+}
+
+// A manifest that parses but fails validation, and one that names another
+// app or version than its key, are skipped; the older valid one answers.
+func TestPreviousProgramSkipsInvalidAndMisnamedManifests(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ if err := shipWithProgram(t, s, "v1", tinyProgram(t, "example.com/console"), false); err != nil {
+ t.Fatal(err)
+ }
+ art := map[string]Artifact{adoptPlatform: {File: adoptPlatform, SHA256: strings.Repeat("a", 64), Size: 1}}
+ late := "2999-01-01T00:00:00Z"
+ // Fails validateManifest: schema 2.
+ putManifest(t, s, "v2", Manifest{Schema: 2, App: "console", Version: "v2", Kind: "binary",
+ BuiltAt: late, Artifacts: art, Program: "example.com/invalid"})
+ // Valid, but declares another app.
+ putManifest(t, s, "v3", Manifest{Schema: 1, App: "other", Version: "v3", Kind: "binary",
+ BuiltAt: late, Artifacts: art, Program: "example.com/other-app"})
+ // Valid, but declares another version.
+ putManifest(t, s, "v4", Manifest{Schema: 1, App: "console", Version: "v9", Kind: "binary",
+ BuiltAt: late, Artifacts: art, Program: "example.com/other-version"})
+ got, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
+ if err != nil || got != "example.com/console" || ver != "v1" {
+ t.Fatalf("PreviousProgram = %q %q %v, want example.com/console from v1", got, ver, err)
+ }
+}
+
// A release from an old client has no program and must not switch the check
// off for the next ship.
func TestPreviousProgramSkipsReleasesWithoutOne(t *testing.T) {
diff --git a/internal/release/programhistory_test.go b/internal/release/programhistory_test.go
index 519eebe93..51d40c418 100644
--- a/internal/release/programhistory_test.go
+++ b/internal/release/programhistory_test.go
@@ -102,6 +102,53 @@ func TestBoxRecordsTheLiveProgramEvenWithAHistory(t *testing.T) {
}
}
+// Ruling G1 at the box: ship A, then B1 and B2 both with --program-change. A
+// box that adopted A and missed B1 meets B2 with a history of [A]; B2's own
+// manifest carries the consent, so it is adopted.
+func TestBoxAdoptsAConsentedChangeItMissedTheFirstReleaseOf(t *testing.T) {
+ host := "console.example.com"
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt, bake := t.TempDir(), t.TempDir()
+ reg := testRegistry(t)
+ a, b1 := tinyProgram(t, "example.com/console"), tinyProgram(t, "example.com/carlos")
+ for _, r := range []struct {
+ ver string
+ bytes []byte
+ allow bool
+ }{{"va", a, false}, {"vb1", b1, true}, {"vb2", sameProgram(b1), true}} {
+ if err := shipWithProgram(t, s, r.ver, r.bytes, r.allow); err != nil {
+ t.Fatalf("ship %s: %v", r.ver, err)
+ }
+ }
+ addRoute(t, reg, host, "console", "canary/x", false)
+ restart := func(string) error { return nil }
+ // History by adoption only: the box adopts A.
+ if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "va"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake)); err != nil {
+ t.Fatalf("Pass va: %v", err)
+ }
+ if got := ReadPrograms(bake, host); !slices.Equal(got, []string{"example.com/console"}) {
+ t.Fatalf("history after adopting va = %v, want [example.com/console]", got)
+ }
+ // The channel moves straight to vb2; this box never saw vb1.
+ if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "vb2"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake)); err != nil {
+ t.Fatalf("Pass vb2: %v", err)
+ }
+ if v, _ := LiveVersion(opt, host); v != "vb2" {
+ t.Fatalf("live = %q, want vb2: its manifest records --program-change", v)
+ }
+ if r, _ := ReadRefusal(bake, host); r != nil {
+ t.Fatalf("refusal = %+v, want none", r)
+ }
+}
+
func TestBoxSeedsHistoryFromTheLiveBinary(t *testing.T) {
bake := t.TempDir()
if got := ReadPrograms(bake, "console.example.com"); len(got) != 0 {