Decouple `runtime`/`mirror` from the kitchen-sink `ci::Error`
Mirrors the earlier `CompileError` move on the runtime side: a small
`RuntimeError` (sum of `secret::Error`, `mlua::Error`,
`CommandSpawnFailed`, and `Git`) carries failures through
`Runtime::secret`/`sh`, `MirrorJob::execute`, and the `RustRunFn`
callback type. A flattening `From<RuntimeError> for Error` at the
boundary keeps existing kitchen-sink callers working.
Sets up the next move: `pipeline`, `registration`, `mirror`,
`runtime`, and `RunMeta` no longer reference `super::error`, so they
can land in `quire-core` without dragging rusqlite, yaml, and the
rest of the orchestrator-flavored variants along.
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 e4675a1..3edb624 100644
--- a/quire-server/src/ci/error.rs
+++ b/quire-server/src/ci/error.rs
@@ -4,6 +4,7 @@ use miette::Diagnostic;
use super::pipeline::{CompileError, PipelineError};
use super::run::RunState;
+use super::runtime::RuntimeError;
use quire_core::fennel::FennelError;
use quire_core::secret;
@@ -81,6 +82,25 @@ impl From<CompileError> for Error {
}
}
+impl From<RuntimeError> for Error {
+ fn from(err: RuntimeError) -> Self {
+ match err {
+ RuntimeError::Secret(e) => Error::Secret(e),
+ RuntimeError::Lua(e) => Error::Lua(e),
+ RuntimeError::CommandSpawnFailed {
+ program,
+ cwd,
+ source,
+ } => Error::CommandSpawnFailed {
+ program,
+ cwd,
+ source,
+ },
+ RuntimeError::Git(s) => Error::Git(s),
+ }
+ }
+}
+
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 a71097d..265770c 100644
--- a/quire-server/src/ci/mirror.rs
+++ b/quire-server/src/ci/mirror.rs
@@ -19,10 +19,9 @@ use std::rc::Rc;
use mlua::{Lua, LuaSerdeExt};
-use super::error::{Error, Result};
use super::pipeline::{self, DefinitionError, Job, RunFn};
use super::registration::Registration;
-use super::runtime::{Cmd, Runtime, ShOpts};
+use super::runtime::{Cmd, Runtime, RuntimeError, RuntimeResult, ShOpts};
/// Closure state for the `quire/mirror` job's run-fn: everything the
/// tag-and-push needs at execute time, captured once at registration.
@@ -46,7 +45,7 @@ impl MirrorJob {
/// `git push` exit lands in the run log alongside any other shell
/// output. Returns `Err` only for setup failures (unknown secret,
/// failed tag, base64 spawn).
- fn execute(&self, rt: &Runtime) -> Result<()> {
+ fn execute(&self, rt: &Runtime) -> RuntimeResult<()> {
let calling = rt.current_job.borrow();
let calling = calling
.as_ref()
@@ -91,7 +90,7 @@ impl MirrorJob {
git_opts.clone(),
)?;
if tag_result.exit != 0 {
- return Err(Error::Git(format!(
+ return Err(RuntimeError::Git(format!(
"git tag failed: {}",
tag_result.stderr.trim()
)));
@@ -600,7 +599,7 @@ mod tests {
runtime.leave_job();
assert!(
- matches!(err, Error::Secret(SecretError::UnknownSecret(ref name)) if name == "missing"),
+ matches!(err, RuntimeError::Secret(SecretError::UnknownSecret(ref name)) if name == "missing"),
"expected UnknownSecret(\"missing\"), got: {err:?}"
);
}
diff --git a/quire-server/src/ci/pipeline.rs b/quire-server/src/ci/pipeline.rs
index 564c553..899e01a 100644
--- a/quire-server/src/ci/pipeline.rs
+++ b/quire-server/src/ci/pipeline.rs
@@ -112,7 +112,7 @@ 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) -> super::error::Result<()>>;
+ std::rc::Rc<dyn Fn(&super::runtime::Runtime) -> super::runtime::RuntimeResult<()>>;
/// How a job runs at execute time.
///
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 08c81e1..19bc899 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -258,7 +258,7 @@ impl Run {
let _: mlua::Value = f.call(rt_value.clone())?;
Ok(())
}
- RunFn::Rust(f) => f(&runtime),
+ RunFn::Rust(f) => f(&runtime).map_err(Into::into),
})();
runtime.leave_job();
@@ -1278,7 +1278,9 @@ mod tests {
);
pipeline.replace_first_run_fn(RunFn::Rust(Rc::new(|_rt| {
- Err(Error::Git("simulated rust failure".into()))
+ Err(crate::ci::runtime::RuntimeError::Git(
+ "simulated rust failure".into(),
+ ))
})));
let err = run
diff --git a/quire-server/src/ci/runtime.rs b/quire-server/src/ci/runtime.rs
index 98387e4..ab8ef79 100644
--- a/quire-server/src/ci/runtime.rs
+++ b/quire-server/src/ci/runtime.rs
@@ -15,7 +15,40 @@ use mlua::{IntoLua, Lua, LuaSerdeExt};
use super::pipeline::{Job, Pipeline};
use super::run::RunMeta;
-use quire_core::secret::{SecretRegistry, SecretString, redact};
+use quire_core::secret::{self, SecretRegistry, SecretString, redact};
+
+/// Errors produced by [`Runtime`] methods and the `RunFn::Rust`
+/// callbacks that hold them. A small sum carved out of the
+/// orchestrator's kitchen-sink error so the runtime layer doesn't
+/// drag rusqlite/yaml/etc. along with it.
+#[derive(Debug, thiserror::Error, miette::Diagnostic)]
+pub enum RuntimeError {
+ #[error(transparent)]
+ Secret(#[from] secret::Error),
+
+ #[error(transparent)]
+ Lua(Box<mlua::Error>),
+
+ #[error("command spawn failed: {program} in {cwd}")]
+ CommandSpawnFailed {
+ program: String,
+ cwd: std::path::PathBuf,
+ #[source]
+ source: std::io::Error,
+ },
+
+ #[error("git error: {0}")]
+ Git(String),
+}
+
+impl From<mlua::Error> for RuntimeError {
+ fn from(err: mlua::Error) -> Self {
+ Self::Lua(Box::new(err))
+ }
+}
+
+pub type RuntimeResult<T> = std::result::Result<T, RuntimeError>;
+
/// Per-sh timing: (index, started_at, finished_at).
pub(super) type ShTimings = Vec<(usize, Timestamp, Timestamp)>;
@@ -176,23 +209,23 @@ impl Runtime {
/// the full caveat.
///
/// [`SecretRegistry::resolve`]: quire_core::secret::SecretRegistry::resolve
- pub(super) fn secret(&self, name: &str) -> super::error::Result<String> {
+ pub(super) fn secret(&self, name: &str) -> RuntimeResult<String> {
self.registry.borrow_mut().resolve(name).map_err(Into::into)
}
/// Run `cmd` with `opts` and record its output against the
/// current job (if one is active). Non-zero exits come back in
/// `:exit`, not as `Err`.
- pub(super) fn sh(&self, cmd: Cmd, opts: ShOpts) -> super::error::Result<ShOutput> {
+ pub(super) fn sh(&self, cmd: Cmd, opts: ShOpts) -> RuntimeResult<ShOutput> {
let started_at = Timestamp::now();
let program = cmd.program().to_string();
- let output = cmd.run(opts, &self.workspace).map_err(|e| {
- super::error::Error::CommandSpawnFailed {
- program,
- cwd: self.workspace.clone(),
- source: e,
- }
- })?;
+ let output =
+ cmd.run(opts, &self.workspace)
+ .map_err(|e| RuntimeError::CommandSpawnFailed {
+ program,
+ cwd: self.workspace.clone(),
+ source: e,
+ })?;
let finished_at = Timestamp::now();
if let Some(job) = self.current_job.borrow().as_ref() {
let n = {