carlos / platform Public

release: an up-to-date pass clears a refusal that no longer blocks

Final review F8 (Plan 1 D1). After v2 is refused and rolled back to v1, promoting v1 again made every pass return at cur == ver, before any refusal cleanup, so the watcher reported the v2 refusal forever. Before that return, a record for the host that does not Block the current pointer is removed, best-effort (the agent's failure is ignored; the next root pass retries). A record that still blocks stays.

Tests: TestChannelBackOnTheRestoredBuildClearsTheRefusal, TestUpToDateKeepsARefusalThatStillBlocks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 595ac83b45d4be538500e4a64353569fc8473732 parent 2792231
2 files changed, +83 −0
  • internal/release/adopt.go +14 −0
  • internal/release/adopt_test.go +69 −0
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index 4eac8ea96..b4958aa7d 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -956,6 +956,20 @@ func adoptHost(ctx context.Context, reg *registry.Registry, s Store, optDir stri
backfillManifest(ctx, s, optDir, RouteAccount(rt), rt.App, cur, rt.Host)
}
if cur == ver {
+ // A refusal that no longer blocks the pointer is stale even though
+ // nothing moves: v2 was refused and rolled back to v1, then the
+ // channel went back to v1. Without this every later pass returns
+ // here and the watcher reports the v2 refusal forever (final review
+ // F8). Best-effort, like the clear after a successful adoption: the
+ // agent cannot remove root's file, so its failure is ignored and the
+ // next root pass retries. A record that still blocks stays: the link
+ // is on the refused build only when its rollback failed, and that is
+ // worth showing.
+ if cfg.bakeDir != "" {
+ if r, err := ReadRefusal(cfg.bakeDir, rt.Host); err == nil && r != nil && !r.Blocks(ptr) {
+ _ = ClearRefusal(cfg.bakeDir, rt.Host)
+ }
+ }
return // up to date — stay quiet on the periodic tick
}
// Two independent waivers, both root-signed in origin: ignore is the
diff --git a/internal/release/adopt_test.go b/internal/release/adopt_test.go
index 82963fc45..d4efb3f95 100644
--- a/internal/release/adopt_test.go
+++ b/internal/release/adopt_test.go
@@ -3880,6 +3880,75 @@ func TestRefusedPromotionIsNeitherStagedNorRelayed(t *testing.T) {
}
}
+// TestChannelBackOnTheRestoredBuildClearsTheRefusal (final review F8): v2
+// was refused and rolled back to v1, then the channel was promoted back to
+// v1. The pass finds the box up to date, restarts nothing, and removes the
+// v2 record that no longer blocks anything.
+func TestChannelBackOnTheRestoredBuildClearsTheRefusal(t *testing.T) {
+ host := "console.example.com"
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ shipRelease(t, s, "bab", "console", "v1", adoptPlatform, []byte("good build"))
+ shipRelease(t, s, "bab", "console", "v2", adoptPlatform, []byte("bad build"))
+ if err := StageArtifact(opt, "bab", "v1", "console", []byte("good build")); err != nil {
+ t.Fatal(err)
+ }
+ addRoute(t, reg, host, "console", "canary/x", false)
+ if err := PointLive(opt, host, "bab", "v1", "console"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Promote(ctx, s, priv, "bab", "console", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ bake := t.TempDir()
+ if err := WriteRefusal(bake, Refusal{Host: host, App: "console", Version: "v2",
+ PointerPromotedAt: time.Unix(1, 0).UTC(), Reason: "exited: status=2", RestoredVersion: "v1", Recovered: true}); err != nil {
+ t.Fatal(err)
+ }
+ restart := func(h string) error { t.Fatalf("restart consulted for up-to-date %s", h); return nil }
+ if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake)); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ if v, _ := LiveVersion(opt, host); v != "v1" {
+ t.Fatalf("live = %q, want v1", v)
+ }
+ if r, _ := ReadRefusal(bake, host); r != nil {
+ t.Fatalf("stale v2 refusal survived the channel's return to v1: %+v", r)
+ }
+}
+
+// TestUpToDateKeepsARefusalThatStillBlocks: the link is on the refused build
+// only when its rollback failed; that record still names the pointer and
+// stays for the watcher.
+func TestUpToDateKeepsARefusalThatStillBlocks(t *testing.T) {
+ host := "console.example.com"
+ ctx, reg, s, pub, opt := startUnitFixture(t, host)
+ if err := StageArtifact(opt, "bab", "new", "console", []byte("bad build")); err != nil {
+ t.Fatal(err)
+ }
+ if err := PointLive(opt, host, "bab", "new", "console"); err != nil {
+ t.Fatal(err)
+ }
+ ptr, err := Channel(ctx, s, pub, "bab", "console", "canary/x")
+ if err != nil {
+ t.Fatal(err)
+ }
+ bake := t.TempDir()
+ if err := WriteRefusal(bake, Refusal{Host: host, App: "console", Version: ptr.Version,
+ PointerPromotedAt: ptr.PromotedAt, Reason: "exited: status=2", RecoveryError: "could not roll the symlink back"}); 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 r, _ := ReadRefusal(bake, host); r == nil {
+ t.Fatal("a refusal that still blocks the pointer was removed")
+ }
+}
+
// 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) {