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
change luvwoonrvxmmysouwztqksqlvprmoumx
commit 6295475f93b94ab5fe01686e5bbb0d34920fbc80
author Alpha Chen <alpha@kejadlen.dev>
date
parent ywkrxqqs
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 [