Wrap mlua errors as Error::Lua instead of dyn Error
The previous commit widened JobFailed::source to a dyn Error so both
Lua and Rust run-fns could land there, but that erased the type and
broke pattern-matching. Add an Error::Lua variant for mlua errors and
restore JobFailed::source to Box<Error>.

Assisted-by: Claude Opus 4.7 via Claude Code
change yvunmvxsvvopmtzpznxkpqotnkuptuqr
commit dc5c616d66f9ba2fcfea474ec3d68ee7d782183d
author Alpha Chen <alpha@kejadlen.dev>
date
parent mksopxxr
diff --git a/src/ci/run.rs b/src/ci/run.rs
index 057f211..ca7caf3 100644
--- a/src/ci/run.rs
+++ b/src/ci/run.rs
@@ -286,8 +286,7 @@ impl Run {
 
         self.transition(RunState::Active)?;
 
-        let mut failed_job: Option<(String, Box<dyn std::error::Error + Send + Sync + 'static>)> =
-            None;
+        let mut failed_job: Option<(String, Error)> = None;
         for job_id in runtime.topo_order() {
             let run_fn = runtime
                 .job(job_id)
@@ -296,15 +295,12 @@ impl Run {
                 .clone();
 
             runtime.enter_job(job_id);
-            let result: std::result::Result<
-                (),
-                Box<dyn std::error::Error + Send + Sync + 'static>,
-            > = match run_fn {
+            let result: Result<()> = match run_fn {
                 RunFn::Lua(f) => f
                     .call::<mlua::Value>(rt_value.clone())
                     .map(|_| ())
-                    .map_err(|e| Box::new(e) as _),
-                RunFn::Rust(f) => f(&runtime).map_err(|e| Box::new(e) as _),
+                    .map_err(|e| Error::Lua(Box::new(e))),
+                RunFn::Rust(f) => f(&runtime),
             };
             runtime.leave_job();
 
@@ -323,7 +319,10 @@ impl Run {
 
         if let Some((job, source)) = failed_job {
             self.transition(RunState::Failed)?;
-            return Err(Error::JobFailed { job, source });
+            return Err(Error::JobFailed {
+                job,
+                source: Box::new(source),
+            });
         }
 
         self.transition(RunState::Complete)?;
diff --git a/src/error.rs b/src/error.rs
index 0437018..741ef14 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -30,16 +30,14 @@ pub enum Error {
     #[error("invalid run transition: {from:?} -> {to:?}")]
     InvalidTransition { from: RunState, to: RunState },
 
+    #[error(transparent)]
+    Lua(Box<mlua::Error>),
+
     #[error("job '{job}' failed")]
     JobFailed {
         job: String,
-        // Boxed `dyn Error` rather than a concrete type so both Lua
-        // and Rust run-fns can land here without an extra wrapper —
-        // an mlua::Error from `RunFn::Lua`, a crate::Error from
-        // `RunFn::Rust`, both walk through `display_chain` the same
-        // way.
         #[source]
-        source: Box<dyn std::error::Error + Send + Sync + 'static>,
+        source: Box<Error>,
     },
 
     #[error("git error: {0}")]