Skip to main content

buildfix_receipts/
load.rs

1use 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    /// Directory name under artifacts/... (best effort).
13    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        // Skip reserved output directories — not sensor receipts.
47        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    // Deterministic order matters.
71    out.sort_by(|a, b| a.path.cmp(&b.path));
72    Ok(out)
73}