buildfix_receipts/
load.rs1use anyhow::Context;
2use buildfix_types::receipt::ReceiptEnvelope;
3use camino::{Utf8Path, Utf8PathBuf};
4use fs_err as fs;
5use glob::glob;
6use thiserror::Error;
7use tracing::debug;
8
9#[derive(Debug, Clone)]
10pub struct LoadedReceipt {
11 pub path: Utf8PathBuf,
12 pub sensor_id: String,
14 pub receipt: Result<ReceiptEnvelope, ReceiptLoadError>,
15}
16
17#[derive(Debug, Error, Clone)]
18pub enum ReceiptLoadError {
19 #[error("io error: {message}")]
20 Io { message: String },
21
22 #[error("json parse error: {message}")]
23 Json { message: String },
24}
25
26pub fn load_receipts(artifacts_dir: &Utf8Path) -> anyhow::Result<Vec<LoadedReceipt>> {
27 let pattern = artifacts_dir.join("*/report.json");
28 let pattern_str = pattern.as_str();
29
30 debug!(pattern = %pattern_str, "scanning artifacts for receipts");
31
32 let mut out = Vec::new();
33 for entry in glob(pattern_str).context("glob artifacts/*/report.json")? {
34 let path = entry
35 .map_err(|e| anyhow::anyhow!("glob error: {e}"))?
36 .to_string_lossy()
37 .to_string();
38
39 let utf8_path = Utf8PathBuf::from(path);
40 let sensor_id = utf8_path
41 .parent()
42 .and_then(|p| p.file_name())
43 .unwrap_or("unknown")
44 .to_string();
45
46 if sensor_id == "buildfix" || sensor_id == "cockpit" {
48 debug!(path = %utf8_path, %sensor_id, "skipping non-sensor receipt");
49 continue;
50 }
51
52 let receipt = match fs::read_to_string(&utf8_path) {
53 Ok(s) => {
54 serde_json::from_str::<ReceiptEnvelope>(&s).map_err(|e| ReceiptLoadError::Json {
55 message: e.to_string(),
56 })
57 }
58 Err(e) => Err(ReceiptLoadError::Io {
59 message: e.to_string(),
60 }),
61 };
62
63 out.push(LoadedReceipt {
64 path: utf8_path,
65 sensor_id,
66 receipt,
67 });
68 }
69
70 out.sort_by(|a, b| a.path.cmp(&b.path));
72 Ok(out)
73}