carlos / platform Public

release: adoptHost skips a refused promotion before staging or relaying

Reads the D1 refusal record before ensureStaged so a refused build is neither downloaded nor relayed to a root pass, and clears a stale record after a successful adoption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev aeed9c492f42e706ff37b1978d3f6ee601852292 parent cc42b52
2 files changed, +73 −0
  • internal/release/adopt.go +17 −0
  • internal/release/adopt_test.go +56 −0
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index ed2b3485b..755926370 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -961,6 +961,18 @@ func adoptHost(ctx context.Context, reg *registry.Registry, s Store, optDir stri
fmt.Printf(" %s: skipping %s (failed on another host this pass)\n", rt.Host, ver)
return
}
+ // D1's refusal (refusal.go): this box rolled this exact promotion back
+ // because the unit did not come up. Checked before ensureStaged so a
+ // refused build is neither downloaded nor relayed to a root pass.
+ if cfg.bakeDir != "" {
+ if r, err := ReadRefusal(cfg.bakeDir, rt.Host); err != nil {
+ fmt.Printf(" %s: reading refusal record: %v\n", rt.Host, err)
+ } else if r.Blocks(ptr) {
+ fmt.Printf(" %s: refused: %s did not come up on this box at %s (%s); retry with: carlos promote --retry %s %s\n",
+ rt.Host, ver, r.RefusedAt.Format(time.RFC3339), r.Reason, ver, channel)
+ return
+ }
+ }
m, err := ensureStaged(ctx, s, optDir, RouteAccount(rt), rt.App, ver, rt.Host)
if err != nil {
fmt.Printf(" %s: %v\n", rt.Host, err)
@@ -1204,6 +1216,11 @@ func adoptHost(ctx context.Context, reg *registry.Registry, s Store, optDir stri
fmt.Printf(" %s: repointed but recording the version failed: %v\n", rt.Host, err)
return
}
+ // A record left from an older promotion is stale once a build is live.
+ // Best-effort: the agent cannot remove root's file, the root pass can.
+ if cfg.bakeDir != "" {
+ _ = ClearRefusal(cfg.bakeDir, rt.Host)
+ }
if rt.Hibernate {
// Two different facts, one grep. With no restart callback this pass
// genuinely only moved a symlink, and the next wake is when the new
diff --git a/internal/release/adopt_test.go b/internal/release/adopt_test.go
index 57257f31e..abcd88902 100644
--- a/internal/release/adopt_test.go
+++ b/internal/release/adopt_test.go
@@ -3833,3 +3833,59 @@ func TestFirstAdoptionRestartFailureStartsNothing(t *testing.T) {
t.Fatal("a build whose restart failed was stamped")
}
}
+
+// TestRefusedPromotionIsNeitherStagedNorRelayed: a refusal naming the current
+// pointer stops adoptHost before ensureStaged, so the agent never relays a
+// root pass for it and nothing restarts.
+func TestRefusedPromotionIsNeitherStagedNorRelayed(t *testing.T) {
+ host := "console.example.com"
+ ctx, reg, s, pub, opt := startUnitFixture(t, host)
+ bake := t.TempDir()
+ ptr, err := Channel(ctx, s, pub, "bab", "console", "canary/x")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := WriteRefusal(bake, Refusal{Host: host, App: "console", Version: ptr.Version,
+ PointerPromotedAt: ptr.PromotedAt, Reason: "exited: status=2"}); err != nil {
+ t.Fatal(err)
+ }
+ restart := func(h string) error { t.Fatalf("restart consulted for refused %s", h); return nil }
+ relayed := false
+ var out bytes.Buffer
+ withCapturedStdout(t, &out, func() {
+ if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake),
+ OnStageBlocked(func(string, string, error) { relayed = true })); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ })
+ if v, _ := LiveVersion(opt, host); v != "old" {
+ t.Fatalf("live = %q, want old", v)
+ }
+ if relayed {
+ t.Fatal("a refused promotion was relayed to root")
+ }
+ if !strings.Contains(out.String(), "promote --retry") {
+ t.Fatalf("printed %q, want the retry hint", out.String())
+ }
+}
+
+// TestRePromotionClearsTheBlock: a pointer re-signed for the same version
+// (what promote --retry does) is adopted, and the stale record is removed.
+func TestRePromotionClearsTheBlock(t *testing.T) {
+ host := "console.example.com"
+ ctx, reg, s, pub, opt := startUnitFixture(t, host)
+ bake := t.TempDir()
+ if err := WriteRefusal(bake, Refusal{Host: host, App: "console", Version: "new",
+ PointerPromotedAt: time.Unix(1, 0).UTC(), Reason: "older promotion"}); err != nil {
+ t.Fatal(err)
+ }
+ if err := Pass(ctx, reg, s, pub, opt, func(string) error { return nil }, BakeDir(bake)); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, host); v != "new" {
+ t.Fatalf("live = %q, want new (the record names an older promotion)", v)
+ }
+ if r, _ := ReadRefusal(bake, host); r != nil {
+ t.Fatalf("stale refusal survived a successful adoption: %+v", r)
+ }
+}