Skip to main content

speccoder_runtime/pipeline/
full.rs

1//! The `full` check tier: once, after a run ticks a feature's last task,
2//! and recorded so `pr` can refuse a branch whose full checks failed.
3
4use std::path::Path;
5
6use speccoder_core::FullTier;
7
8use crate::git::Git;
9use crate::{attempts, sandbox, verify};
10
11/// Where the record of a feature's full tier lives.
12pub const PREFIX: &str = "refs/speccoder/full";
13
14/// Runs the `full` checks of `project` (in the worktree `wt_root`), confined
15/// like every other command.
16#[must_use]
17pub fn run_once(project: &Path, wt_root: &Path) -> FullTier {
18    let config = verify::find_config(project)
19        .and_then(|root| std::fs::read_to_string(root.join(verify::config::FILE)).ok())
20        .unwrap_or_default();
21    let runner = match sandbox::parse(&config).and_then(|c| sandbox::runner(wt_root, &c)) {
22        Ok((r, _)) => r,
23        Err(e) => {
24            return FullTier {
25                note: Some(format!("the full tier could not run: {e}")),
26                ..FullTier::default()
27            };
28        }
29    };
30    match verify::run_checks(project, Some(verify::config::Tier::Full), &runner) {
31        Ok(report) => FullTier {
32            ran: report.checks.iter().map(|c| c.name.clone()).collect(),
33            passed: report.passed,
34            failed: report
35                .checks
36                .iter()
37                .filter(|c| c.status.is_failure())
38                .map(|c| c.name.clone())
39                .collect(),
40            note: None,
41        },
42        // "No applicable checks": nothing in the full tier to fail.
43        Err(e) => FullTier {
44            passed: true,
45            note: Some(if e.starts_with("no applicable checks") {
46                "no full-tier checks declared".to_owned()
47            } else {
48                e
49            }),
50            ..FullTier::default()
51        },
52    }
53}
54
55/// Records `tier` for the branch head of `wt_root` under
56/// `refs/speccoder/full/<feature>`: a commit on the head's tree, its parent
57/// the head, its trailers the outcome.
58pub fn record(wt_root: &Path, feature: &str, tier: &FullTier) {
59    let git = Git::at(wt_root);
60    let (Ok(head), Ok(tree)) = (
61        git.line(&["rev-parse", "--verify", "HEAD^{commit}"]),
62        git.line(&["rev-parse", "--verify", "HEAD^{tree}"]),
63    ) else {
64        return;
65    };
66    let outcome = if tier.passed { "passed" } else { "failed" };
67    let message = format!(
68        "speccoder: full tier {outcome}\n\nTier: full\nOutcome: {outcome}\nRan: {}\n{}",
69        tier.ran.join(", "),
70        if tier.failed.is_empty() {
71            String::new()
72        } else {
73            format!("Failed: {}\n", tier.failed.join(", "))
74        }
75    );
76    let mut args = attempts::IDENTITY.to_vec();
77    args.extend(["commit-tree", &tree, "-p", &head, "-m", &message]);
78    if let Ok(commit) = git.line(&args) {
79        let _ = git.run(&["update-ref", &format!("{PREFIX}/{feature}"), &commit]);
80    }
81}
82
83/// The recorded full tier for `branch` of the repository at `repo_root`, if
84/// it was recorded for the branch's current head.
85#[must_use]
86pub fn read(repo_root: &Path, feature: &str, branch: &str) -> Option<FullTier> {
87    let git = Git::at(repo_root);
88    let reference = format!("{PREFIX}/{feature}");
89    let parent = git
90        .line(&["rev-parse", "--verify", &format!("{reference}^1")])
91        .ok()?;
92    let head = git
93        .line(&["rev-parse", "--verify", &format!("{branch}^{{commit}}")])
94        .ok()?;
95    if parent != head {
96        return None;
97    }
98    let body = git.line(&["log", "-1", "--format=%B", &reference]).ok()?;
99    let field = |key: &str| {
100        body.lines()
101            .find_map(|l| l.strip_prefix(key))
102            .map(|v| {
103                v.split(", ")
104                    .filter(|s| !s.is_empty())
105                    .map(str::to_owned)
106                    .collect::<Vec<_>>()
107            })
108            .unwrap_or_default()
109    };
110    Some(FullTier {
111        ran: field("Ran: "),
112        passed: body.contains("\nOutcome: passed"),
113        failed: field("Failed: "),
114        note: None,
115    })
116}