Add serve command to quire-ci
Adds a subcommand that starts an axum HTTP server
with and endpoints. Moves axum to a workspace dependency
and adds tracing-subscriber for structured logging. This is the
foundation for the CI server — endpoints will be added incrementally.
Assisted-by: Owl Alpha via pi
diff --git a/Cargo.lock b/Cargo.lock
index 51628a3..e87f313 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2548,6 +2548,7 @@ dependencies = [
name = "quire-ci"
version = "0.1.0"
dependencies = [
+ "axum",
"facet",
"figue",
"fs-err",
@@ -2566,6 +2567,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-opentelemetry",
+ "tracing-subscriber",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index e88dde7..d5ea1c2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ members = ["quire-ci", "quire-core", "quire-server"]
resolver = "3"
[workspace.dependencies]
+axum = "*"
clap = { version = "*", features = ["derive", "env"] }
facet = "*"
fs-err = "*"
diff --git a/quire-ci/Cargo.toml b/quire-ci/Cargo.toml
index 76a758b..5fedb85 100644
--- a/quire-ci/Cargo.toml
+++ b/quire-ci/Cargo.toml
@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
+axum = { workspace = true }
facet = { workspace = true }
figue = "*"
fs-err = { workspace = true }
@@ -17,8 +18,9 @@ serde = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
-tokio = { workspace = true, features = ["rt-multi-thread"] }
+tokio = { workspace = true, features = ["full"] }
opentelemetry = { workspace = true }
opentelemetry_sdk = { workspace = true }
tracing = { workspace = true }
tracing-opentelemetry = { workspace = true }
+tracing-subscriber = { workspace = true }
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 5179ad3..7d5ed03 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -1,3 +1,4 @@
+mod server;
mod sink;
use std::cell::RefCell;
@@ -120,6 +121,13 @@ enum Commands {
#[facet(args::named, default)]
git_dir: Option<PathBuf>,
},
+
+ /// Start the HTTP server.
+ Serve {
+ /// Port to listen on.
+ #[facet(args::named, default = 3000)]
+ port: u16,
+ },
}
/// RAII wrapper around a tempdir holding captured sh logs. On drop,
@@ -288,6 +296,13 @@ fn main() -> Result<()> {
match cli.command {
Commands::Validate => validate(workspace),
+ Commands::Serve { port } => {
+ let rt = tokio::runtime::Builder::new_multi_thread()
+ .enable_all()
+ .build()
+ .into_diagnostic()?;
+ rt.block_on(server::run(port))
+ }
Commands::Run {
events,
out_dir,
diff --git a/quire-ci/src/server.rs b/quire-ci/src/server.rs
new file mode 100644
index 0000000..9ac7a0d
--- /dev/null
+++ b/quire-ci/src/server.rs
@@ -0,0 +1,49 @@
+use std::net::SocketAddr;
+
+use axum::Router;
+use axum::routing::get;
+use miette::{IntoDiagnostic, Result};
+use tracing_subscriber::EnvFilter;
+use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
+use tracing_subscriber::util::SubscriberInitExt;
+
+const VERSION: &str = env!("QUIRE_VERSION");
+
+async fn health() -> &'static str {
+ "ok"
+}
+
+async fn index() -> String {
+ format!("quire-ci {VERSION}\n")
+}
+
+pub async fn run(port: u16) -> Result<()> {
+ let filter = EnvFilter::builder()
+ .with_env_var("QUIRE_LOG")
+ .from_env()
+ .into_diagnostic()?;
+
+ let fmt_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
+
+ tracing_subscriber::registry()
+ .with(fmt_layer)
+ .with(filter)
+ .init();
+
+ let app = Router::new()
+ .route("/health", get(health))
+ .route("/", get(index));
+
+ let addr = SocketAddr::from(([0, 0, 0, 0], port));
+ tracing::info!(%addr, "starting HTTP server");
+
+ let listener = tokio::net::TcpListener::bind(addr)
+ .await
+ .into_diagnostic()?;
+
+ axum::serve(listener, app).await.into_diagnostic()?;
+
+ tracing::info!(version = %VERSION, "server shutdown complete");
+
+ Ok(())
+}