Use miette error aggregation in mirror::trigger
- Restore RepoConfig/RepoGithubConfig as named public structs
- Repo not found is now an Err (not a silent warn+Ok)
- Per-ref loop collects errors into a MirrorErrors aggregate (#[related])
instead of tracing::warn/error per ref; the call site logs a single %e
https://claude.ai/code/session_01MtUMXi7Z3GCDWQFY8puWpu
diff --git a/quire-server/src/bin/quire/server.rs b/quire-server/src/bin/quire/server.rs
index 6db216e..b39b434 100644
--- a/quire-server/src/bin/quire/server.rs
+++ b/quire-server/src/bin/quire/server.rs
@@ -158,10 +158,6 @@ async fn handle_event_connection(mut stream: tokio::net::UnixStream, quire: Quir
ci::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",
- );
+ tracing::error!(repo = %event.repo, error = %e, "mirror failed");
}
}
diff --git a/quire-server/src/mirror.rs b/quire-server/src/mirror.rs
index 35302d7..983bb3f 100644
--- a/quire-server/src/mirror.rs
+++ b/quire-server/src/mirror.rs
@@ -2,68 +2,70 @@
//!
//! Triggered from the push event handler, independent of CI.
-use quire_core::event::PushEvent;
+use miette::{Diagnostic, IntoDiagnostic as _};
+use quire_core::event::{PushEvent, PushRef};
+use thiserror::Error;
use crate::quire::Quire;
+#[derive(Debug, Error, Diagnostic)]
+#[error("mirror: {count} ref(s) failed")]
+struct MirrorErrors {
+ count: usize,
+ #[related]
+ errors: Vec<miette::Report>,
+}
+
/// Mirror updated refs to a configured remote.
///
/// 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) -> crate::Result<()> {
+pub fn trigger(quire: &Quire, event: &PushEvent) -> miette::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 Ok(());
- }
- Err(e) => {
- return Err(crate::Error::Io(std::io::Error::other(e.to_string())));
+ return Err(miette::miette!("repo not found on disk: {}", event.repo));
}
+ Err(e) => return Err(e),
};
- let config = quire.global_config()?;
+ let config = quire.global_config().into_diagnostic()?;
let mirror_token = config
.github
.mirror_token
.map(|s| s.reveal().map(str::to_owned))
- .transpose()?;
+ .transpose()
+ .into_diagnostic()?;
- for push_ref in event.updated_refs() {
- let mirror_url = match repo.mirror_url(&push_ref.new_sha) {
- Ok(Some(url)) => url,
- Ok(None) => continue,
- Err(e) => {
- tracing::warn!(
- ref_name = %push_ref.ref_name,
- sha = %push_ref.new_sha,
- error = &e as &(dyn std::error::Error + 'static),
- "mirror: failed to read repo config, skipping ref",
- );
- continue;
- }
- };
+ let mut errors: Vec<miette::Report> = vec![];
- let Some(token) = mirror_token.as_deref() else {
- tracing::warn!(
- ref_name = %push_ref.ref_name,
- "mirror: mirror-token not configured, skipping ref",
- );
- continue;
- };
-
- if let Err(e) = push_to_mirror(&repo, &push_ref.ref_name, &mirror_url, token) {
- tracing::error!(
- ref_name = %push_ref.ref_name,
- mirror_url,
- error = &e as &(dyn std::error::Error + 'static),
- "mirror: push failed",
- );
+ for push_ref in event.updated_refs() {
+ if let Err(e) = mirror_ref(&repo, push_ref, mirror_token.as_deref()) {
+ errors.push(miette::miette!("{}: {e}", push_ref.ref_name));
}
}
- Ok(())
+ if errors.is_empty() {
+ Ok(())
+ } else {
+ let count = errors.len();
+ Err(MirrorErrors { count, errors }.into())
+ }
+}
+
+fn mirror_ref(
+ repo: &crate::quire::Repo,
+ push_ref: &PushRef,
+ token: Option<&str>,
+) -> crate::Result<()> {
+ let repo_config = repo.repo_config(&push_ref.new_sha)?;
+ let Some(mirror_url) = repo_config.github.mirror else {
+ return Ok(());
+ };
+ let token = token
+ .ok_or_else(|| crate::Error::Io(std::io::Error::other("mirror-token not configured")))?;
+ push_to_mirror(repo, &push_ref.ref_name, &mirror_url, token)
}
fn push_to_mirror(
diff --git a/quire-server/src/quire/mod.rs b/quire-server/src/quire/mod.rs
index 8aaba98..0907f56 100644
--- a/quire-server/src/quire/mod.rs
+++ b/quire-server/src/quire/mod.rs
@@ -156,21 +156,10 @@ impl Repo {
Ci::new(self.path())
}
- /// Read `github.mirror` URL from `.quire/config.fnl` at the given commit SHA.
+ /// Read and parse `.quire/config.fnl` at the given commit SHA.
///
- /// Returns `None` if unconfigured or the file does not exist at that commit.
- pub fn mirror_url(&self, sha: &str) -> AppResult<Option<String>> {
- #[derive(serde::Deserialize, Default)]
- #[serde(default, rename_all = "kebab-case")]
- struct Config {
- github: Github,
- }
- #[derive(serde::Deserialize, Default)]
- #[serde(default, rename_all = "kebab-case")]
- struct Github {
- mirror: Option<String>,
- }
-
+ /// Returns defaults if the file does not exist at that commit.
+ pub fn repo_config(&self, sha: &str) -> AppResult<RepoConfig> {
let output = self
.git(&["show", &format!("{sha}:.quire/config.fnl")])
.stdout(std::process::Stdio::piped())
@@ -180,7 +169,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(None);
+ return Ok(RepoConfig::default());
}
return Err(Error::Io(std::io::Error::other(format!(
"failed to read .quire/config.fnl at {sha}: {stderr}"
@@ -188,8 +177,7 @@ impl Repo {
}
let source = String::from_utf8(output.stdout)?;
- let cfg: Config = Fennel::new()?.load_string(&source, ".quire/config.fnl")?;
- Ok(cfg.github.mirror)
+ Ok(Fennel::new()?.load_string(&source, ".quire/config.fnl")?)
}
/// The base directory for CI runs (`runs/<repo>/`).
@@ -207,6 +195,22 @@ impl Repo {
}
}
+/// 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>,
+}
+
/// Application runtime context.
///
/// Carries configuration and provides resolved paths to repositories.