testing_conventions/
e2e.rs1use std::path::Path;
11use std::process::Command;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use anyhow::{bail, Context, Result};
15use serde::{Deserialize, Serialize};
16
17pub const ATTESTATION_PATH: &str = "e2e-attestation.json";
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Attestation {
27 pub command: String,
29 pub ran_at: u64,
31 pub exit_code: i32,
33 pub commit: String,
35}
36
37pub 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 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 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
92fn 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
109fn 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}