Pass the bare repo path through to quire-ci dispatch
Mirror jobs dispatched via :executor :quire-ci failed with "fatal:
not a git repository" because quire-ci synthesized GIT_DIR as
<workspace>/.git, but the materialized workspace is a flat `git
archive` extract with no `.git` inside. Carry the bare repo path
through `Dispatch` so the mirror job sees the same GIT_DIR the host
executor already uses.
Assisted-by: Claude Opus 4.7 (1M context) via Claude Code
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index ee768a1..2b3692b 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -167,11 +167,15 @@ fn main() -> miette::Result<()> {
(path, Some(DumpLogsOnDrop { dir }))
}
};
- let (meta, secrets) = match dispatch {
+ let (git_dir, meta, secrets) = match dispatch {
Some(path) => load_dispatch(&path)?,
- None => (placeholder_meta(), HashMap::new()),
+ None => (
+ cli.workspace.join(".git"),
+ placeholder_meta(),
+ HashMap::new(),
+ ),
};
- run_pipeline(cli.workspace, sink, log_dir, meta, secrets)
+ run_pipeline(cli.workspace, sink, log_dir, git_dir, meta, secrets)
}
}
}
@@ -215,7 +219,11 @@ fn placeholder_meta() -> RunMeta {
/// spawning. Wraps revealed secret values back into `SecretString`.
fn load_dispatch(
path: &std::path::Path,
-) -> miette::Result<(RunMeta, HashMap<String, quire_core::secret::SecretString>)> {
+) -> miette::Result<(
+ PathBuf,
+ RunMeta,
+ HashMap<String, quire_core::secret::SecretString>,
+)> {
use quire_core::ci::dispatch::Dispatch;
use quire_core::secret::SecretString;
@@ -226,13 +234,14 @@ fn load_dispatch(
.into_iter()
.map(|(name, value)| (name, SecretString::from(value)))
.collect();
- Ok((dispatch.meta, secrets))
+ Ok((dispatch.git_dir, dispatch.meta, secrets))
}
fn run_pipeline(
workspace: PathBuf,
sink: Box<dyn EventSink>,
log_dir: PathBuf,
+ git_dir: PathBuf,
meta: RunMeta,
secrets: HashMap<String, quire_core::secret::SecretString>,
) -> miette::Result<()> {
@@ -249,7 +258,6 @@ fn run_pipeline(
let sink: Rc<RefCell<Box<dyn EventSink>>> = Rc::new(RefCell::new(sink));
- let git_dir = workspace.join(".git");
let runtime = Rc::new(Runtime::new(
pipeline, secrets, &meta, &git_dir, workspace, log_dir,
));
diff --git a/quire-core/src/ci/dispatch.rs b/quire-core/src/ci/dispatch.rs
index 9c1556b..6a230d8 100644
--- a/quire-core/src/ci/dispatch.rs
+++ b/quire-core/src/ci/dispatch.rs
@@ -12,6 +12,7 @@
//! permissions (mode 0600 on Unix) before writing.
use std::collections::HashMap;
+use std::path::PathBuf;
use serde::{Deserialize, Serialize};
@@ -24,8 +25,15 @@ use crate::ci::run::RunMeta;
/// orchestrator reveals values into this map before writing the
/// file (mode 0600); quire-ci wraps them back into `SecretString`s
/// on read.
+///
+/// `git_dir` is the bare repo the run is scoped to. quire-ci surfaces
+/// it via `(jobs :quire/push).git-dir`, which the mirror job's run-fn
+/// passes to git as `GIT_DIR`. The materialized workspace is a flat
+/// `git archive` extract with no `.git` inside, so quire-ci has no
+/// way to recover this path on its own.
#[derive(Debug, Serialize, Deserialize)]
pub struct Dispatch {
pub meta: RunMeta,
+ pub git_dir: PathBuf,
pub secrets: HashMap<String, String>,
}
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index c23f17e..588d081 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -197,7 +197,7 @@ fn trigger_ref(
// The orchestrator already validated `pipeline` to fail-fast on
// bad ci.fnl; `quire-ci` recompiles inside its own process.
drop(pipeline);
- run.execute_via_quire_ci(&workspace, &meta, secrets)?;
+ run.execute_via_quire_ci(&repo.path(), &workspace, &meta, secrets)?;
}
}
Ok(())
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 2f360f4..658dfb9 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -328,6 +328,7 @@ impl Run {
/// partial progress.
pub fn execute_via_quire_ci(
mut self,
+ git_dir: &Path,
workspace: &Path,
meta: &RunMeta,
secrets: &HashMap<String, SecretString>,
@@ -343,7 +344,7 @@ impl Run {
let log = fs_err::File::create(&log_path)?.into_parts().0;
let log_clone = log.try_clone()?;
- write_dispatch(&dispatch_path, meta, secrets)?;
+ write_dispatch(&dispatch_path, git_dir, meta, secrets)?;
tracing::info!(
run_id = %self.id,
@@ -615,6 +616,7 @@ impl Run {
/// to set the mode aborts the dispatch (better than leaking).
fn write_dispatch(
path: &Path,
+ git_dir: &Path,
meta: &RunMeta,
secrets: &HashMap<String, SecretString>,
) -> Result<()> {
@@ -629,6 +631,7 @@ fn write_dispatch(
}
let dispatch = Dispatch {
meta: meta.clone(),
+ git_dir: git_dir.to_path_buf(),
secrets: revealed,
};
let json = serde_json::to_vec_pretty(&dispatch).map_err(std::io::Error::other)?;
@@ -825,6 +828,25 @@ mod tests {
);
}
+ #[test]
+ fn write_dispatch_records_git_dir_for_quire_ci() {
+ use quire_core::ci::dispatch::Dispatch;
+
+ let dir = tempfile::tempdir().expect("tempdir");
+ let dispatch_path = dir.path().join("dispatch.json");
+ let git_dir = dir.path().join("repos").join("test.git");
+
+ write_dispatch(&dispatch_path, &git_dir, &test_meta(), &HashMap::new())
+ .expect("write_dispatch");
+
+ let bytes = fs_err::read(&dispatch_path).expect("read dispatch");
+ let dispatch: Dispatch = serde_json::from_slice(&bytes).expect("parse dispatch");
+ assert_eq!(
+ dispatch.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 [