Replace bootstrap file handoff with CLI flags for local runs
`quire-ci run` now accepts `--git-dir`, `--sha`, and `--git-ref`
directly. The server passes them as CLI args when transport is None
(local `quire ci run`). No files are written or read at any point in
either path. `write_bootstrap`, `load_bootstrap`, and `--bootstrap`
are removed entirely.
https://claude.ai/code/session_01Ngf4zbFf87zJFUR5ee2Y8b
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 7a42e0e..e19de27 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -7,7 +7,7 @@ use std::rc::Rc;
use facet::Facet;
use figue::{self as args, Driver, FigueBuiltins};
-use miette::{IntoDiagnostic, Result, bail};
+use miette::{IntoDiagnostic, Result};
use quire_core::api::SecretResponse;
use quire_core::ci::bootstrap::Bootstrap;
use quire_core::ci::event::{Event, EventKind, JobOutcome, RunOutcome};
@@ -106,11 +106,20 @@ enum Commands {
#[facet(args::named, default)]
out_dir: Option<PathBuf>,
- /// Path to a JSON bootstrap file produced by the orchestrator.
- /// Required for local runs (no `QUIRE__SERVER_URL`); omitted for
- /// server-dispatched runs, which fetch bootstrap via the API.
+ /// Path to the bare git repo for this run. Required for local
+ /// runs (no `QUIRE__SERVER_URL`); server-dispatched runs
+ /// receive this via the bootstrap API instead.
#[facet(args::named, default)]
- bootstrap: Option<PathBuf>,
+ git_dir: Option<PathBuf>,
+
+ /// Commit SHA for this run. Required for local runs.
+ #[facet(args::named, default)]
+ sha: Option<String>,
+
+ /// Git ref for this run (e.g. `refs/heads/main`). Required for
+ /// local runs.
+ #[facet(args::named, default)]
+ git_ref: Option<String>,
},
}
@@ -275,7 +284,9 @@ fn main() -> Result<()> {
Commands::Run {
events,
out_dir,
- bootstrap,
+ git_dir,
+ sha,
+ git_ref,
} => {
let sink: Box<dyn EventSink> = match events.parse::<EventsTarget>().unwrap() {
EventsTarget::Null => Box::new(NullSink),
@@ -315,10 +326,17 @@ fn main() -> Result<()> {
let client = RunClient::new(session.clone());
let (git_dir, meta, sentry_trace_id) = if session.server_url.is_empty() {
- let Some(path) = bootstrap else {
- bail!("--bootstrap is required for local runs (no QUIRE__SERVER_URL set)");
+ let git_dir = git_dir
+ .ok_or_else(|| miette::miette!("--git-dir is required for local runs"))?;
+ let sha = sha.ok_or_else(|| miette::miette!("--sha is required for local runs"))?;
+ let git_ref = git_ref
+ .ok_or_else(|| miette::miette!("--git-ref is required for local runs"))?;
+ let meta = RunMeta {
+ sha,
+ r#ref: git_ref,
+ pushed_at: jiff::Timestamp::now(),
};
- load_bootstrap(&path)?
+ (git_dir, meta, None)
} else {
client.fetch_bootstrap()?
};
@@ -407,27 +425,6 @@ fn validate(workspace: PathBuf) -> Result<()> {
Ok(())
}
-/// Read and parse the bootstrap file the orchestrator wrote before
-/// spawning.
-///
-/// Unlinks the file as soon as the bytes are in memory — getting it off
-/// disk early limits the blast radius of a later panic or crash leaving
-/// a 0600 file behind.
-fn load_bootstrap(path: &std::path::Path) -> Result<(PathBuf, RunMeta, Option<String>)> {
- let bytes = fs_err::read(path).into_diagnostic()?;
- if let Err(e) = fs_err::remove_file(path) {
- // Don't abort — the bytes are already loaded and the server
- // will best-effort unlink after we exit. But this is a
- // security-relevant cleanup, so it's worth surfacing.
- eprintln!(
- "warning: failed to remove bootstrap file {}: {e}",
- path.display()
- );
- }
- let bootstrap: Bootstrap = serde_json::from_slice(&bytes).into_diagnostic()?;
- Ok((bootstrap.git_dir, bootstrap.meta, bootstrap.sentry_trace_id))
-}
-
fn run_pipeline(
workspace: PathBuf,
mut sink: Box<dyn EventSink>,
@@ -616,29 +613,4 @@ mod tests {
};
assert_eq!(path, PathBuf::from("/tmp/run.jsonl"));
}
-
- #[test]
- fn load_bootstrap_unlinks_after_read() {
- let dir = tempfile::tempdir().expect("tempdir");
- let path = dir.path().join("bootstrap.json");
- let bootstrap = Bootstrap {
- meta: RunMeta {
- sha: "0".repeat(40),
- r#ref: "HEAD".to_string(),
- pushed_at: jiff::Timestamp::now(),
- },
- git_dir: PathBuf::from("/tmp/repo.git"),
- sentry_trace_id: None,
- };
- fs_err::write(&path, serde_json::to_vec(&bootstrap).unwrap()).expect("write");
-
- let (git_dir, meta, sentry_trace_id) = load_bootstrap(&path).expect("load");
- assert!(
- !path.exists(),
- "bootstrap file should be removed after read"
- );
- assert_eq!(git_dir, PathBuf::from("/tmp/repo.git"));
- assert_eq!(meta.r#ref, "HEAD");
- assert!(sentry_trace_id.is_none());
- }
}
diff --git a/quire-core/src/ci/bootstrap.rs b/quire-core/src/ci/bootstrap.rs
index 8d23325..ed6b049 100644
--- a/quire-core/src/ci/bootstrap.rs
+++ b/quire-core/src/ci/bootstrap.rs
@@ -1,11 +1,9 @@
-//! Wire format for handing off a run from the orchestrator to
-//! `quire-ci`.
+//! Wire format for the bootstrap API response.
//!
-//! For server-dispatched runs the orchestrator stores a [`Bootstrap`]
-//! in the database; `quire-ci` fetches it via `GET /api/run/bootstrap`
-//! using the per-run bearer token. For local dev runs without a server
-//! the orchestrator writes the JSON to a file and passes the path via
-//! `--bootstrap`.
+//! The orchestrator stores a [`Bootstrap`] in the database; `quire-ci`
+//! fetches it via `GET /api/run/bootstrap` using the per-run bearer
+//! token. Local runs receive bootstrap data directly via CLI flags
+//! (`--git-dir`, `--sha`, `--git-ref`) instead.
use std::path::PathBuf;
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index f80b58f..d6dfe69 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -318,7 +318,6 @@ impl Run {
let run_dir = self.path();
let log_path = run_dir.join("quire-ci.log");
let events_path = run_dir.join("events.jsonl");
- let bootstrap_path = run_dir.join("bootstrap.json");
// fs_err for the path-bearing IO error; unwrap to std::fs::File so
// it's convertible into Stdio.
let log = fs_err::File::create(&log_path)?.into_parts().0;
@@ -342,8 +341,12 @@ impl Run {
match transport {
None => {
- write_bootstrap(&bootstrap_path, git_dir, meta, sentry_trace_id)?;
- cmd.arg("--bootstrap").arg(&bootstrap_path);
+ cmd.arg("--git-dir")
+ .arg(git_dir)
+ .arg("--sha")
+ .arg(&meta.sha)
+ .arg("--git-ref")
+ .arg(&meta.r#ref);
}
Some(t) => {
self.store_bootstrap_data(git_dir, sentry_trace_id)?;
@@ -365,21 +368,6 @@ impl Run {
source,
})?;
- // For local runs (None transport), quire-ci unlinks the bootstrap
- // file after reading it. This is a best-effort safety net for
- // paths where it didn't get that far. `NotFound` is expected.
- if transport.is_none()
- && let Err(e) = fs_err::remove_file(&bootstrap_path)
- && e.kind() != std::io::ErrorKind::NotFound
- {
- tracing::warn!(
- run_id = %self.id,
- path = %bootstrap_path.display(),
- error = %e,
- "failed to remove bootstrap file after run"
- );
- }
-
// Ingest events before checking outcome — partial results from a
// crashed run are still useful in the UI. A parse failure is
// logged but doesn't mask the run outcome.
@@ -641,37 +629,6 @@ impl Run {
}
}
-/// Serialize the bootstrap payload as JSON and write it to `path` with
-/// owner-only permissions on Unix.
-fn write_bootstrap(
- path: &Path,
- git_dir: &Path,
- meta: &RunMeta,
- sentry_trace_id: Option<&str>,
-) -> Result<()> {
- use quire_core::ci::bootstrap::Bootstrap;
-
- let bootstrap = Bootstrap {
- meta: meta.clone(),
- git_dir: git_dir.to_path_buf(),
- sentry_trace_id: sentry_trace_id.map(Into::into),
- };
- let json = serde_json::to_vec_pretty(&bootstrap).map_err(std::io::Error::other)?;
-
- // Open with mode 0600 from the start so there's no window where
- // the file is world-readable.
- use fs_err::os::unix::fs::OpenOptionsExt;
- use std::io::Write;
- let mut file = fs_err::OpenOptions::new()
- .write(true)
- .create(true)
- .truncate(true)
- .mode(0o600)
- .open(path)?;
- file.write_all(&json)?;
- Ok(())
-}
-
/// Take the final path component of a runs base (`runs/<repo>/`) and
/// sanitize it for use as the tag segment in `quire-ci/<segment>:<id>`.
/// Materialize a working tree at `sha` into `workspace` via
@@ -859,24 +816,6 @@ mod tests {
);
}
- #[test]
- fn write_bootstrap_records_git_dir_for_quire_ci() {
- use quire_core::ci::bootstrap::Bootstrap;
-
- let dir = tempfile::tempdir().expect("tempdir");
- let bootstrap_path = dir.path().join("bootstrap.json");
- let git_dir = dir.path().join("repos").join("test.git");
-
- write_bootstrap(&bootstrap_path, &git_dir, &test_meta(), None).expect("write_bootstrap");
-
- let bytes = fs_err::read(&bootstrap_path).expect("read bootstrap");
- let bootstrap: Bootstrap = serde_json::from_slice(&bytes).expect("parse bootstrap");
- assert_eq!(
- bootstrap.git_dir, git_dir,
- "quire-ci needs the bare repo path to set GIT_DIR for the mirror job"
- );
- }
-
#[test]
fn run_state_round_trips() {
for state in [