fix: wrap propagated io errors with anyhow context across prod code

A recent CI flake surfaced as bare "error: No such file or directory
(os error 2)" with no chain, because the failing operation lived behind
a raw `?` on a `std::fs::` / `File::create` / `Command::spawn` call. The
error had no verb, no path, no hint at which step in server startup
broke. Retry loops were explicitly rejected -- the goal is to diagnose
the next occurrence, not mask it.

Wraps 50+ such sites across fabro-cli, fabro-server, fabro-workflow,
fabro-util, fabro-vault, fabro-telemetry, fabro-interview, fabro-llm,
and fabro-devcontainer with `.with_context(|| format!("<verb> {path}"))`
so anyhow's error chain carries both the operation and the path when
an io error escapes.

Where the enclosing function returns `io::Result` (fabro-util run_log,
fabro-interview recording, fabro-llm attachment loader), the error is
re-wrapped via `io::Error::new` to keep the signature stable. Where a
crate uses its own thiserror enum, either a new `io_context` helper
was added (fabro-vault) or the path was folded into the existing
`Error::Io(String)` message (fabro-workflow).

No retry loops. No behavior changes. Skipped sites documented:
`.ok()`-swallowed, `match ErrorKind::NotFound`, `let _ = ...`, typed
error variants that already carry the path, and test modules.

