Skip to main content

testing_conventions/
e2e.rs

1//! `e2e attest` / `e2e verify` (#17) — the e2e attestation nudge.
2//!
3//! `attest` runs the e2e suite locally and records that it ran against the
4//! current commit; `verify` (a later slice, #68) confirms in CI that the latest
5//! code commit is attested. The point is to *nudge* agents to run e2e locally —
6//! CI never runs e2e, it only checks the committed attestation.
7//!
8//! This slice (#67) implements `attest`; `verify` is a later slice (#68).
9
10use std::path::Path;
11use std::process::Command;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use anyhow::{bail, Context, Result};
15use serde::{Deserialize, Serialize};
16
17/// Where the committed attestation lives, relative to the repo root.
18pub const ATTESTATION_PATH: &str = "e2e-attestation.json";
19
20/// A record of one local e2e run — written to disk and committed by [`attest`].
21///
22/// `commit` is the SHA of the code commit the run was made against (HEAD at
23/// attest time); [`verify`](crate::e2e) (#68) checks it against the latest code
24/// commit. The rest is information for humans — nothing is gated on it.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Attestation {
27    /// The command that was run (e.g. `pnpm run e2e`).
28    pub command: String,
29    /// When it ran, as a Unix timestamp (seconds).
30    pub ran_at: u64,
31    /// The command's exit code — recorded, never gated on.
32    pub exit_code: i32,
33    /// The commit the run was made against (HEAD at attest time).
34    pub commit: String,
35}
36
37/// Run `command` in `repo`, write an [`Attestation`] naming the current HEAD to
38/// `repo`/[`ATTESTATION_PATH`], and commit it on top. Returns the attestation.
39///
40/// Writes regardless of the command's exit code — this forces a *run*, not a
41/// *pass*.
42pub fn attest(repo: &Path, command: &str) -> Result<Attestation> {
43    let commit = git_capture(repo, &["rev-parse", "HEAD"])
44        .context("resolving HEAD — `e2e attest` must run inside a git repo with a commit")?;
45
46    // Run the e2e command via the shell, streaming its output through.
47    let status = Command::new("sh")
48        .arg("-c")
49        .arg(command)
50        .current_dir(repo)
51        .status()
52        .with_context(|| format!("running e2e command `{command}`"))?;
53    let exit_code = status.code().unwrap_or(-1);
54
55    let ran_at = SystemTime::now()
56        .duration_since(UNIX_EPOCH)
57        .map(|d| d.as_secs())
58        .unwrap_or(0);
59
60    let attestation = Attestation {
61        command: command.to_string(),
62        ran_at,
63        exit_code,
64        commit: commit.clone(),
65    };
66
67    // Write the attestation, then commit just that file on top — it names the
68    // code commit it was run against (a commit can't name its own SHA).
69    let path = repo.join(ATTESTATION_PATH);
70    let json = serde_json::to_string_pretty(&attestation).context("serializing the attestation")?;
71    std::fs::write(&path, format!("{json}\n"))
72        .with_context(|| format!("writing {}", path.display()))?;
73
74    git_run(repo, &["add", ATTESTATION_PATH])?;
75    let short = &commit[..commit.len().min(7)];
76    let message = format!("e2e attestation for {short}");
77    git_run(
78        repo,
79        &[
80            "-c",
81            "commit.gpgsign=false",
82            "commit",
83            "-q",
84            "-m",
85            message.as_str(),
86        ],
87    )?;
88
89    Ok(attestation)
90}
91
92/// Run `git` with `args` in `repo`, returning trimmed stdout; errors if git fails.
93fn git_capture(repo: &Path, args: &[&str]) -> Result<String> {
94    let out = Command::new("git")
95        .args(args)
96        .current_dir(repo)
97        .output()
98        .with_context(|| format!("running `git {}`", args.join(" ")))?;
99    if !out.status.success() {
100        bail!(
101            "`git {}` failed: {}",
102            args.join(" "),
103            String::from_utf8_lossy(&out.stderr).trim()
104        );
105    }
106    Ok(String::from_utf8(out.stdout)?.trim().to_string())
107}
108
109/// Run `git` with `args` in `repo` for its side effect; errors if git fails.
110fn git_run(repo: &Path, args: &[&str]) -> Result<()> {
111    let status = Command::new("git")
112        .args(args)
113        .current_dir(repo)
114        .status()
115        .with_context(|| format!("running `git {}`", args.join(" ")))?;
116    if !status.success() {
117        bail!("`git {}` failed", args.join(" "));
118    }
119    Ok(())
120}