carlos / platform Public

release: an unreadable manifest defers a wrong-program build, never refuses it

Ruling G3. When ensureStaged has the binary but cannot obtain its manifest (no sibling, store failing), D2 read the unknown --program-change as absent and wrote a refusal. That record then sat at the gate before ensureStaged, so the manifest was never read again and an authorised change stayed blocked after the store recovered. Now the box prints that the manifest could not be read, does not point the build live, writes no refusal, and asks again next pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 58e1eef539f455da85517cf1ae1e319494d50f91 parent f696196
2 files changed, +74 −5
  • internal/release/adopt.go +14 −5
  • internal/release/programhistory_test.go +60 −0
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index 2564ab52c..eed2f3fb7 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -1032,13 +1032,22 @@ func adoptHost(ctx context.Context, reg *registry.Registry, s Store, optDir stri
hist = append(slices.Clone(hist), live)
}
}
- if len(hist) > 0 && !slices.Contains(hist, newProg) && !(m != nil && m.ProgramChange) {
+ if len(hist) > 0 && !slices.Contains(hist, newProg) && m == nil {
+ // ensureStaged has the binary but could not obtain its manifest
+ // (no sibling, and the store did not answer), so whether the
+ // release carries --program-change is unknown. Unknown is not a
+ // refusal: a record here would sit at the gate above and stop the
+ // next pass from ever reading the manifest, so an authorised
+ // change would stay blocked after the store recovered. Not pointed
+ // live, and the next pass asks again.
+ fmt.Printf(" %s: %s is %s, not in this route's history, and its manifest could not be read to check for --program-change; not pointed live, will retry\n",
+ rt.Host, ver, newProg)
+ return
+ }
+ if len(hist) > 0 && !slices.Contains(hist, newProg) && !m.ProgramChange {
fmt.Printf(" %s: %s is %s, but this route has only run %s — not pointed live (ship with --program-change if this is intended)\n",
rt.Host, ver, newProg, strings.Join(hist, ", "))
- sha := ""
- if m != nil {
- sha = m.Artifacts[adoptPlatform].SHA256
- }
+ sha := m.Artifacts[adoptPlatform].SHA256
// Nothing was restarted, so the box is still on its old build.
if err := WriteRefusal(cfg.bakeDir, Refusal{Host: rt.Host, App: rt.App, Version: ver, SHA256: sha,
Platform: adoptPlatform, PointerPromotedAt: ptr.PromotedAt, RefusedAt: now.UTC(),
diff --git a/internal/release/programhistory_test.go b/internal/release/programhistory_test.go
index 51d40c418..3b3b1dc57 100644
--- a/internal/release/programhistory_test.go
+++ b/internal/release/programhistory_test.go
@@ -149,6 +149,66 @@ func TestBoxAdoptsAConsentedChangeItMissedTheFirstReleaseOf(t *testing.T) {
}
}
+// Ruling G3: an authorised B build is already staged, its manifest sibling is
+// missing, and the store cannot answer for the manifest. Whether it carries
+// --program-change is unknown, so the box neither points it live nor writes a
+// refusal; once the store recovers, the next pass reads the manifest and
+// adopts it, with the promotion unchanged.
+func TestBoxDefersAWrongProgramWhoseManifestItCannotRead(t *testing.T) {
+ host := "console.example.com"
+ base := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt, bake := t.TempDir(), t.TempDir()
+ reg := testRegistry(t)
+ a, b := tinyProgram(t, "example.com/console"), tinyProgram(t, "example.com/carlos")
+ if err := shipWithProgram(t, base, "va", a, false); err != nil {
+ t.Fatal(err)
+ }
+ if err := shipWithProgram(t, base, "vb", b, true); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, host, "console", "canary/x", false)
+ restart := func(string) error { return nil }
+ if err := Promote(ctx, base, priv, "bab", "console", "canary/x", "va"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Pass(ctx, reg, base, pub, opt, restart, BakeDir(bake)); err != nil {
+ t.Fatalf("Pass va: %v", err)
+ }
+ // vb's binary is staged with no manifest sibling (staged by a binary
+ // older than the sibling, or a sibling write that failed).
+ if err := StageArtifact(opt, "bab", "vb", "console", b); err != nil {
+ t.Fatal(err)
+ }
+ if err := Promote(ctx, base, priv, "bab", "console", "canary/x", "vb"); err != nil {
+ t.Fatal(err)
+ }
+ broken := &failingStore{Store: base, failKey: ManifestKey("bab", "console", "vb")}
+ var out bytes.Buffer
+ withCapturedStdout(t, &out, func() {
+ if err := Pass(ctx, reg, broken, pub, opt, restart, BakeDir(bake)); err != nil {
+ t.Fatalf("Pass with the manifest unreadable: %v", err)
+ }
+ })
+ if v, _ := LiveVersion(opt, host); v != "va" {
+ t.Fatalf("live = %q, want va: an unknown override is never pointed live", v)
+ }
+ if !strings.Contains(out.String(), "could not be read to check for --program-change; not pointed live, will retry") {
+ t.Fatalf("printed %q, want the will-retry line", out.String())
+ }
+ if r, err := ReadRefusal(bake, host); err != nil || r != nil {
+ t.Fatalf("refusal = %+v, %v; want none: an unreadable manifest is not a refusal", r, err)
+ }
+ // The store recovers; the same promotion is adopted.
+ if err := Pass(ctx, reg, base, pub, opt, restart, BakeDir(bake)); err != nil {
+ t.Fatalf("Pass after recovery: %v", err)
+ }
+ if v, _ := LiveVersion(opt, host); v != "vb" {
+ t.Fatalf("live = %q, want vb once its manifest (program_change) is readable", v)
+ }
+}
+
func TestBoxSeedsHistoryFromTheLiveBinary(t *testing.T) {
bake := t.TempDir()
if got := ReadPrograms(bake, "console.example.com"); len(got) != 0 {