Skip to main content

gha_container_proof/
engine.rs

1//! Top-level orchestration for the four CLI commands. Each `run_*` function
2//! returns a finished [`ContainerProofReceipt`].
3
4use anyhow::Result;
5
6use crate::model::ContainerProofReceipt;
7use crate::plan::{ActionPlanInput, JobPlanInput, plan_action, plan_job};
8use crate::probe::{ProbeInput, probe};
9use crate::workflow::{CheckWorkflowOptions, scan_workflows};
10
11pub fn run_check_workflow(options: &CheckWorkflowOptions) -> Result<ContainerProofReceipt> {
12    let scan = scan_workflows(options)?;
13    Ok(ContainerProofReceipt::build(
14        "check-workflow",
15        scan.subjects,
16        scan.checks,
17    ))
18}
19
20pub fn run_plan_job(input: &JobPlanInput) -> Result<ContainerProofReceipt> {
21    let subject = plan_job(input);
22    Ok(ContainerProofReceipt::build(
23        "plan-job",
24        vec![subject],
25        Vec::new(),
26    ))
27}
28
29pub fn run_plan_action(input: &ActionPlanInput) -> Result<ContainerProofReceipt> {
30    let subject = plan_action(input);
31    Ok(ContainerProofReceipt::build(
32        "plan-action",
33        vec![subject],
34        Vec::new(),
35    ))
36}
37
38pub fn run_probe(input: &ProbeInput) -> Result<ContainerProofReceipt> {
39    let subject = probe(input);
40    Ok(ContainerProofReceipt::build(
41        "probe",
42        vec![subject],
43        Vec::new(),
44    ))
45}
46
47/// Promote selected warnings to failures when strict mode is on.
48///
49/// Strict mode in v1 is binary: any warning becomes a failure. The
50/// [`docs/RULES.md`](../../docs/RULES.md) lists which checks are *typically*
51/// promoted, but for stability we apply it uniformly so the CLI's exit code
52/// matches the simple "no warnings, no failures" expectation users have.
53pub fn apply_strict(receipt: &mut ContainerProofReceipt) {
54    for subject in &mut receipt.subjects {
55        for check in &mut subject.checks {
56            if check.status == crate::model::CheckStatus::Warn {
57                check.status = crate::model::CheckStatus::Fail;
58            }
59        }
60        subject.finalize();
61    }
62    for check in &mut receipt.checks {
63        if check.status == crate::model::CheckStatus::Warn {
64            check.status = crate::model::CheckStatus::Fail;
65        }
66    }
67    // Recompute summary.
68    let mut summary = crate::model::ReceiptSummary::from_checks(&receipt.checks);
69    for subject in &receipt.subjects {
70        summary.add(&subject.summary);
71    }
72    receipt.summary = summary;
73    receipt.compatibility = receipt
74        .subjects
75        .iter()
76        .map(|subject| subject.classification)
77        .fold(
78            crate::model::Compatibility::Exact,
79            crate::model::Compatibility::worse,
80        );
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::model::{Compatibility, RunnerOs};
87
88    #[test]
89    fn run_plan_job_smoke() {
90        let receipt = run_plan_job(&JobPlanInput {
91            job_id: "build".to_owned(),
92            runner_os: RunnerOs::Linux,
93            runs_on: vec!["ubuntu-22.04".to_owned()],
94            container_image: Some("node:22-bookworm".to_owned()),
95            env: Vec::new(),
96            ports: Vec::new(),
97            volumes: Vec::new(),
98            options: String::new(),
99            credentials_username_present: false,
100            credentials_password_present: false,
101            location: None,
102        })
103        .unwrap();
104        assert_eq!(receipt.mode, "plan-job");
105        assert_eq!(receipt.subjects.len(), 1);
106        assert!(receipt.compatibility != Compatibility::Unsupported);
107    }
108
109    #[test]
110    fn apply_strict_promotes_warnings() {
111        let mut receipt = run_plan_job(&JobPlanInput {
112            job_id: "build".to_owned(),
113            runner_os: RunnerOs::Linux,
114            runs_on: vec!["ubuntu-22.04".to_owned()],
115            container_image: Some("node:latest".to_owned()),
116            env: Vec::new(),
117            ports: Vec::new(),
118            volumes: Vec::new(),
119            options: String::new(),
120            credentials_username_present: false,
121            credentials_password_present: false,
122            location: None,
123        })
124        .unwrap();
125        assert!(receipt.summary.warnings >= 1);
126        apply_strict(&mut receipt);
127        assert_eq!(receipt.summary.warnings, 0);
128        assert!(receipt.summary.failed >= 1);
129    }
130}