cellos_server/routes/meta.rs
1//! `/v1/version` — server build metadata.
2//!
3//! This is the canonical day-1 reachability probe for cellctl. A fresh
4//! operator types `cellctl version` to confirm the server is alive
5//! before doing anything else; the route MUST exist (E2E report
6//! SRV-001) and MUST require Bearer auth like every other route.
7//!
8//! ## Response shape (stable)
9//!
10//! ```json
11//! {
12//! "server": {
13//! "version": "0.5.0", // env!("CARGO_PKG_VERSION")
14//! "gitSha": "abc1234", // optional; only present when CELLOS_GIT_SHA was set at build time
15//! "buildProfile": "release" // "release" or "debug"
16//! },
17//! "api": { "version": "v1" }
18//! }
19//! ```
20//!
21//! `gitSha` is best-effort: the field is *omitted* (via
22//! `#[serde(skip_serializing_if = "Option::is_none")]`) when the
23//! `CELLOS_GIT_SHA` env var is unset at build time. This keeps test
24//! output stable regardless of git state and avoids forcing a
25//! `build.rs` that would re-run on every commit.
26//!
27//! ## Why a typed response instead of `serde_json::json!`
28//!
29//! The response shape is a stable contract — cellctl pins on
30//! `server.version` today and future tooling will pin on more fields.
31//! A struct catches typos at compile time and gives us a place to hang
32//! doc comments that explain each field.
33
34use axum::extract::State;
35use axum::http::HeaderMap;
36use axum::Json;
37use serde::Serialize;
38
39use crate::auth::require_bearer;
40use crate::error::AppError;
41use crate::state::AppState;
42
43/// Compile-time build profile string. `cfg!(debug_assertions)` is
44/// evaluated at the call site, so this constant materialises to a
45/// single `&'static str` per build.
46const BUILD_PROFILE: &str = if cfg!(debug_assertions) {
47 "debug"
48} else {
49 "release"
50};
51
52/// Top-level wire shape returned by `GET /v1/version`.
53#[derive(Debug, Serialize)]
54pub struct VersionResponse {
55 pub server: ServerInfo,
56 pub api: ApiInfo,
57}
58
59#[derive(Debug, Serialize)]
60pub struct ServerInfo {
61 /// `env!("CARGO_PKG_VERSION")` — the cellos-server crate version.
62 pub version: &'static str,
63 /// Short git SHA captured at build time via the optional
64 /// `CELLOS_GIT_SHA` env var. Omitted from the JSON when unset so
65 /// downstream consumers can rely on "present means real".
66 #[serde(rename = "gitSha", skip_serializing_if = "Option::is_none")]
67 pub git_sha: Option<&'static str>,
68 /// `"release"` for non-debug builds, `"debug"` otherwise.
69 #[serde(rename = "buildProfile")]
70 pub build_profile: &'static str,
71}
72
73#[derive(Debug, Serialize)]
74pub struct ApiInfo {
75 /// API surface version. Stable string — bump only when the URL
76 /// prefix bumps (e.g. `/v2`).
77 pub version: &'static str,
78}
79
80/// `GET /v1/version` handler. Bearer auth required.
81pub async fn get_version(
82 State(state): State<AppState>,
83 headers: HeaderMap,
84) -> Result<Json<VersionResponse>, AppError> {
85 require_bearer(&headers, &state.api_token)?;
86 Ok(Json(VersionResponse {
87 server: ServerInfo {
88 version: env!("CARGO_PKG_VERSION"),
89 git_sha: option_env!("CELLOS_GIT_SHA"),
90 build_profile: BUILD_PROFILE,
91 },
92 api: ApiInfo { version: "v1" },
93 }))
94}