carlos / platform Public

deploy: name the destination URL before ship on every path

One instance prints its URL; a fleet-sized channel prints an aggregate
count (the live lines and timeout diagnostics already name hosts one by
one); the static note stops repeating hosts the deploying line just
printed. The held-for-bake path now names where the promote was going
before it returns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 332670c4eb85e13e27ea15ed41d2711a35cde1c0 parent a1e15c3
3 files changed, +55 −2
  • deploy.go +23 −2
  • deploy_e2e_test.go +8 −0
  • deploy_test.go +24 −0
diff --git a/deploy.go b/deploy.go
index 08da41667..a74fc7264 100644
--- a/deploy.go
+++ b/deploy.go
@@ -70,6 +70,9 @@ func runDeploy(args []string) {
if err != nil {
log.Fatalf("deploy: %v", err)
}
+ if line := deployingLine(hosts, channel); line != "" {
+ fmt.Fprintln(stdout, line)
+ }
// printBanner=false: the "using ..." line already printed above, before
// the instance list — so a listing failure still names the console —
// and runShipAPI would otherwise print it a second time.
@@ -98,8 +101,8 @@ func runDeploy(args []string) {
// one window (defaultDeployTimeout, pinned by test), so nothing here
// adjusts the clock — the operator just gets told why "promoted" can
// sit in front of the old page for a minute.
- mutedf(stdout, "watching %s — a static route re-checks its channel about every %ds, so the URL can take that long to flip\n",
- strings.Join(hosts, ", "), int(staticRecheckWindow.Seconds()))
+ mutedf(stdout, "a static route re-checks its channel about every %ds, so the URL can take that long to flip\n",
+ int(staticRecheckWindow.Seconds()))
}
targets := make([]watchTarget, 0, len(hosts))
for _, h := range hosts {
@@ -125,6 +128,24 @@ const defaultDeployTimeout = 120 * time.Second
// constant would promise a match this process cannot know it has.
const staticRecheckWindow = 60 * time.Second
+// deployingLine names the destination a deploy is about to write to,
+// printed BEFORE ship so every later path — the bake hold, the watch,
+// even a ship failure — has already said where this deploy was going.
+// One instance gets its URL; a fleet-sized channel gets a count (the
+// per-host "live" lines and timeout diagnostics name hosts as they
+// resolve), and the no-instance path gets nothing: there is no URL to
+// name, and its own "promoted, nothing to watch" message stays the
+// honest report.
+func deployingLine(hosts []string, channel string) string {
+ switch len(hosts) {
+ case 0:
+ return ""
+ case 1:
+ return "deploying https://" + hosts[0]
+ }
+ return fmt.Sprintf("deploying %d instances on %s", len(hosts), channel)
+}
+
// deployTargets decides what this deploy promotes to and what it then watches,
// and reports whether it took the static path. It is a function, not a stretch
// of runDeploy, so the decision can be tested without a console: `channels` is
diff --git a/deploy_e2e_test.go b/deploy_e2e_test.go
index db2f6e6c9..a810f457e 100644
--- a/deploy_e2e_test.go
+++ b/deploy_e2e_test.go
@@ -199,6 +199,7 @@ func TestE2EStaticDeployThroughConsole(t *testing.T) {
"-channel", channel, "-host", host, "-timeout", "30s", siteDir})
out := buf.String()
for _, wantLine := range []string{
+ "deploying https://" + host,
"shipped " + app + " sha0001",
"promoted " + app + " sha0001 -> " + channel,
"re-checks its channel",
@@ -208,6 +209,9 @@ func TestE2EStaticDeployThroughConsole(t *testing.T) {
t.Fatalf("static deploy stdout missing %q:\n%s", wantLine, out)
}
}
+ if strings.Contains(out, "watching "+host) {
+ t.Fatalf("static note still repeats the host the deploying line just printed:\n%s", out)
+ }
ptr, err := release.Channel(context.Background(), relStore, pub, release.OpsAccount, app, channel)
if err != nil {
t.Fatalf("channel after deploy: %v", err)
@@ -409,6 +413,7 @@ func TestE2EDeployThroughConsole(t *testing.T) {
// The three lines an operator reads, in order, and the "live" line names
// the PUBLIC url — not the loopback origin the watch happened to dial.
for _, want := range []string{
+ "deploying https://" + host,
"shipped " + app + " " + version,
"promoted " + app + " " + version + " -> " + channel,
"live https://" + host,
@@ -420,6 +425,9 @@ func TestE2EDeployThroughConsole(t *testing.T) {
if i, j := strings.Index(out, "shipped"), strings.Index(out, "promoted"); i > j {
t.Fatalf("promoted printed before shipped:\n%s", out)
}
+ if i, j := strings.Index(out, "deploying https://"), strings.Index(out, "shipped"); i < 0 || i > j {
+ t.Fatalf("deploying line missing or printed after shipped:\n%s", out)
+ }
if strings.Contains(out, edgeSrv.URL) {
t.Fatalf("deploy printed the test's loopback origin instead of the public URL:\n%s", out)
}
diff --git a/deploy_test.go b/deploy_test.go
index c90cffa10..df287c329 100644
--- a/deploy_test.go
+++ b/deploy_test.go
@@ -398,3 +398,27 @@ func TestWatchVersionCacheBustsEveryPoll(t *testing.T) {
t.Fatalf("cache-bust param did not vary between polls: %v", queries)
}
}
+
+// TestDeployingLine pins the destination line deploy prints before ship:
+// the one place every later path (held, watch, even a ship failure) gets
+// its "where is this going" from. Zero hosts is the no-instance path,
+// which keeps its own honest "nothing to watch" message instead.
+func TestDeployingLine(t *testing.T) {
+ cases := []struct {
+ name string
+ hosts []string
+ channel string
+ want string
+ }{
+ {"no hosts", nil, "stable", ""},
+ {"one host", []string{"hello.baf.oncarlos.com"}, "stable",
+ "deploying https://hello.baf.oncarlos.com"},
+ {"fleet", []string{"a.example.com", "b.example.com", "c.example.com"}, "stable",
+ "deploying 3 instances on stable"},
+ }
+ for _, c := range cases {
+ if got := deployingLine(c.hosts, c.channel); got != c.want {
+ t.Errorf("%s: deployingLine = %q, want %q", c.name, got, c.want)
+ }
+ }
+}