Make CI io errors self-describing
Two fixes for uninformative error messages:
(1) Error::Io used \"io error: {0}\" with #[from] so display_chain
printed the io::Error twice (once via Display and once via .source()).
Dropped the {0} so the chain walk handles it.
(2) Added CommandSpawnFailed variant that captures the program name
and cwd at the Cmd::run boundary. Previously a missing binary or bad
cwd surfaced as bare \"No such file or directory\" with no clue which
program or directory.
Assisted-by: GLM-5.1 via pi
diff --git a/src/ci/error.rs b/src/ci/error.rs
index 2995401..d2912d8 100644
--- a/src/ci/error.rs
+++ b/src/ci/error.rs
@@ -10,7 +10,7 @@ use crate::secret;
/// Errors produced by CI operations.
#[derive(Debug, thiserror::Error, Diagnostic)]
pub enum Error {
- #[error("io error: {0}")]
+ #[error("io error")]
Io(#[from] std::io::Error),
#[error(transparent)]
@@ -73,6 +73,14 @@ pub enum Error {
#[error(transparent)]
Secret(#[from] secret::Error),
+ #[error("command spawn failed: {program} in {cwd}")]
+ CommandSpawnFailed {
+ program: String,
+ cwd: std::path::PathBuf,
+ #[source]
+ source: std::io::Error,
+ },
+
#[error("unknown secret: {0:?}")]
UnknownSecret(String),
}
diff --git a/src/ci/runtime.rs b/src/ci/runtime.rs
index 84d4d46..c529cd7 100644
--- a/src/ci/runtime.rs
+++ b/src/ci/runtime.rs
@@ -215,12 +215,28 @@ impl Runtime {
pub(super) fn sh(&self, cmd: Cmd, opts: ShOpts) -> super::error::Result<ShOutput> {
let started_at = Timestamp::now();
let output = match self.docker_target() {
- None => cmd.run(opts, &self.workspace)?,
+ None => {
+ let program = cmd.program().to_string();
+ cmd.run(opts, &self.workspace).map_err(|e| {
+ super::error::Error::CommandSpawnFailed {
+ program,
+ cwd: self.workspace.clone(),
+ source: e,
+ }
+ })?
+ }
Some((container_id, work_dir)) => {
let wrapped = Cmd::wrap_in_docker_exec(cmd, container_id, work_dir, &opts);
+ let program = wrapped.program().to_string();
// env was embedded into the docker exec argv; clear it
// so it isn't also set on the host `docker` process.
- wrapped.run(ShOpts::default(), &self.workspace)?
+ wrapped
+ .run(ShOpts::default(), &self.workspace)
+ .map_err(|e| super::error::Error::CommandSpawnFailed {
+ program,
+ cwd: self.workspace.clone(),
+ source: e,
+ })?
}
};
let finished_at = Timestamp::now();
@@ -390,6 +406,13 @@ impl From<Cmd> for std::process::Command {
}
impl Cmd {
+ pub fn program(&self) -> &str {
+ match self {
+ Cmd::Shell(_) => "sh",
+ Cmd::Argv { program, .. } => program,
+ }
+ }
+
/// Spawn this command with the given options, blocking until exit,
/// and capture the result. Inherits the runner's env with
/// `opts.env` merged on top. `cwd` becomes the child's working