Verified: cargo build --workspace, cargo +nightly clippy --workspace
--all-targets -- -D warnings, cargo nextest run --workspace (3991/3991
pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-17 11:44:38 -04:00
parent 2d8d72717d
commit 7bc3fe0dbc
No known key found for this signature in database
22 changed files with 220 additions and 83 deletions

View file

@ -141,7 +141,8 @@ async fn write_artifact_file(
dest_file: &Path,
) -> Result<()> {
if let Some(parent) = dest_file.parent() {
std::fs::create_dir_all(parent)?;
std::fs::create_dir_all(parent)
.with_context(|| format!("creating directory {}", parent.display()))?;
}
let bytes = client
.download_stage_artifact(run_id, &entry.stage_id, &entry.relative_path)

View file

@ -1,6 +1,6 @@
use std::io::Write;
use anyhow::bail;
use anyhow::{Context, bail};
use fabro_api::types;
use fabro_config::load::load_settings_user;
use fabro_config::user::active_settings_path;
@ -62,7 +62,8 @@ pub(crate) async fn run(
.await?;
if let Some(ref output_path) = args.output {
std::fs::write(output_path, &rendered)?;
std::fs::write(output_path, &rendered)
.with_context(|| format!("writing rendered graph to {}", output_path.display()))?;
if cli.output.format == OutputFormat::Json {
print_json_pretty(&serde_json::json!({
"path": absolute_or_current(output_path),

View file

@ -1326,10 +1326,9 @@ fn persist_server_env_secrets(storage_dir: &Path, secrets: &[(String, String)])
return Ok(());
}
envfile::merge_env_file(
&Storage::new(storage_dir).server_state().env_path(),
secrets.iter().cloned(),
)?;
let env_path = Storage::new(storage_dir).server_state().env_path();
envfile::merge_env_file(&env_path, secrets.iter().cloned())
.with_context(|| format!("merging server env secrets into {}", env_path.display()))?;
Ok(())
}
@ -1364,14 +1363,18 @@ fn restore_optional_file(path: &Path, previous_contents: Option<&str>) -> Result
match previous_contents {
Some(contents) => {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
std::fs::create_dir_all(parent)
.with_context(|| format!("creating directory {}", parent.display()))?;
}
std::fs::write(path, contents)?;
std::fs::write(path, contents)
.with_context(|| format!("restoring {}", path.display()))?;
}
None => match std::fs::remove_file(path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
Err(err) => {
return Err(anyhow::Error::new(err).context(format!("removing {}", path.display())));
}
},
}
@ -1389,7 +1392,8 @@ fn persist_github_install_changes(
let previous_vault = std::fs::read_to_string(&vault_path).ok();
let result = (|| -> Result<()> {
let mut server_env = envfile::read_env_file(&server_env_path)?;
let mut server_env = envfile::read_env_file(&server_env_path)
.with_context(|| format!("reading env file {}", server_env_path.display()))?;
for key in &writes.server_env_remove {
server_env.remove(*key);
}
@ -1399,7 +1403,8 @@ fn persist_github_install_changes(
if server_env.is_empty() {
restore_optional_file(&server_env_path, None)?;
} else {
envfile::write_env_file(&server_env_path, &server_env)?;
envfile::write_env_file(&server_env_path, &server_env)
.with_context(|| format!("writing env file {}", server_env_path.display()))?;
}
let mut vault = Vault::load(vault_path.clone()).map_err(anyhow::Error::from)?;
@ -1415,7 +1420,14 @@ fn persist_github_install_changes(
.map_err(anyhow::Error::from)?;
}
std::fs::write(writes.settings_write.path, writes.settings_write.contents)?;
std::fs::write(writes.settings_write.path, writes.settings_write.contents).with_context(
|| {
format!(
"writing settings file {}",
writes.settings_write.path.display()
)
},
)?;
Ok(())
})();
@ -1467,7 +1479,8 @@ async fn persist_install_outputs_with_settings(
persist_server_env_secrets(storage_dir, server_env_secrets)?;
if let Some(write) = settings_write {
std::fs::write(write.path, write.contents)?;
std::fs::write(write.path, write.contents)
.with_context(|| format!("writing settings file {}", write.path.display()))?;
}
let persist_result = persist_vault_secrets_with(
@ -1482,8 +1495,10 @@ async fn persist_install_outputs_with_settings(
if let Err(err) = persist_result {
if let Some(write) = settings_write {
match write.previous_contents {
Some(previous) => std::fs::write(write.path, previous)?,
None if write.path.exists() => std::fs::remove_file(write.path)?,
Some(previous) => std::fs::write(write.path, previous)
.with_context(|| format!("restoring settings file {}", write.path.display()))?,
None if write.path.exists() => std::fs::remove_file(write.path)
.with_context(|| format!("removing settings file {}", write.path.display()))?,
None => {}
}
}
@ -1818,7 +1833,8 @@ async fn run_install_inner(
);
fabro_util::printerr!(printer, "");
std::fs::create_dir_all(&fabro_dir)?;
std::fs::create_dir_all(&fabro_dir)
.with_context(|| format!("creating fabro home directory {}", fabro_dir.display()))?;
{
let env_path = legacy_env::legacy_env_file_path();

View file

@ -25,7 +25,8 @@ pub(crate) struct ActiveServerRecord {
pub(crate) fn write_server_record(path: &Path, record: &ServerRecord) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
std::fs::create_dir_all(parent)
.with_context(|| format!("creating server record directory {}", parent.display()))?;
}
std::fs::write(path, serde_json::to_string_pretty(record)?)
.with_context(|| format!("Failed to write server metadata to {}", path.display()))

View file

@ -1,7 +1,7 @@
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Result, anyhow, bail};
use anyhow::{Context, Result, anyhow, bail};
use chrono::Utc;
use fabro_config::user::load_settings_config;
use fabro_config::{Storage, envfile, resolve_server_from_file};
@ -174,13 +174,33 @@ fn load_or_create_local_dev_token(storage_dir: &Path, home: &Home) -> Result<Str
.and_then(|entries| entries.get("FABRO_DEV_TOKEN").cloned())
.filter(|token| dev_token::validate_dev_token_format(token))
{
dev_token::write_dev_token(&home.dev_token_path(), &token)?;
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token)?;
dev_token::write_dev_token(&home.dev_token_path(), &token)
.with_context(|| format!("writing dev token to {}", home.dev_token_path().display()))?;
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token).with_context(
|| {
format!(
"writing dev token to {}",
storage.server_state().dev_token_path().display()
)
},
)?;
return Ok(token);
}
let token = dev_token::load_or_create_dev_token(&home.dev_token_path())?;
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token)?;
let token = dev_token::load_or_create_dev_token(&home.dev_token_path()).with_context(|| {
format!(
"loading or creating dev token at {}",
home.dev_token_path().display()
)
})?;
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token).with_context(
|| {
format!(
"writing dev token to {}",
storage.server_state().dev_token_path().display()
)
},
)?;
Ok(token)
}
@ -207,7 +227,8 @@ fn load_or_create_local_session_secret(storage_dir: &Path) -> Result<String> {
}
let secret = session_secret::generate_session_secret();
envfile::merge_env_file(&server_env_path, [("SESSION_SECRET", secret.as_str())])?;
envfile::merge_env_file(&server_env_path, [("SESSION_SECRET", secret.as_str())])
.with_context(|| format!("merging session secret into {}", server_env_path.display()))?;
Ok(secret)
}
@ -319,13 +340,17 @@ async fn execute_daemon(
let server_state = Storage::new(storage_dir).server_state();
let log_path = server_state.log_path();
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent)?;
std::fs::create_dir_all(parent)
.with_context(|| format!("creating log directory {}", parent.display()))?;
}
let record_path = server_state.record_path();
let log_file = std::fs::File::create(&log_path)?;
let stdout_log = log_file.try_clone()?;
let exe = std::env::current_exe()?;
let log_file = std::fs::File::create(&log_path)
.with_context(|| format!("creating server log file {}", log_path.display()))?;
let stdout_log = log_file
.try_clone()
.with_context(|| format!("cloning server log file handle for {}", log_path.display()))?;
let exe = std::env::current_exe().context("resolving current fabro executable path")?;
let mut cmd = TokioCommand::new(&exe);
cmd.args(["server", "__serve"])
@ -376,7 +401,9 @@ async fn execute_daemon(
#[cfg(unix)]
fabro_proc::pre_exec_setsid(cmd.as_std_mut());
let mut child = cmd.spawn()?;
let mut child = cmd
.spawn()
.with_context(|| format!("spawning fabro server subprocess {}", exe.display()))?;
if let Ok(Some(status)) = child.try_wait() {
record::remove_server_record(&record_path);
@ -464,13 +491,15 @@ fn print_dev_token(printer: Printer, home: &Home, token: &str) {
async fn acquire_lock(storage_dir: &Path) -> Result<std::fs::File> {
let lock_path = Storage::new(storage_dir).server_state().lock_path();
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
std::fs::create_dir_all(parent)
.with_context(|| format!("creating server lock directory {}", parent.display()))?;
}
let lock_file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)?;
.open(&lock_path)
.with_context(|| format!("opening server lock file {}", lock_path.display()))?;
let poll_interval = Duration::from_millis(50);
let timeout = Duration::from_secs(5);

View file

@ -277,7 +277,9 @@ fn inspect_output_dir(path: &Path) -> Result<OutputDirState> {
Ok(OutputDirState::ExistingEmpty)
}
Err(err) if err.kind() == ErrorKind::NotFound => Ok(OutputDirState::Missing),
Err(err) => Err(err.into()),
Err(err) => {
Err(anyhow::Error::new(err).context(format!("reading metadata for {}", path.display())))
}
}
}

View file

@ -144,8 +144,10 @@ impl Backend {
bail!("download failed: HTTP {}", resp.status());
}
let bytes = resp.bytes().await?;
let mut file = fs::File::create(&dest)?;
file.write_all(&bytes)?;
let mut file = fs::File::create(&dest)
.with_context(|| format!("creating download file {}", dest.display()))?;
file.write_all(&bytes)
.with_context(|| format!("writing download to {}", dest.display()))?;
}
}
Ok(dest)
@ -231,7 +233,8 @@ fn verify_checksum(path: &Path, expected_hex: &str) -> Result<()> {
let mut file = std::io::BufReader::new(
fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?,
);
std::io::copy(&mut file, &mut hasher)?;
std::io::copy(&mut file, &mut hasher)
.with_context(|| format!("reading {} for checksum", path.display()))?;
let computed = format!("{:x}", hasher.finalize());
// The .sha256 file may contain "hash filename" or just "hash"
let expected = expected_hex
@ -272,10 +275,12 @@ impl UpgradeCheckState {
fn save(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
fs::create_dir_all(parent)
.with_context(|| format!("creating directory {}", parent.display()))?;
}
let json = serde_json::to_string(self)?;
fs::write(path, json)?;
fs::write(path, json)
.with_context(|| format!("writing upgrade check state {}", path.display()))?;
Ok(())
}
}
@ -365,7 +370,10 @@ pub(crate) async fn run_upgrade(
let tarball_name = format!("fabro-{triple}.tar.gz");
let checksum_name = format!("{tarball_name}.sha256");
let current_exe = std::env::current_exe()?.canonicalize()?;
let current_exe = std::env::current_exe()
.context("resolving current fabro executable path")?
.canonicalize()
.context("canonicalizing current fabro executable path")?;
let exe_dir = current_exe
.parent()
.context("could not determine executable directory")?;
@ -382,7 +390,8 @@ pub(crate) async fn run_upgrade(
)?;
// Verify SHA256 using streaming hash
let checksum_content = fs::read_to_string(&checksum_path)?;
let checksum_content = fs::read_to_string(&checksum_path)
.with_context(|| format!("reading checksum file {}", checksum_path.display()))?;
verify_checksum(&tarball_path, &checksum_content)?;
debug!("SHA256 checksum verified");

View file

@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
use anyhow::{Result, bail};
use anyhow::{Context, Result, bail};
pub(crate) use fabro_config::user::*;
use fabro_types::settings::cli::CliTargetSettings;
use fabro_types::settings::{CliSettings, SettingsLayer};
@ -208,9 +208,12 @@ pub(crate) fn build_server_client_builder(
let key_path = fabro_config::expand_tilde(&tls.key);
let ca_path = fabro_config::expand_tilde(&tls.ca);
let cert_pem = std::fs::read(&cert_path)?;
let key_pem = std::fs::read(&key_path)?;
let ca_pem = std::fs::read(&ca_path)?;
let cert_pem = std::fs::read(&cert_path)
.with_context(|| format!("reading TLS client certificate {}", cert_path.display()))?;
let key_pem = std::fs::read(&key_path)
.with_context(|| format!("reading TLS client key {}", key_path.display()))?;
let ca_pem = std::fs::read(&ca_path)
.with_context(|| format!("reading TLS CA certificate {}", ca_path.display()))?;
let mut identity_pem = cert_pem;
identity_pem.push(b'\n');

View file

@ -23,11 +23,15 @@ pub(crate) fn parse_compose(
compose_path: &Path,
service_name: &str,
) -> Result<ComposeServiceSpec, String> {
let contents = std::fs::read_to_string(compose_path)
.map_err(|e| format!("failed to read compose file: {e}"))?;
let contents = std::fs::read_to_string(compose_path).map_err(|e| {
format!(
"failed to read compose file {}: {e}",
compose_path.display()
)
})?;
let doc: serde_yaml::Value =
serde_yaml::from_str(&contents).map_err(|e| format!("failed to parse YAML: {e}"))?;
let doc: serde_yaml::Value = serde_yaml::from_str(&contents)
.map_err(|e| format!("failed to parse YAML {}: {e}", compose_path.display()))?;
let service = doc
.get("services")

View file

@ -564,9 +564,12 @@ pub(crate) async fn resolve_features(
.as_nanos()
);
let tmp_dir = std::env::temp_dir().join(unique_id);
fs::create_dir_all(&tmp_dir)
.await
.map_err(|e| DevcontainerError::Feature(format!("failed to create temp dir: {e}")))?;
fs::create_dir_all(&tmp_dir).await.map_err(|e| {
DevcontainerError::Feature(format!(
"failed to create temp dir {}: {e}",
tmp_dir.display()
))
})?;
// Collect feature IDs in a stable order
let mut feature_ids: Vec<String> = features.keys().cloned().collect();

View file

@ -53,7 +53,12 @@ impl RecordingInterviewer {
/// Returns an error if serialization or file writing fails.
pub fn save_to_file(&self, path: &Path) -> std::io::Result<()> {
let json = self.to_json()?;
std::fs::write(path, json)?;
std::fs::write(path, json).map_err(|err| {
std::io::Error::new(
err.kind(),
format!("write interview recording {}: {err}", path.display()),
)
})?;
Ok(())
}
@ -62,7 +67,12 @@ impl RecordingInterviewer {
/// # Errors
/// Returns an error if file reading or deserialization fails.
pub fn load_from_file(path: &Path) -> std::io::Result<Vec<(Question, Answer)>> {
let json = std::fs::read_to_string(path)?;
let json = std::fs::read_to_string(path).map_err(|err| {
std::io::Error::new(
err.kind(),
format!("read interview recording {}: {err}", path.display()),
)
})?;
Self::from_json(&json)
}
}

View file

@ -100,7 +100,9 @@ pub fn load_file_as_base64(path: &str) -> Result<(String, String), std::io::Erro
format!("{home}/{rest}")
},
);
let data = std::fs::read(&expanded)?;
let data = std::fs::read(&expanded).map_err(|err| {
std::io::Error::new(err.kind(), format!("read attachment {expanded}: {err}"))
})?;
let mime = mime_from_extension(&expanded).to_string();
let b64 = BASE64_STANDARD.encode(&data);
Ok((b64, mime))

View file

@ -157,7 +157,8 @@ fn build_local_object_store_with_preference(
return Ok(Arc::new(InMemory::new()));
}
std::fs::create_dir_all(store_path)?;
std::fs::create_dir_all(store_path)
.with_context(|| format!("creating object store directory {}", store_path.display()))?;
Ok(Arc::new(LocalFileSystem::new_with_prefix(store_path)?))
}
@ -318,7 +319,8 @@ where
let bind_request =
resolve_bind_request_from_settings(&effective_settings, args.bind.as_deref())?;
let shared_settings = Arc::new(RwLock::new(effective_settings));
std::fs::create_dir_all(&data_dir)?;
std::fs::create_dir_all(&data_dir)
.with_context(|| format!("creating data directory {}", data_dir.display()))?;
let (auth_mode, max_concurrent_runs) = {
let auth_mode = resolve_auth_mode_with_lookup(&resolved_server_settings, |name| {
server_secrets.get(name)
@ -499,7 +501,9 @@ where
#[cfg(debug_assertions)]
let mut watch_web_child = if watch_web {
let web_dir = std::env::current_dir()?.join("apps/fabro-web");
let web_dir = std::env::current_dir()
.context("reading current directory for --watch-web")?
.join("apps/fabro-web");
info!(dir = %web_dir.display(), "Starting bun run dev (--watch-web)");
#[expect(
clippy::disallowed_methods,
@ -508,7 +512,8 @@ where
let child = std::process::Command::new("bun")
.args(["run", "dev"])
.current_dir(&web_dir)
.spawn()?;
.spawn()
.with_context(|| format!("spawning `bun run dev` in {}", web_dir.display()))?;
Some(child)
} else {
None
@ -577,10 +582,12 @@ async fn bind_listener(requested: &BindRequest) -> anyhow::Result<BoundServerLis
match requested {
BindRequest::Unix(path) => {
if path.exists() {
std::fs::remove_file(path)?;
std::fs::remove_file(path)
.with_context(|| format!("removing stale unix socket {}", path.display()))?;
}
let listener = UnixListener::bind(path)?;
let listener = UnixListener::bind(path)
.with_context(|| format!("binding unix socket {}", path.display()))?;
Ok(BoundServerListener {
listener: BoundListener::Unix(listener),
bind: Bind::Unix(path.clone()),

View file

@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock, Mutex, RwLock};
use std::time::{Duration, Instant};
use anyhow::Context as _;
use axum::body::Body;
#[cfg(test)]
use axum::body::to_bytes;
@ -3363,8 +3364,8 @@ fn worker_command(
mode: RunExecutionMode,
run_dir: &std::path::Path,
) -> anyhow::Result<Command> {
let exe =
std::env::var_os("CARGO_BIN_EXE_fabro").map_or(std::env::current_exe()?, PathBuf::from);
let current_exe = std::env::current_exe().context("reading current executable path")?;
let exe = std::env::var_os("CARGO_BIN_EXE_fabro").map_or(current_exe, PathBuf::from);
let storage_dir = state.server_storage_dir();
let server_target = current_server_target(&storage_dir)?;
let artifact_upload_token = state
@ -4257,7 +4258,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
));
let mut child = match worker_command(state.as_ref(), run_id, execution_mode, &run_dir)
.and_then(|mut cmd| cmd.spawn().map_err(anyhow::Error::from))
.and_then(|mut cmd| cmd.spawn().context("spawning run worker process"))
{
Ok(child) => child,
Err(err) => {

View file

@ -1,6 +1,7 @@
use std::panic::PanicHookInfo;
use std::path::Path;
use anyhow::Context as _;
use sentry::integrations::backtrace;
use sentry::protocol::{Context, Event, Exception, Mechanism, OsContext, Values};
@ -116,7 +117,8 @@ fn spawn_panic_sender(event: &Event<'static>) {
pub fn capture(path: &Path) -> anyhow::Result<()> {
let dsn = SENTRY_DSN.ok_or_else(|| anyhow::anyhow!("SENTRY_DSN not set at compile time"))?;
let json = std::fs::read(path)?;
let json =
std::fs::read(path).with_context(|| format!("read panic payload {}", path.display()))?;
let event: Event<'static> = serde_json::from_slice(&json)?;
let guard = sentry::init((dsn, sentry::ClientOptions::default()));

View file

@ -1,5 +1,6 @@
use std::path::Path;
use anyhow::Context;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use uuid::Uuid;
@ -122,7 +123,8 @@ pub async fn upload(path: &Path) -> anyhow::Result<()> {
let write_key = SEGMENT_WRITE_KEY
.ok_or_else(|| anyhow::anyhow!("SEGMENT_WRITE_KEY not set at compile time"))?;
let content = std::fs::read_to_string(path)?;
let content = std::fs::read_to_string(path)
.with_context(|| format!("read telemetry batch {}", path.display()))?;
let Some(payload) = build_segment_batch(&content) else {
return Ok(());
};

View file

@ -2,7 +2,7 @@ use std::fs;
use std::io::Write as _;
use std::path::Path;
use anyhow::{Result, anyhow};
use anyhow::{Context, Result, anyhow};
use rand::TryRngCore;
use rand::rngs::OsRng;
@ -52,7 +52,10 @@ pub fn load_or_create_dev_token(path: &Path) -> Result<String> {
return Err(anyhow!("invalid dev token format in {}", path.display()));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
Err(err) => {
return Err(anyhow::Error::from(err))
.with_context(|| format!("read dev token {}", path.display()));
}
}
let token = generate_dev_token();
@ -70,7 +73,8 @@ pub fn write_dev_token(path: &Path, token: &str) -> Result<()> {
fn atomic_write_private(path: &Path, contents: &str) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
fs::create_dir_all(parent)
.with_context(|| format!("create directory {}", parent.display()))?;
}
let temp_path = path.with_file_name(format!(
@ -81,7 +85,8 @@ fn atomic_write_private(path: &Path, contents: &str) -> Result<()> {
rand::random::<u64>()
));
write_private_file(&temp_path, contents)?;
fs::rename(&temp_path, path)?;
fs::rename(&temp_path, path)
.with_context(|| format!("rename {} to {}", temp_path.display(), path.display()))?;
Ok(())
}
@ -96,13 +101,17 @@ fn write_private_file(path: &Path, contents: &str) -> Result<()> {
.create(true)
.truncate(true)
.mode(0o600)
.open(path)?
.open(path)
.with_context(|| format!("open {} for writing", path.display()))?
};
#[cfg(not(unix))]
let mut file = std::fs::File::create(path)?;
let mut file =
std::fs::File::create(path).with_context(|| format!("create {}", path.display()))?;
file.write_all(contents.as_bytes())?;
file.sync_all()?;
file.write_all(contents.as_bytes())
.with_context(|| format!("write {}", path.display()))?;
file.sync_all()
.with_context(|| format!("sync {}", path.display()))?;
Ok(())
}

View file

@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::fmt;
use std::path::{Component, Path, PathBuf};
use std::{fmt, io};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
@ -77,7 +77,7 @@ impl Vault {
let entries = match std::fs::read_to_string(&path) {
Ok(contents) => serde_json::from_str(&contents)?,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => HashMap::new(),
Err(err) => return Err(err.into()),
Err(err) => return Err(io_context("read vault", &path, &err).into()),
};
Ok(Self { path, entries })
@ -231,7 +231,8 @@ impl Vault {
.path
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
std::fs::create_dir_all(&parent)?;
std::fs::create_dir_all(&parent)
.map_err(|err| io_context("create vault directory", &parent, &err))?;
let file_name = self
.path
@ -240,18 +241,32 @@ impl Vault {
.unwrap_or("secrets.json");
let tmp_path = parent.join(format!(".{file_name}.tmp-{}", ulid::Ulid::new()));
let json = serde_json::to_vec_pretty(&self.entries)?;
std::fs::write(&tmp_path, json)?;
std::fs::write(&tmp_path, json)
.map_err(|err| io_context("write vault temp file", &tmp_path, &err))?;
set_private_permissions(&tmp_path)?;
std::fs::rename(&tmp_path, &self.path)?;
std::fs::rename(&tmp_path, &self.path).map_err(|err| {
io_context(
&format!("rename vault temp file to {}", self.path.display()),
&tmp_path,
&err,
)
})?;
Ok(())
}
}
/// Wrap an `io::Error` with a human-readable verb and path so downstream
/// reporting shows which operation failed on which file.
fn io_context(op: &str, path: &Path, source: &io::Error) -> io::Error {
io::Error::new(source.kind(), format!("{op} {}: {source}", path.display()))
}
#[cfg(unix)]
fn set_private_permissions(path: &Path) -> Result<(), Error> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.map_err(|err| io_context("set permissions on", path, &err))?;
Ok(())
}

View file

@ -282,9 +282,16 @@ async fn materialize_blob_ref(
let path = local_materialized_blob_path(run_dir, blob_id);
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
std::fs::create_dir_all(parent).map_err(|err| {
Error::Io(format!(
"creating artifact blob directory {}: {err}",
parent.display()
))
})?;
}
std::fs::write(&path, &bytes)?;
std::fs::write(&path, &bytes).map_err(|err| {
Error::Io(format!("writing artifact blob {}: {err}", path.display()))
})?;
}
return Ok(format!("{ARTIFACT_POINTER_PREFIX}{}", path.display()));
}

View file

@ -107,7 +107,12 @@ pub struct Started {
/// Start a fresh workflow run. Errors if a checkpoint already exists (use
/// `resume()` instead).
pub async fn start(run_dir: &Path, services: StartServices) -> Result<Started, Error> {
std::fs::create_dir_all(run_dir).map_err(|err| Error::Io(err.to_string()))?;
std::fs::create_dir_all(run_dir).map_err(|err| {
Error::Io(format!(
"creating run directory {}: {err}",
run_dir.display()
))
})?;
let state = services
.run_store
.state()

View file

@ -13,7 +13,12 @@ pub(crate) fn persist(
let (graph, source, diagnostics) = validated.into_parts();
options.run_record.graph = graph.clone();
std::fs::create_dir_all(&options.run_dir)?;
std::fs::create_dir_all(&options.run_dir).map_err(|err| {
Error::Io(format!(
"creating run directory {}: {err}",
options.run_dir.display()
))
})?;
Ok(Persisted::new(
graph,

View file

@ -148,7 +148,10 @@ fn scan_orphan_runs(base: &Path) -> Result<Vec<RunInfo>> {
let entries = match std::fs::read_dir(base) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err.into()),
Err(err) => {
return Err(anyhow::Error::new(err)
.context(format!("reading orphan runs directory {}", base.display())));
}
};
let mut runs = Vec::new();