Address review: move RepoConfig to quire-server, trigger returns Result
- Remove quire-core::ci::repo_config; define RepoConfig / RepoGithubConfig
  in quire-server::quire alongside the other config structs
- mirror::trigger now returns crate::Result<()>; use ? for global config
  and token reveal; call site in server.rs logs a single tracing::error

https://claude.ai/code/session_01MtUMXi7Z3GCDWQFY8puWpu
change
commit effd78c1357f06e7509571cb3e58623f512d9c3f
author Claude <noreply@anthropic.com>
date
parent ceea8347
diff --git a/quire-core/src/ci/mod.rs b/quire-core/src/ci/mod.rs
index eaeb2ee..6f417fc 100644
--- a/quire-core/src/ci/mod.rs
+++ b/quire-core/src/ci/mod.rs
@@ -10,6 +10,5 @@ pub mod event;
 pub mod logs;
 pub mod pipeline;
 pub mod registration;
-pub mod repo_config;
 pub mod run;
 pub mod runtime;
diff --git a/quire-core/src/ci/repo_config.rs b/quire-core/src/ci/repo_config.rs
deleted file mode 100644
index bba9f78..0000000
--- a/quire-core/src/ci/repo_config.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-//! Per-repo CI configuration parsed from `.quire/config.fnl`.
-//!
-//! Loaded at run time (inside `quire-ci`) from the materialized
-//! workspace. Missing file → all defaults.
-
-/// Per-repo CI configuration.
-#[derive(serde::Deserialize, Debug, Default, Clone)]
-#[serde(default, rename_all = "kebab-case")]
-pub struct RepoConfig {
-    pub github: GithubRepoConfig,
-}
-
-/// Per-repo GitHub configuration.
-#[derive(serde::Deserialize, Debug, Default, Clone)]
-#[serde(default, rename_all = "kebab-case")]
-pub struct GithubRepoConfig {
-    /// Remote URL to mirror every pushed ref to.
-    /// E.g. `"https://github.com/user/repo.git"`.
-    pub mirror: Option<String>,
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    fn load(source: &str) -> RepoConfig {
-        crate::fennel::Fennel::new()
-            .expect("Fennel::new")
-            .load_string(source, "config.fnl")
-            .expect("load_string")
-    }
-
-    #[test]
-    fn defaults_when_empty_table() {
-        let cfg = load("{}");
-        assert!(cfg.github.mirror.is_none());
-    }
-
-    #[test]
-    fn parses_mirror_url() {
-        let cfg = load(r#"{:github {:mirror "https://github.com/user/repo.git"}}"#);
-        assert_eq!(
-            cfg.github.mirror.as_deref(),
-            Some("https://github.com/user/repo.git")
-        );
-    }
-}
diff --git a/quire-server/src/bin/quire/server.rs b/quire-server/src/bin/quire/server.rs
index d0fd40d..6db216e 100644
--- a/quire-server/src/bin/quire/server.rs
+++ b/quire-server/src/bin/quire/server.rs
@@ -157,5 +157,11 @@ async fn handle_event_connection(mut stream: tokio::net::UnixStream, quire: Quir
     }
 
     ci::trigger(&quire, &event);
-    mirror::trigger(&quire, &event);
+    if let Err(e) = mirror::trigger(&quire, &event) {
+        tracing::error!(
+            repo = %event.repo,
+            error = &e as &(dyn std::error::Error + 'static),
+            "mirror failed",
+        );
+    }
 }
diff --git a/quire-server/src/mirror.rs b/quire-server/src/mirror.rs
index b19943e..6ead68f 100644
--- a/quire-server/src/mirror.rs
+++ b/quire-server/src/mirror.rs
@@ -11,44 +11,24 @@ use crate::quire::Quire;
 /// Reads `github.mirror-token` from global config for auth. For each updated
 /// ref, reads `.quire/config.fnl` at the new SHA to obtain the `github.mirror`
 /// URL. Skips repos with no mirror URL configured.
-pub fn trigger(quire: &Quire, event: &PushEvent) {
+pub fn trigger(quire: &Quire, event: &PushEvent) -> crate::Result<()> {
     let repo = match quire.repo(&event.repo) {
         Ok(r) if r.exists() => r,
         Ok(_) => {
             tracing::warn!(repo = %event.repo, "mirror: repo not found on disk");
-            return;
+            return Ok(());
         }
         Err(e) => {
-            tracing::error!(repo = %event.repo, error = %e, "mirror: invalid repo name");
-            return;
+            return Err(crate::Error::Io(std::io::Error::other(e.to_string())));
         }
     };
 
-    let config = match quire.global_config() {
-        Ok(c) => c,
-        Err(e) => {
-            tracing::error!(
-                repo = %event.repo,
-                error = &e as &(dyn std::error::Error + 'static),
-                "mirror: failed to load global config",
-            );
-            return;
-        }
-    };
-
-    let mirror_token = match config.github.mirror_token {
-        None => None,
-        Some(ref secret) => match secret.reveal() {
-            Ok(t) => Some(t.to_string()),
-            Err(e) => {
-                tracing::error!(
-                    error = &e as &(dyn std::error::Error + 'static),
-                    "mirror: failed to reveal mirror token",
-                );
-                return;
-            }
-        },
-    };
+    let config = quire.global_config()?;
+    let mirror_token = config
+        .github
+        .mirror_token
+        .map(|s| s.reveal().map(str::to_owned))
+        .transpose()?;
 
     for push_ref in event.updated_refs() {
         let repo_config = match repo.repo_config(&push_ref.new_sha) {
@@ -75,6 +55,8 @@ pub fn trigger(quire: &Quire, event: &PushEvent) {
             mirror_token.as_deref(),
         );
     }
+
+    Ok(())
 }
 
 fn push_to_mirror(
diff --git a/quire-server/src/quire/mod.rs b/quire-server/src/quire/mod.rs
index 7bc5384..a9a11ef 100644
--- a/quire-server/src/quire/mod.rs
+++ b/quire-server/src/quire/mod.rs
@@ -51,6 +51,22 @@ fn default_port() -> u16 {
     3000
 }
 
+/// Per-repo CI configuration parsed from `.quire/config.fnl`.
+#[derive(serde::Deserialize, Debug, Default, Clone)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct RepoConfig {
+    pub github: RepoGithubConfig,
+}
+
+/// Per-repo GitHub configuration.
+#[derive(serde::Deserialize, Debug, Default, Clone)]
+#[serde(default, rename_all = "kebab-case")]
+pub struct RepoGithubConfig {
+    /// Remote URL to mirror every pushed ref to.
+    /// E.g. `"https://github.com/user/repo.git"`.
+    pub mirror: Option<String>,
+}
+
 #[derive(serde::Deserialize, Debug, Default)]
 pub struct CiConfig {
     /// How the orchestrator dispatches CI runs. Defaults to shelling
@@ -159,7 +175,7 @@ impl Repo {
     /// Read and parse `.quire/config.fnl` at the given commit SHA.
     ///
     /// Returns defaults if the file does not exist at that commit.
-    pub fn repo_config(&self, sha: &str) -> AppResult<quire_core::ci::repo_config::RepoConfig> {
+    pub fn repo_config(&self, sha: &str) -> AppResult<RepoConfig> {
         let output = self
             .git(&["show", &format!("{sha}:.quire/config.fnl")])
             .stdout(std::process::Stdio::piped())
@@ -169,7 +185,7 @@ impl Repo {
         if !output.status.success() {
             let stderr = String::from_utf8_lossy(&output.stderr);
             if stderr.contains("does not exist") || stderr.contains("not found") {
-                return Ok(quire_core::ci::repo_config::RepoConfig::default());
+                return Ok(RepoConfig::default());
             }
             return Err(Error::Io(std::io::Error::other(format!(
                 "failed to read .quire/config.fnl at {sha}: {stderr}"