carlos / platform Public

deploy: no-host static redeploy promotes onto the records' own channel

Re-review important #1: staticRecordChannel answered "" for a no-host
call, so `carlos deploy -kind static -app X` — the obvious redeploy once
a domain is attached — fell to the channels listing: a two-channel app was
refused despite the records naming the channel, and a single listed
channel DIFFERING from the records' promoted onto the wrong channel while
watching hosts that follow another. Now the no-host case resolves the
single distinct non-empty channel the records agree on, and only
disagreement (or nothing recorded) falls through to the listing and its
-channel refusal. The gate test's no-host case now runs with no -channel
(the old assertion's explicit flag hid exactly this gap) plus a
disagreeing-records case pinning the fallthrough.

Minors: staticDeployHosts skips non-static records itself (self-consistent
rather than caller-guarded); attachUnclaimedHost's comment states that it
runs on redeploys now and why a 401/403 on the GET rightly aborts (same
instanceGate the deploy's instanceList already passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell df5f5dfb250fa9f7b4baa320147c9400d29bd575 parent 41fc607
2 files changed, +68 −13
  • deploy.go +40 −9
  • deploy_test.go +28 −4
diff --git a/deploy.go b/deploy.go
index 03f75ad44..541e76a9a 100644
--- a/deploy.go
+++ b/deploy.go
@@ -252,21 +252,38 @@ func staticOnlyRecords(recs []apiInstanceRecord) bool {
return true
}
-// staticRecordChannel is the channel of the static record naming host, or ""
-// — the record a first-route attach wrote already knows what its domain
-// follows, so a redeploy needs no -channel and no channels-listing round
-// trip. "" (host absent, or no -host at all) sends the caller to the
-// channels listing instead.
+// staticRecordChannel resolves a static-path deploy's channel from the
+// records themselves — the record a first-route attach wrote already knows
+// what its domain follows, so a redeploy needs no -channel and no
+// channels-listing round trip. With -host, it is that host's record's
+// channel; without, it is the single distinct non-empty channel the records
+// agree on — the no-host redeploy is watching THOSE hosts, so their own
+// channel is the only one that keeps the promote and the watch pointed at
+// the same thing (a channels listing that happened to hold one OTHER
+// channel would promote onto it while watching hosts following another).
+// "" — host unrecorded, records disagreeing, or nothing recorded — sends
+// the caller to the channels listing (and its refusal names -channel).
func staticRecordChannel(recs []apiInstanceRecord, host string) string {
- if host == "" {
+ if host != "" {
+ for _, r := range recs {
+ if r.Host == host {
+ return r.Channel
+ }
+ }
return ""
}
+ channel := ""
for _, r := range recs {
- if r.Host == host {
- return r.Channel
+ if r.Channel == "" {
+ continue
+ }
+ if channel == "" {
+ channel = r.Channel
+ } else if r.Channel != channel {
+ return "" // disagreeing records: a human picks, via -channel
}
}
- return ""
+ return channel
}
// staticDeployHosts resolves the hosts a static-path deploy watches. -host
@@ -284,6 +301,12 @@ func staticDeployHosts(hostFlag string, recs []apiInstanceRecord) ([]string, err
}
var hosts []string
for _, r := range recs {
+ // Static records only, so the helper holds its own invariant
+ // rather than trusting the caller's gate: an instance record's
+ // host is adopted and watched via the instance path, never here.
+ if r.Kind != "static" {
+ continue
+ }
hosts = append(hosts, r.Host)
}
if len(hosts) > 0 {
@@ -341,6 +364,14 @@ func staticDeployChannel(chans []apiChannel) (string, error) {
// but it is still a write request this deploy has no business sending: the
// list already proves there is nothing to do, and only a host held by a
// DIFFERENT owner answers 409 "hostname unavailable".
+//
+// Since the kind-aware static gate, this runs on REDEPLOYS too (records
+// present), not just the first deploy — so a 401/403 on the GET aborting
+// the whole deploy is a question worth answering: the domains door sits
+// behind the SAME instanceGate as the instanceList runDeploy has already
+// passed, so a token that got this far can list domains too, and a 401/403
+// here means the credential genuinely lost authority mid-deploy — worth
+// stopping for, not degrading past.
func attachUnclaimedHost(ctx context.Context, sess *apiSession, acct apiAcct, app, host, kind, channel string, recs []apiInstanceRecord) {
domains, err := sess.domainList(ctx, acct, app)
if err != nil {
diff --git a/deploy_test.go b/deploy_test.go
index 4172aaabf..500092546 100644
--- a/deploy_test.go
+++ b/deploy_test.go
@@ -1168,10 +1168,34 @@ func TestDeployTargetsStaticKindAware(t *testing.T) {
t.Fatal("second host never consulted the channels listing")
}
- // No -host at all: the static records themselves are the watch targets.
- _, hosts, static, err = deployTargets([]apiInstanceRecord{staticRec}, "static", "canary/site", "", channels)
- if err != nil || !static || len(hosts) != 1 || hosts[0] != "site.example.test" {
- t.Fatalf("no -host = hosts %v static %v err %v, want the record's host watched", hosts, static, err)
+ // No -host at all — the obvious redeploy invocation once a domain is
+ // attached: the records are the watch targets AND their agreed channel
+ // is the promote target, with no -channel and NO channels round trip. A
+ // listing that happened to hold one other channel must not win here —
+ // promoting onto it while watching hosts that follow the record's
+ // channel would verify the wrong thing (re-review, important #1).
+ channelsCalled = false
+ ch, hosts, static, err = deployTargets([]apiInstanceRecord{staticRec}, "static", "", "", channels)
+ if err != nil || !static || len(hosts) != 1 || hosts[0] != "site.example.test" || ch != "canary/site" {
+ t.Fatalf("no -host = ch %q hosts %v static %v err %v, want the record's host watched on the record's channel", ch, hosts, static, err)
+ }
+ if channelsCalled {
+ t.Fatal("no -host redeploy consulted the channels listing — the records already agree on a channel")
+ }
+
+ // Records DISAGREEING on a channel resolve nothing: fall through to the
+ // channels listing (whose single entry answers, and whose plural case
+ // refuses naming -channel) rather than guessing between the records.
+ twoChannels := []apiInstanceRecord{
+ {Host: "site.example.test", Channel: "canary/site", Kind: "static"},
+ {Host: "blog.example.test", Channel: "canary/blog", Kind: "static"},
+ }
+ ch, hosts, static, err = deployTargets(twoChannels, "static", "", "", channels)
+ if err != nil || !static || len(hosts) != 2 || ch != "canary/other" {
+ t.Fatalf("disagreeing records = ch %q hosts %v static %v err %v, want the listing's channel via fallthrough", ch, hosts, static, err)
+ }
+ if !channelsCalled {
+ t.Fatal("disagreeing records never fell through to the channels listing")
}
// A real instance record still pulls a -kind static deploy onto the