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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fHdm92yjvguPi2LiXfLB9
This commit is contained in:
Timmy
2026-07-15 16:03:49 +01:00
co-authored by Claude Fable 5
parent 2a7db16a77
commit a1ae532c6e
+39
View File
@@ -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,