console: /healthz reports the build the PROCESS is running
X-Carlos-Version is stamped by the edge from the route row, so it reports what adoption intended from the moment the symlink moves — whether or not anything restarted. systemctl is-active is no better. Only the process can answer this honestly, and now it does. 'ok' stays the first token: vet and the edge probe read status not body, but a human eyeballing it should see what they always saw.
2 files changed,
+35
−1
internal/console/server.go+11 −1internal/console/server_test.go+24 −0
diff --git a/internal/console/server.go b/internal/console/server.go| index ff4d49cd2..04caade06 100644 |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| @@ -37,6 +37,7 @@ import ( |
| "github.com/carlosframework/platform/internal/status" |
| "github.com/carlosframework/platform/internal/svcrole" |
| "github.com/carlosframework/platform/internal/usage" |
| + "github.com/carlosframework/platform/internal/version" |
| ) |
| //go:embed templates/*.html |
| @@ -797,8 +798,17 @@ func NewServer(cfg Config, st *Store) (*Server, error) { |
| // still land somewhere sensible rather than 404ing. |
| s.mux.HandleFunc("GET /apps", s.handleLegacyApps) |
| s.mux.HandleFunc("GET /apps/{app}", s.handleLegacyApp) |
| + // "ok <build>" — the build comes from THIS PROCESS (ldflags, at link |
| + // time), which is the whole point: X-Carlos-Version is stamped by the |
| + // edge from the route row, so it reports the version adoption INTENDED |
| + // from the moment the symlink moves, whether or not anything restarted. |
| + // `carlos deploy` polls this instead for any app declaring a build probe. |
| + // |
| + // "ok" stays the first token: internal/vet and the edge health probe read |
| + // this endpoint and check status, not body, but keeping the prefix means |
| + // a human or a script eyeballing it sees what it always saw. |
| s.mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { |
| - fmt.Fprintln(w, "ok") |
| + fmt.Fprintln(w, "ok", version.Build) |
| }) |
| // Catch up on accounts minted outside this database — `carlos accounts |
| // create` writes the bucket manifest and the registry row and never opens |
diff --git a/internal/console/server_test.go b/internal/console/server_test.go| index e756ca8c3..099cd46d3 100644 |
| --- a/internal/console/server_test.go |
| +++ b/internal/console/server_test.go |
| @@ -23,6 +23,7 @@ import ( |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/status" |
| + "github.com/carlosframework/platform/internal/version" |
| ) |
| // consoleFixture is a whole console: fixture platform (registry + bucket |
| @@ -302,6 +303,29 @@ func TestConsoleE2E(t *testing.T) { |
| } |
| } |
| +// TestHealthzCarriesTheBuild: the console's own build, from the PROCESS. |
| +// |
| +// This is the deploy watch's exit criterion, and it exists because both cheap |
| +// answers lie. X-Carlos-Version is stamped by the EDGE from the route row |
| +// (edge.go:1336), so adoption sets it while the old process keeps serving; |
| +// `systemctl is-active` says "active" for a process started from the previous |
| +// version just as happily (tito, 2026-08-09, seconds after adopt and restart |
| +// both reported success). Only the running process can answer this. |
| +func TestHealthzCarriesTheBuild(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + status, _, body := get(t, &http.Client{}, fx.ts.URL+"/healthz") |
| + if status != 200 { |
| + t.Fatalf("/healthz = %d, want 200", status) |
| + } |
| + if !strings.Contains(body, "ok") { |
| + t.Fatalf("/healthz body = %q, want it to still start with ok (vet and the edge probe read this)", body) |
| + } |
| + fields := strings.Fields(body) |
| + if len(fields) != 2 || fields[1] != version.Build { |
| + t.Fatalf("/healthz body = %q, want exactly \"ok %s\" — the watch matches a whitespace-delimited token", body, version.Build) |
| + } |
| +} |
| + |
| var confirmLinkRe = regexp.MustCompile(`https?://[^\s"]+/auth/confirm\?token=[A-Za-z0-9_%.~-]+`) |
| func TestReturningSignInViaConfirmationLink(t *testing.T) { |