From 832a9ba18dad79752d7624d5c1bd054ddf41faa5 Mon Sep 17 00:00:00 2001 From: Ihor Solodrai Date: Wed, 23 Sep 2026 14:58:08 -0700 Subject: [PATCH] semcode-index: Exit non-zero when a lore archive fails When semcode-index --lore cannot clone, fetch or index a lore archive, it reports the error on stderr, moves on to the next archive, and still exits with status 0. With explicit archive names it also counts the failed archive in "Archives processed". A script or CI job that runs semcode-index before querying lore therefore cannot tell a refreshed archive from a stale one. BPF CI ran into this. It switched from re-cloning the bpf archive on every job to refreshing the copy baked into its image, and the fetch failed because gitoxide had no committer for the reflog ("reflog messages need a committer which isn't set"). The step still passed, and reviews ran against the image's copy of lore, up to a day old. Keep going with the remaining archives, list the failed ones in the summary as the refresh path already does, and return an error at the end if any archive failed. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Ihor Solodrai --- src/bin/index.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/bin/index.rs b/src/bin/index.rs index 06bc744..07ff4ad 100644 --- a/src/bin/index.rs +++ b/src/bin/index.rs @@ -1112,6 +1112,7 @@ async fn main() -> Result<()> { let mut total_new_emails = 0usize; let mut total_emails_all_archives = 0usize; + let mut failed_archives: Vec<(String, String)> = Vec::new(); // Process each lore archive for lore_url in lore_args { @@ -1123,6 +1124,7 @@ async fn main() -> Result<()> { Ok(path) => path, Err(e) => { eprintln!("Error cloning {}: {:#}", lore_url, e); + failed_archives.push((lore_url.clone(), format!("{:#}", e))); continue; } }; @@ -1133,6 +1135,7 @@ async fn main() -> Result<()> { Ok(repo) => repo, Err(e) => { eprintln!("Error opening repository {}: {}", clone_path.display(), e); + failed_archives.push((lore_url.clone(), e.to_string())); continue; } }; @@ -1158,6 +1161,7 @@ async fn main() -> Result<()> { } Err(e) => { eprintln!("Error indexing {}: {}", lore_url, e); + failed_archives.push((lore_url.clone(), e.to_string())); continue; } } @@ -1167,13 +1171,25 @@ async fn main() -> Result<()> { println!("\n=== Lore Email Indexing Complete ==="); println!("Total time: {:.1}s", total_time.as_secs_f64()); - println!("Archives processed: {}", lore_args.len()); + println!( + "Archives processed: {}/{}", + lore_args.len() - failed_archives.len(), + lore_args.len() + ); println!("New emails indexed: {}", total_new_emails); println!( "Total emails across archives: {}", total_emails_all_archives ); + // Report failed archives if any + if !failed_archives.is_empty() { + eprintln!("\nFailed archives:"); + for (name, err) in &failed_archives { + eprintln!(" {}: {}", name, err); + } + } + if total_new_emails > 0 { // Compact only lore tables. The full optimize_database() // method processes every table in the database including @@ -1204,6 +1220,14 @@ async fn main() -> Result<()> { println!("\nTo query this database, run:"); println!(" semcode --database {}", database_path); + if !failed_archives.is_empty() { + return Err(anyhow::anyhow!( + "{} of {} lore archive(s) failed", + failed_archives.len(), + lore_args.len() + )); + } + return Ok(()); } else { // --lore without arguments: refresh all existing archives @@ -1401,6 +1425,14 @@ async fn main() -> Result<()> { println!("\nTo query this database, run:"); println!(" semcode --database {}", database_path); + if !failed_archives.is_empty() { + return Err(anyhow::anyhow!( + "{} of {} lore archive(s) failed to refresh", + failed_archives.len(), + total_archives + )); + } + return Ok(()); } }