From a1ae532c6e1f3d72b032dbc4ea9de193b4962bf5 Mon Sep 17 00:00:00 2001 From: Timmy Date: Wed, 15 Jul 2026 16:03:49 +0100 Subject: [PATCH] Add /api/version endpoint for upgrade convergence checks Reports crate version + compile-time BUILD_GIT_HASH as JSON. The gateway will poll this after `upgrade all` to verify each sled is actually running the published artifact. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019fHdm92yjvguPi2LiXfLB9 --- server/src/http/mod.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/server/src/http/mod.rs b/server/src/http/mod.rs index b3a6a670..cedc36f6 100644 --- a/server/src/http/mod.rs +++ b/server/src/http/mod.rs @@ -63,6 +63,23 @@ pub fn health_handler() -> poem::Response { poem::Response::builder().status(StatusCode::OK).body("ok") } +/// Version probe — reports the crate version and compile-time git hash. +/// +/// Used by the gateway to verify that an upgrade converged: after `upgrade all` +/// the gateway polls this endpoint until the reported `git_hash` matches the +/// published artifact's hash (or times out). +#[poem::handler] +pub fn version_handler() -> poem::Response { + let body = serde_json::json!({ + "version": env!("CARGO_PKG_VERSION"), + "git_hash": option_env!("BUILD_GIT_HASH").unwrap_or("unknown"), + }); + poem::Response::builder() + .status(StatusCode::OK) + .content_type("application/json") + .body(body.to_string()) +} + /// Assemble the full Poem route tree (WebSocket, MCP, OAuth, assets, webhooks). pub fn build_routes( ctx: AppContext, @@ -77,6 +94,7 @@ pub fn build_routes( let mut route = Route::new() .at("/health", get(health_handler)) + .at("/api/version", get(version_handler)) .at("/ws", get(ws::ws_handler)) .at("/crdt-sync", get(crate::crdt_sync::crdt_sync_handler)) .at("/rpc", post(rpc_http_handler)) @@ -335,6 +353,27 @@ mod tests { let _endpoint = build_routes(ctx, None, None, 3001, None); } + #[tokio::test] + async fn version_endpoint_reports_version_and_git_hash() { + let tmp = tempfile::tempdir().unwrap(); + let ctx = context::AppContext::new_test(tmp.path().to_path_buf()); + let app = build_routes(ctx, None, None, 3001, None); + let cli = poem::test::TestClient::new(app); + + let resp = cli.get("/api/version").send().await; + resp.assert_status_is_ok(); + let text = resp.0.into_body().into_string().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&text).unwrap(); + assert_eq!( + body.get("version").and_then(|v| v.as_str()), + Some(env!("CARGO_PKG_VERSION")) + ); + assert!( + body.get("git_hash").and_then(|v| v.as_str()).is_some(), + "git_hash must be present: {body}" + ); + } + #[test] fn build_routes_accepts_custom_port() { // Verify build_routes compiles and runs with a non-default port,