Add Fennel::load_config_str; typed MirrorError; real errors in mirror
quire-core:
- Add Fennel::load_config_str(source, name) — like load_config but reads
from a string, useful for content piped from git show
quire-server/quire/mod.rs:
- repo_config uses load_config_str, keeping the ".quire/config.fnl" label
in error messages (no temp file needed)
quire-server/mirror.rs:
- Replace miette::miette! and io::Error::other string wrappers with a
typed MirrorError enum (RepoNotFound, TokenNotConfigured, PushFailed,
App, Io); mirror_ref and push_to_mirror return Result<(), MirrorError>
https://claude.ai/code/session_01MtUMXi7Z3GCDWQFY8puWpu
diff --git a/quire-core/src/fennel.rs b/quire-core/src/fennel.rs
index 58ab385..564ff99 100644
--- a/quire-core/src/fennel.rs
+++ b/quire-core/src/fennel.rs
@@ -189,6 +189,17 @@ impl Fennel {
{
Self::new()?.load_file(path)
}
+
+ /// Create a fresh Fennel VM and parse `source` into `T`.
+ ///
+ /// Like [`load_config`] but reads from a string rather than a file.
+ /// `name` is used in error messages (e.g. `".quire/config.fnl"`).
+ pub fn load_config_str<T>(source: &str, name: &str) -> Result<T, FennelError>
+ where
+ T: serde::de::DeserializeOwned,
+ {
+ Self::new()?.load_string(source, name)
+ }
}
impl FennelError {
diff --git a/quire-server/src/mirror.rs b/quire-server/src/mirror.rs
index 983bb3f..689782a 100644
--- a/quire-server/src/mirror.rs
+++ b/quire-server/src/mirror.rs
@@ -16,6 +16,24 @@ struct MirrorErrors {
errors: Vec<miette::Report>,
}
+#[derive(Debug, Error, Diagnostic)]
+enum MirrorError {
+ #[error("repo not found on disk: {0}")]
+ RepoNotFound(String),
+
+ #[error("mirror-token not configured")]
+ TokenNotConfigured,
+
+ #[error("git push to {url} failed: {stderr}")]
+ PushFailed { url: String, stderr: String },
+
+ #[error(transparent)]
+ App(#[from] crate::Error),
+
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+}
+
/// Mirror updated refs to a configured remote.
///
/// Reads `github.mirror-token` from global config for auth. For each updated
@@ -25,7 +43,7 @@ pub fn trigger(quire: &Quire, event: &PushEvent) -> miette::Result<()> {
let repo = match quire.repo(&event.repo) {
Ok(r) if r.exists() => r,
Ok(_) => {
- return Err(miette::miette!("repo not found on disk: {}", event.repo));
+ return Err(MirrorError::RepoNotFound(event.repo.clone()).into());
}
Err(e) => return Err(e),
};
@@ -42,7 +60,7 @@ pub fn trigger(quire: &Quire, event: &PushEvent) -> miette::Result<()> {
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));
+ errors.push(miette::Report::from(e));
}
}
@@ -58,13 +76,12 @@ fn mirror_ref(
repo: &crate::quire::Repo,
push_ref: &PushRef,
token: Option<&str>,
-) -> crate::Result<()> {
+) -> Result<(), MirrorError> {
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")))?;
+ let token = token.ok_or(MirrorError::TokenNotConfigured)?;
push_to_mirror(repo, &push_ref.ref_name, &mirror_url, token)
}
@@ -73,7 +90,7 @@ fn push_to_mirror(
ref_name: &str,
mirror_url: &str,
token: &str,
-) -> crate::Result<()> {
+) -> Result<(), MirrorError> {
// Force-push the ref to the mirror. The `+` prefix allows rewrites.
let refspec = format!("+{ref_name}:{ref_name}");
@@ -91,10 +108,10 @@ fn push_to_mirror(
.output()?;
if !out.status.success() {
- let stderr = String::from_utf8_lossy(&out.stderr);
- return Err(crate::Error::Io(std::io::Error::other(format!(
- "git push to {mirror_url} failed: {stderr}"
- ))));
+ return Err(MirrorError::PushFailed {
+ url: mirror_url.to_string(),
+ stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
+ });
}
tracing::info!(ref_name, mirror_url, "mirror: push succeeded");
diff --git a/quire-server/src/quire/mod.rs b/quire-server/src/quire/mod.rs
index 608a824..589b26e 100644
--- a/quire-server/src/quire/mod.rs
+++ b/quire-server/src/quire/mod.rs
@@ -163,26 +163,16 @@ impl Repo {
let path = format!("{sha}:.quire/config.fnl");
// cat-file -e: exit 0 if the object exists, non-zero if absent.
- let exists = self.git(&["cat-file", "-e", &path]).status()?.success();
- if !exists {
+ if !self.git(&["cat-file", "-e", &path]).status()?.success() {
return Ok(RepoConfig::default());
}
- let output = self
+ let out = self
.git(&["show", &path])
.stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::piped())
.output()?;
-
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- return Err(Error::Io(std::io::Error::other(format!(
- "failed to read .quire/config.fnl at {sha}: {stderr}"
- ))));
- }
-
- let source = String::from_utf8(output.stdout)?;
- Ok(Fennel::new()?.load_string(&source, ".quire/config.fnl")?)
+ let source = String::from_utf8(out.stdout)?;
+ Ok(Fennel::load_config_str(&source, ".quire/config.fnl")?)
}
/// The base directory for CI runs (`runs/<repo>/`).