carlos / platform Public

release: a process group shares the bound route's channel

Final review F4 (Plan 1 D1). Every group refusal carries the bound route's PromotedAt, so a same-upstream route on another channel (a separate promotion) got a record that could never block its own pointer. processGroup now also requires routeChannel(r) == routeChannel(bound); such a route is neither repointed nor refused.

Test: TestRollbackGroupLeavesAnotherChannelAlone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ce103d11d3395a93bdc51acda5d653d30625a6a2 parent 44e9fbb
2 files changed, +67 −3
  • internal/release/rollback.go +10 −3
  • internal/release/rollback_test.go +57 −0
diff --git a/internal/release/rollback.go b/internal/release/rollback.go
index 98b53309d..57f1e3f50 100644
--- a/internal/release/rollback.go
+++ b/internal/release/rollback.go
@@ -34,8 +34,14 @@ func upstream(rt registry.Route) string {
}
// processGroup is every OTHER route served by bound's process: unstamped,
-// same account and app, same non-empty upstream. A bound route with no
-// upstream is a group of one.
+// same account and app, same non-empty upstream, and the SAME CHANNEL. A
+// bound route with no upstream is a group of one.
+//
+// The channel matters because every group record is written with bound's
+// promotion (its pointer's PromotedAt). A same-upstream route on another
+// channel follows a separate promotion, so a record stamped with bound's
+// would never block its own pointer: its next pass would re-adopt the
+// rejected build anyway. It is left out, untouched and unrefused.
func processGroup(reg *registry.Registry, bound registry.Route) []registry.Route {
up := upstream(bound)
if up == "" {
@@ -49,7 +55,8 @@ func processGroup(reg *registry.Registry, bound registry.Route) []registry.Route
var out []registry.Route
for _, r := range all {
if r.Host == bound.Host || r.Unit != "" || r.App != bound.App ||
- RouteAccount(r) != RouteAccount(bound) || upstream(r) != up {
+ RouteAccount(r) != RouteAccount(bound) || upstream(r) != up ||
+ routeChannel(r) != routeChannel(bound) {
continue
}
out = append(out, r)
diff --git a/internal/release/rollback_test.go b/internal/release/rollback_test.go
index b7604c80c..5687b96e1 100644
--- a/internal/release/rollback_test.go
+++ b/internal/release/rollback_test.go
@@ -85,6 +85,63 @@ func TestNotUpRollsTheWholeGroupBackAndRefusesEveryHost(t *testing.T) {
}
}
+// Final review F4: a same-upstream route following ANOTHER channel is a
+// separate promotion. It is neither repointed nor refused with the bound
+// route's PromotedAt.
+func TestRollbackGroupLeavesAnotherChannelAlone(t *testing.T) {
+ ctx, reg, s, pub, opt := startUnitFixture(t, "console.example.com")
+ if err := reg.SetUnit("console.example.com", "console.service"); err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.SetUpstreams("console.example.com", "127.0.0.1:8090", "", ""); err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.Upsert(registry.Route{Host: "zz-beta.example.com", Kind: "instance", App: "console",
+ Channel: "beta", Addr: "127.0.0.1:8090"}); err != nil {
+ t.Fatal(err)
+ }
+ if err := StageArtifact(opt, "bab", "new", "console", []byte("bad build")); err != nil {
+ t.Fatal(err)
+ }
+ if err := PointLive(opt, "zz-beta.example.com", "bab", "new", "console"); err != nil {
+ t.Fatal(err)
+ }
+ if got := processGroup(reg, mustRoute(t, reg, "console.example.com")); len(got) != 0 {
+ t.Fatalf("group = %+v, want none: zz-beta follows another channel", got)
+ }
+ restart := func(h string) error {
+ if h == "console.example.com" {
+ return &UnitNotUpError{Unit: "console.service", Reason: "not ready after 30s"}
+ }
+ return nil
+ }
+ bake := t.TempDir()
+ var out bytes.Buffer
+ withCapturedStdout(t, &out, func() {
+ if err := Pass(ctx, reg, s, pub, opt, restart, BakeDir(bake), WithRollbackRestart(func(string) error { return nil })); err != nil {
+ t.Fatalf("Pass: %v", err)
+ }
+ })
+ if v, _ := LiveVersion(opt, "zz-beta.example.com"); v != "new" {
+ t.Fatalf("another channel's route was rolled back to %q", v)
+ }
+ if r, _ := ReadRefusal(bake, "zz-beta.example.com"); r != nil {
+ t.Fatalf("another channel's route was refused with the bound route's promotion: %+v", r)
+ }
+ if r, _ := ReadRefusal(bake, "console.example.com"); r == nil {
+ t.Fatal("the bound route has no refusal")
+ }
+}
+
+func mustRoute(t *testing.T, reg *registry.Registry, host string) registry.Route {
+ t.Helper()
+ rt, err := reg.Get(host)
+ if err != nil || rt == nil {
+ t.Fatalf("get %s: %+v, %v", host, rt, err)
+ }
+ return *rt
+}
+
func TestRollbackGroupLeavesOtherUpstreamAlone(t *testing.T) {
ctx, reg, s, pub, opt := startUnitFixture(t, "console.example.com")
if err := reg.SetUnit("console.example.com", "console.service"); err != nil {