Derive git metadata from --git-dir for local runs; add --local flag
Replace --sha and --git-ref CLI args with --local flag in quire-ci.
When --local is passed, quire-ci runs git rev-parse HEAD and
git symbolic-ref HEAD to derive commit SHA and ref from the git dir,
falling back to "@" for detached HEAD.
Remove meta from execute() signature since local runs no longer need
it forwarded; the server still creates the run with meta for its own
DB record.
Add bail!/ensure! preference note to AGENTS.md.
https://claude.ai/code/session_01Ngf4zbFf87zJFUR5ee2Y8b
diff --git a/AGENTS.md b/AGENTS.md
index ebbb145..bc837d9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,6 +10,13 @@ When the user asks to add a task, run `ranger task create` (plus `ranger tag add
The backlog shifts between sessions and even within a session — tasks get reordered, retitled, moved between states, closed, or added by the user without notice. Before acting on a task you remember (or one referenced earlier in the conversation), re-run `ranger task show <key>` and confirm state, ordering, and description against ground truth. Do not trust earlier `ranger task list` output to still be accurate; refetch when placement matters (e.g. moving to top/back of ready).
+## Error handling
+
+Prefer `bail!` and `ensure!` from miette over constructing errors manually with
+`miette::miette!()` and `.ok_or_else()`. Use `bail!` for early returns and
+`ensure!` for precondition checks. `IntoDiagnostic` (via `.into_diagnostic()`)
+is the right bridge for non-miette error types.
+
## Before committing
Always run `just all` and verify everything passes before committing. No exceptions — this is not optional. If you commit without running it, you will break the build.
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index e19de27..d213310 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -106,20 +106,17 @@ enum Commands {
#[facet(args::named, default)]
out_dir: Option<PathBuf>,
- /// 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.
+ /// Run in local mode. Derives commit SHA and ref from `--git-dir`
+ /// instead of fetching bootstrap data from the server. Pass
+ /// `QUIRE__SERVER_URL` to use the server API instead.
#[facet(args::named, default)]
- git_dir: Option<PathBuf>,
-
- /// Commit SHA for this run. Required for local runs.
- #[facet(args::named, default)]
- sha: Option<String>,
+ local: bool,
- /// Git ref for this run (e.g. `refs/heads/main`). Required for
- /// local runs.
+ /// Path to the bare git repo for this run. Required when
+ /// `--local` is set; server-dispatched runs receive this via the
+ /// bootstrap API instead.
#[facet(args::named, default)]
- git_ref: Option<String>,
+ git_dir: Option<PathBuf>,
},
}
@@ -284,9 +281,8 @@ fn main() -> Result<()> {
Commands::Run {
events,
out_dir,
+ local,
git_dir,
- sha,
- git_ref,
} => {
let sink: Box<dyn EventSink> = match events.parse::<EventsTarget>().unwrap() {
EventsTarget::Null => Box::new(NullSink),
@@ -325,12 +321,11 @@ fn main() -> Result<()> {
};
let client = RunClient::new(session.clone());
- let (git_dir, meta, sentry_trace_id) = if session.server_url.is_empty() {
+ let (git_dir, meta, sentry_trace_id) = if local {
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 sha = git_rev_parse(&git_dir, "HEAD")?;
+ let git_ref = git_symbolic_ref(&git_dir).unwrap_or_else(|_| "@".to_string());
let meta = RunMeta {
sha,
r#ref: git_ref,
@@ -403,6 +398,35 @@ fn init_sentry(dsn: &str, trace_id: Option<&str>, meta: &RunMeta) -> sentry::Cli
guard
}
+fn git_rev_parse(git_dir: &std::path::Path, rev: &str) -> Result<String> {
+ let out = std::process::Command::new("git")
+ .arg("--git-dir")
+ .arg(git_dir)
+ .arg("rev-parse")
+ .arg(rev)
+ .output()
+ .into_diagnostic()?;
+ if !out.status.success() {
+ let stderr = String::from_utf8_lossy(&out.stderr);
+ miette::bail!("git rev-parse {rev} failed: {stderr}");
+ }
+ Ok(String::from_utf8(out.stdout).into_diagnostic()?.trim().to_string())
+}
+
+fn git_symbolic_ref(git_dir: &std::path::Path) -> Result<String> {
+ let out = std::process::Command::new("git")
+ .arg("--git-dir")
+ .arg(git_dir)
+ .arg("symbolic-ref")
+ .arg("HEAD")
+ .output()
+ .into_diagnostic()?;
+ if !out.status.success() {
+ miette::bail!("HEAD is detached");
+ }
+ Ok(String::from_utf8(out.stdout).into_diagnostic()?.trim().to_string())
+}
+
fn validate(workspace: PathBuf) -> Result<()> {
let pipeline = compile_at(&workspace)?;
diff --git a/quire-core/src/ci/bootstrap.rs b/quire-core/src/ci/bootstrap.rs
index ed6b049..d581e0f 100644
--- a/quire-core/src/ci/bootstrap.rs
+++ b/quire-core/src/ci/bootstrap.rs
@@ -2,8 +2,8 @@
//!
//! 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.
+//! token. Local runs pass `--local --git-dir <path>` and derive the
+//! commit SHA and ref directly from the git dir.
use std::path::PathBuf;
diff --git a/quire-server/src/bin/quire/commands/ci.rs b/quire-server/src/bin/quire/commands/ci.rs
index cea5410..4455273 100644
--- a/quire-server/src/bin/quire/commands/ci.rs
+++ b/quire-server/src/bin/quire/commands/ci.rs
@@ -85,7 +85,7 @@ pub async fn run(quire: &Quire, maybe_sha: Option<&str>) -> Result<()> {
let workspace = tmp.path().join("workspace");
quire::ci::materialize_workspace(&repo_path.join(".git"), &commit.sha, &workspace)
.into_diagnostic()?;
- let exec_result = run.execute(&repo_path.join(".git"), &workspace, &meta, None, None, None);
+ let exec_result = run.execute(&repo_path.join(".git"), &workspace, None, None, None);
// Print the combined quire-ci log regardless of outcome.
let log_path = tmp.path().join(&run_id).join("quire-ci.log");
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index 153df02..afc86f5 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -254,7 +254,6 @@ fn run_ref_inner(
run.execute(
&ctx.repo.path(),
&workspace,
- &meta,
sentry_trace_id,
sentry_dsn,
Some(transport),
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index d6dfe69..284efc4 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -298,7 +298,6 @@ impl Run {
mut self,
git_dir: &Path,
workspace: &Path,
- meta: &RunMeta,
sentry_trace_id: Option<&str>,
sentry_dsn: Option<&str>,
transport: Option<&Transport>,
@@ -341,12 +340,7 @@ impl Run {
match transport {
None => {
- cmd.arg("--git-dir")
- .arg(git_dir)
- .arg("--sha")
- .arg(&meta.sha)
- .arg("--git-ref")
- .arg(&meta.r#ref);
+ cmd.arg("--local").arg("--git-dir").arg(git_dir);
}
Some(t) => {
self.store_bootstrap_data(git_dir, sentry_trace_id)?;