Decouple `pipeline`/`registration` from the kitchen-sink `ci::Error`
`pipeline::compile` and `registration::register` now thread a small
sum-of-failures `CompileError` (Fennel evaluation or PipelineError)
through their public signatures instead of reaching into the
orchestrator-flavored `ci::Error`. A flattening `From<CompileError>
for Error` at the boundary keeps existing callers working.
Sets up the next move: `pipeline` and `registration` no longer
reference `super::error`, so they can land in `quire-core` without
dragging rusqlite, docker, and the rest of the kitchen-sink along.
The runtime-side `RustRunFn` callback still uses the kitchen-sink
result type — runtime errors are still a quire-server concern.
Assisted-by: Claude Opus 4.7 (1M context) via Claude Code
diff --git a/quire-server/src/ci/error.rs b/quire-server/src/ci/error.rs
index a464723..e35b4d5 100644
--- a/quire-server/src/ci/error.rs
+++ b/quire-server/src/ci/error.rs
@@ -2,7 +2,7 @@
use miette::Diagnostic;
-use super::pipeline::PipelineError;
+use super::pipeline::{CompileError, PipelineError};
use super::run::RunState;
use quire_core::fennel::FennelError;
use quire_core::secret;
@@ -90,6 +90,15 @@ impl From<PipelineError> for Error {
}
}
+impl From<CompileError> for Error {
+ fn from(err: CompileError) -> Self {
+ match err {
+ CompileError::Fennel(e) => Error::Fennel(e),
+ CompileError::Pipeline(e) => Error::Pipeline(e),
+ }
+ }
+}
+
impl From<FennelError> for Error {
fn from(err: FennelError) -> Self {
Error::Fennel(Box::new(err))
diff --git a/quire-server/src/ci/mirror.rs b/quire-server/src/ci/mirror.rs
index 9d7f394..3fedcc4 100644
--- a/quire-server/src/ci/mirror.rs
+++ b/quire-server/src/ci/mirror.rs
@@ -337,7 +337,7 @@ mod tests {
let Err(err) = compile(source, "ci.fnl") else {
panic!("expected error");
};
- let crate::ci::error::Error::Pipeline(pe) = err else {
+ let crate::ci::pipeline::CompileError::Pipeline(pe) = err else {
panic!("expected PipelineError, got {err:?}");
};
assert!(
@@ -356,7 +356,7 @@ mod tests {
let Err(err) = compile(source, "ci.fnl") else {
panic!("expected error");
};
- let crate::ci::error::Error::Pipeline(pe) = err else {
+ let crate::ci::pipeline::CompileError::Pipeline(pe) = err else {
panic!("expected PipelineError, got {err:?}");
};
pe.diagnostics
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index f323bf5..1f7a996 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -72,7 +72,7 @@ impl Ci {
/// Single chokepoint for compile + structural validation, used by
/// [`Ci::pipeline`] and `trigger_ref` so the two paths can't drift.
fn compile(&self, source: &str) -> error::Result<Pipeline> {
- pipeline::compile(source, CI_FNL)
+ Ok(pipeline::compile(source, CI_FNL)?)
}
/// Read the contents of `.quire/ci.fnl` at a given commit SHA.
diff --git a/quire-server/src/ci/pipeline.rs b/quire-server/src/ci/pipeline.rs
index fb5977e..564c553 100644
--- a/quire-server/src/ci/pipeline.rs
+++ b/quire-server/src/ci/pipeline.rs
@@ -11,9 +11,8 @@ use petgraph::Graph;
use petgraph::graph::NodeIndex;
use petgraph::visit::{Bfs, Reversed};
-use super::error::Result;
use super::registration::{self, Registrations};
-use quire_core::fennel::Fennel;
+use quire_core::fennel::{Fennel, FennelError};
/// A registration-time error caught while individual `(ci.job …)` and
/// `(ci.image …)` calls are being processed.
@@ -112,7 +111,8 @@ pub struct Job {
/// A Rust-side run-fn: a closure invoked synchronously by the
/// executor with the runtime in scope.
-pub(super) type RustRunFn = std::rc::Rc<dyn Fn(&super::runtime::Runtime) -> Result<()>>;
+pub(super) type RustRunFn =
+ std::rc::Rc<dyn Fn(&super::runtime::Runtime) -> super::error::Result<()>>;
/// How a job runs at execute time.
///
@@ -336,6 +336,35 @@ pub struct PipelineError {
pub(crate) diagnostics: Vec<Diagnostic>,
}
+/// Errors from [`compile`] — Fennel evaluation failures and pipeline-shape
+/// failures unified at the compile boundary, so callers can match on
+/// the compile result without reaching into the kitchen-sink
+/// `ci::Error`.
+#[derive(Debug, thiserror::Error, miette::Diagnostic)]
+pub enum CompileError {
+ #[error(transparent)]
+ #[diagnostic(transparent)]
+ Fennel(#[from] Box<FennelError>),
+
+ #[error(transparent)]
+ #[diagnostic(transparent)]
+ Pipeline(#[from] Box<PipelineError>),
+}
+
+impl From<FennelError> for CompileError {
+ fn from(err: FennelError) -> Self {
+ Self::Fennel(Box::new(err))
+ }
+}
+
+impl From<PipelineError> for CompileError {
+ fn from(err: PipelineError) -> Self {
+ Self::Pipeline(Box::new(err))
+ }
+}
+
+pub type CompileResult<T> = std::result::Result<T, CompileError>;
+
/// Compile a ci.fnl source string into a validated [`Pipeline`].
///
/// Two phases, fail-fast between them: [`registration::register`]
@@ -343,7 +372,7 @@ pub struct PipelineError {
/// [`validate_post_graph`] checks the dependency graph. Errors from a
/// phase are wrapped in a [`PipelineError`] for miette to render with
/// inline labels.
-pub(crate) fn compile(source: &str, name: &str) -> Result<Pipeline> {
+pub(crate) fn compile(source: &str, name: &str) -> CompileResult<Pipeline> {
let fennel = Fennel::new()?;
let Registrations { jobs, image } = registration::register(&fennel, source, name)?;
@@ -515,7 +544,7 @@ mod tests {
let f = Fennel::new().expect("Fennel::new() should succeed");
let err =
registration::register(&f, source, "ci.fnl").expect_err("expected registration errors");
- let crate::ci::error::Error::Pipeline(pe) = err else {
+ let CompileError::Pipeline(pe) = err else {
panic!("expected PipelineError, got {err:?}")
};
pe.diagnostics
@@ -808,7 +837,7 @@ mod tests {
(ci.job :orphan [:does-not-exist] (fn [_] nil))"#,
"ci.fnl",
);
- let Err(crate::ci::error::Error::Pipeline(pe)) = result else {
+ let Err(CompileError::Pipeline(pe)) = result else {
panic!("expected PipelineError")
};
for d in &pe.diagnostics {
diff --git a/quire-server/src/ci/registration.rs b/quire-server/src/ci/registration.rs
index 4a25939..2e06228 100644
--- a/quire-server/src/ci/registration.rs
+++ b/quire-server/src/ci/registration.rs
@@ -14,9 +14,10 @@ use mlua::{IntoLua, Lua};
use miette::NamedSource;
-use super::error::Result;
use super::mirror;
-use super::pipeline::{self, DefinitionError, Diagnostic, Job, PipelineError, RunFn};
+use super::pipeline::{
+ self, CompileResult, DefinitionError, Diagnostic, Job, PipelineError, RunFn,
+};
use quire_core::fennel::Fennel;
/// Output of [`register`]: jobs and image successfully registered
@@ -35,7 +36,7 @@ pub(super) struct Registrations {
/// not abort the rest of the script — but if any rule fired, the
/// whole batch is returned as a `PipelineError` instead of partial
/// registrations.
-pub(super) fn register(fennel: &Fennel, source: &str, name: &str) -> Result<Registrations> {
+pub(super) fn register(fennel: &Fennel, source: &str, name: &str) -> CompileResult<Registrations> {
let jobs: Rc<RefCell<Vec<Job>>> = Rc::new(RefCell::new(Vec::new()));
let image = Rc::new(RefCell::new(None));
let src = Rc::new(source.to_string());