refactor(workflow): share metadata snapshot helpers

Centralize compatibility notice detection and elapsed-time/error-cause helpers used by metadata snapshot event rendering and emission.
This commit is contained in:
Bryan Helmkamp 2026-04-29 19:43:30 -04:00
parent 4a4f0f8548
commit 07d3890bb8
No known key found for this signature in database
7 changed files with 32 additions and 55 deletions

View file

@ -14,6 +14,7 @@ use std::time::Duration;
use anyhow::{Context, Result, bail};
use chrono::{DateTime, Utc};
use fabro_redact::redact_jsonl_line;
use fabro_types::run_event::is_metadata_snapshot_compat_notice_code;
use fabro_util::json::normalize_json_value;
use fabro_util::terminal::Styles;
use tokio::time;
@ -762,13 +763,6 @@ fn is_metadata_snapshot_compat_notice(envelope: &serde_json::Value) -> bool {
prop_str_field(envelope, "code").is_some_and(is_metadata_snapshot_compat_notice_code)
}
fn is_metadata_snapshot_compat_notice_code(code: &str) -> bool {
matches!(
code,
"checkpoint_metadata_write_failed" | "checkpoint_metadata_push_failed"
)
}
fn str_field<'a>(value: &'a serde_json::Value, key: &str) -> Option<&'a str> {
value.get(key)?.as_str()
}

View file

@ -4,6 +4,7 @@
)]
use fabro_types::RunEvent;
use fabro_types::run_event::is_metadata_snapshot_compat_notice_code;
mod event;
mod info_display;
@ -442,13 +443,6 @@ impl ProgressUI {
}
}
fn is_metadata_snapshot_compat_notice_code(code: &str) -> bool {
matches!(
code,
"checkpoint_metadata_write_failed" | "checkpoint_metadata_push_failed"
)
}
#[cfg(test)]
mod tests {
#![allow(

View file

@ -1,5 +1,19 @@
use serde::{Deserialize, Serialize};
/// Legacy `run.notice` codes paired with the new `metadata.snapshot.failed`
/// event for backward compatibility. Display layers suppress these so the
/// typed event renders without a duplicate raw warning.
pub const NOTICE_CODE_CHECKPOINT_METADATA_WRITE_FAILED: &str = "checkpoint_metadata_write_failed";
pub const NOTICE_CODE_CHECKPOINT_METADATA_PUSH_FAILED: &str = "checkpoint_metadata_push_failed";
#[must_use]
pub fn is_metadata_snapshot_compat_notice_code(code: &str) -> bool {
matches!(
code,
NOTICE_CODE_CHECKPOINT_METADATA_WRITE_FAILED | NOTICE_CODE_CHECKPOINT_METADATA_PUSH_FAILED
)
}
#[derive(
Debug,
Clone,

View file

@ -13,6 +13,7 @@ pub mod run_log;
pub mod session_secret;
pub mod terminal;
pub mod text;
pub mod time;
pub mod version;
pub mod warnings;

View file

@ -0,0 +1,6 @@
use std::time::Instant;
#[must_use]
pub fn elapsed_ms(started: Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}

View file

@ -9,6 +9,8 @@ use fabro_core::outcome::NodeResult;
use fabro_core::state::ExecutionState;
use fabro_types::RunId;
use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};
use fabro_util::error::collect_causes;
use fabro_util::time::elapsed_ms;
use crate::artifact;
use crate::event::{Emitter, Event, RunNoticeLevel, StageScope};
@ -110,7 +112,7 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
started,
MetadataSnapshotFailureKind::LoadState,
message.clone(),
anyhow_causes(&err),
collect_causes(err.as_ref()),
None,
None,
None,
@ -179,7 +181,7 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
started,
MetadataSnapshotFailureKind::LoadState,
message.clone(),
anyhow_causes(&err),
collect_causes(err.as_ref()),
None,
None,
None,
@ -355,7 +357,7 @@ impl GitLifecycle {
started,
MetadataSnapshotFailureKind::Write,
message.clone(),
error_causes(&err),
collect_causes(&err),
None,
None,
None,
@ -455,24 +457,6 @@ impl GitLifecycle {
}
}
fn elapsed_ms(started: Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}
fn anyhow_causes(error: &anyhow::Error) -> Vec<String> {
error.chain().skip(1).map(ToString::to_string).collect()
}
fn error_causes(error: &(dyn std::error::Error + 'static)) -> Vec<String> {
let mut causes = Vec::new();
let mut source = error.source();
while let Some(cause) = source {
causes.push(cause.to_string());
source = cause.source();
}
causes
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, HashMap};

View file

@ -3,6 +3,8 @@ use std::time::Instant;
use fabro_hooks::{HookContext, HookEvent};
use fabro_types::run_event::{MetadataSnapshotFailureKind, MetadataSnapshotPhase};
use fabro_types::{BilledTokenCounts, EventBody};
use fabro_util::error::collect_causes;
use fabro_util::time::elapsed_ms;
use super::types::{Concluded, FinalizeOptions, Retroed};
use crate::error::Error;
@ -171,7 +173,7 @@ pub async fn write_finalize_commit(
started,
MetadataSnapshotFailureKind::LoadState,
message.clone(),
anyhow_causes(&err),
collect_causes(err.as_ref()),
None,
None,
None,
@ -221,7 +223,7 @@ pub async fn write_finalize_commit(
started,
MetadataSnapshotFailureKind::Write,
message.clone(),
error_causes(&err),
collect_causes(&err),
None,
None,
None,
@ -294,24 +296,6 @@ fn emit_metadata_warning(services: &RunServices, code: &str, message: String) {
}
}
fn elapsed_ms(started: Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}
fn anyhow_causes(error: &anyhow::Error) -> Vec<String> {
error.chain().skip(1).map(ToString::to_string).collect()
}
fn error_causes(error: &(dyn std::error::Error + 'static)) -> Vec<String> {
let mut causes = Vec::new();
let mut source = error.source();
while let Some(cause) = source {
causes.push(cause.to_string());
source = cause.source();
}
causes
}
/// Failed and cancelled runs use a shorter diff timeout so a corrupted
/// workspace can't stall downstream consumers waiting on the terminal event.
async fn compute_final_patch(