Make transport optional for local CI runs
`quire ci run` no longer mints a fake server session with port=0.
Instead it uses `Transport::Local`, which carries no session info.
`quire-ci run` now accepts `--run-id` and `--server-url` as optional;
when both are absent with filesystem transport it runs without a session,
matching the behaviour of `quire-ci local`. API transport still requires
both flags and `QUIRE_CI_TOKEN`.
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 69e0d88..611842d 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -95,15 +95,18 @@ enum Commands {
}
/// Session and transport flags for orchestrator-dispatched runs.
+/// All flags are optional — when `--run-id` and `--server-url` are
+/// absent and `--transport` is `filesystem` (the default), the run
+/// executes without a server session (local mode).
#[derive(clap::Args, Debug)]
struct TransportFlags {
/// Run ID assigned by the orchestrator.
#[arg(long)]
- run_id: String,
+ run_id: Option<String>,
/// Base URL of quire-server (e.g. `http://127.0.0.1:3000`).
#[arg(long)]
- server_url: String,
+ server_url: Option<String>,
/// Transport for CI ↔ server communication.
#[arg(long, default_value = "filesystem", value_enum)]
@@ -112,17 +115,38 @@ struct TransportFlags {
impl TransportFlags {
fn resolve(self, auth_token: Option<String>) -> miette::Result<TransportArgs> {
- let auth_token =
- auth_token.ok_or_else(|| miette::miette!("QUIRE_CI_TOKEN env var is required"))?;
- let session = ApiSession {
- run_id: self.run_id,
- server_url: self.server_url,
- auth_token,
- };
- Ok(match self.transport {
- Transport::Filesystem => TransportArgs::Filesystem(Some(session)),
- Transport::Api => TransportArgs::Api(session),
- })
+ match self.transport {
+ Transport::Filesystem => match (self.run_id, self.server_url) {
+ (None, None) => Ok(TransportArgs::Filesystem(None)),
+ (Some(run_id), Some(server_url)) => {
+ let auth_token = auth_token
+ .ok_or_else(|| miette::miette!("QUIRE_CI_TOKEN env var is required"))?;
+ Ok(TransportArgs::Filesystem(Some(ApiSession {
+ run_id,
+ server_url,
+ auth_token,
+ })))
+ }
+ _ => Err(miette::miette!(
+ "--run-id and --server-url must both be provided or both omitted"
+ )),
+ },
+ Transport::Api => {
+ let run_id = self
+ .run_id
+ .ok_or_else(|| miette::miette!("--run-id is required for api transport"))?;
+ let server_url = self
+ .server_url
+ .ok_or_else(|| miette::miette!("--server-url is required for api transport"))?;
+ let auth_token = auth_token
+ .ok_or_else(|| miette::miette!("QUIRE_CI_TOKEN env var is required"))?;
+ Ok(TransportArgs::Api(ApiSession {
+ run_id,
+ server_url,
+ auth_token,
+ }))
+ }
+ }
}
}
diff --git a/quire-server/src/bin/quire/commands/ci.rs b/quire-server/src/bin/quire/commands/ci.rs
index 1ba27a3..56ea9ed 100644
--- a/quire-server/src/bin/quire/commands/ci.rs
+++ b/quire-server/src/bin/quire/commands/ci.rs
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use miette::{IntoDiagnostic, Result};
use quire::Quire;
-use quire::ci::{Ci, CommitRef, RunMeta, Runs, Transport, TransportMode};
+use quire::ci::{Ci, CommitRef, RunMeta, Runs, Transport};
/// Validate a repo's ci.fnl without executing any jobs.
///
@@ -74,11 +74,7 @@ pub async fn run(quire: &Quire, maybe_sha: Option<&str>) -> Result<()> {
pushed_at: jiff::Timestamp::now(),
};
- // Local `quire ci run` uses filesystem transport. Session info is
- // minted so quire-ci receives the standard remote-mode flags; the
- // server_url is a loopback placeholder (never contacted in filesystem
- // mode).
- let transport = Transport::for_new_run(TransportMode::Filesystem, 0);
+ let transport = Transport::local();
let run = runs.create(&meta, &transport)?;
let run_id = run.id().to_string();
println!(
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 381959b..2fa38b1 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -42,15 +42,23 @@ pub enum TransportMode {
/// Runtime transport for a single CI run. Built once per run from
/// the config-shape [`TransportMode`] + the server's listen port,
/// then passed to `Runs::create` and `Run::execute_via_quire_ci`.
-/// Both variants carry an [`ApiSession`] so quire-ci always receives
-/// session info — enabling forward-compatible adoption of API routes.
+/// `Filesystem` and `Api` carry an [`ApiSession`] so quire-ci receives
+/// session info for API route use. `Local` carries no session and is
+/// used for `quire ci run` where no server is involved.
#[derive(Clone, Debug)]
pub enum Transport {
+ Local,
Filesystem(ApiSession),
Api(ApiSession),
}
impl Transport {
+ /// Build a transport for a local `quire ci run` invocation.
+ /// No session info is minted — quire-ci runs without transport flags.
+ pub fn local() -> Self {
+ Transport::Local
+ }
+
/// Build a runtime transport for a new run. Always mints a fresh
/// run ID and CSPRNG bearer token, deriving the loopback server
/// URL from `port`. The variant controls whether quire-ci uses
@@ -143,6 +151,7 @@ impl Runs {
/// agree on which run a token belongs to.
pub fn create(&self, meta: &RunMeta, transport: &Transport) -> Result<Run> {
let (id, auth_token_str) = match transport {
+ Transport::Local => (uuid::Uuid::now_v7().to_string(), None),
Transport::Filesystem(api) | Transport::Api(api) => {
(api.run_id.clone(), Some(api.auth_token.as_str()))
}
@@ -356,6 +365,14 @@ impl Run {
cmd.arg("run").arg("--workspace").arg(workspace);
match transport {
+ Transport::Local => {
+ cmd.arg("--out-dir")
+ .arg(&run_dir)
+ .arg("--events")
+ .arg(&events_path)
+ .arg("--bootstrap")
+ .arg(&bootstrap_path);
+ }
Transport::Filesystem(api) => {
cmd.arg("--run-id")
.arg(&api.run_id)
@@ -996,6 +1013,7 @@ mod tests {
),
] {
let api = match &transport {
+ Transport::Local => unreachable!("test transports carry sessions"),
Transport::Filesystem(api) | Transport::Api(api) => api,
};
assert_eq!(api.server_url, expected_url);