use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
pub const ATTESTATION_PATH: &str = "e2e-attestation.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attestation {
pub command: String,
pub ran_at: u64,
pub exit_code: i32,
pub commit: String,
}
pub fn attest(repo: &Path, command: &str) -> Result<Attestation> {
let commit = git_capture(repo, &["rev-parse", "HEAD"])
.context("resolving HEAD — `e2e attest` must run inside a git repo with a commit")?;
let status = Command::new("sh")
.arg("-c")
.arg(command)
.current_dir(repo)
.status()
.with_context(|| format!("running e2e command `{command}`"))?;
let exit_code = status.code().unwrap_or(-1);
let ran_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let attestation = Attestation {
command: command.to_string(),
ran_at,
exit_code,
commit: commit.clone(),
};
let path = repo.join(ATTESTATION_PATH);
let json = serde_json::to_string_pretty(&attestation).context("serializing the attestation")?;
std::fs::write(&path, format!("{json}\n"))
.with_context(|| format!("writing {}", path.display()))?;
git_run(repo, &["add", ATTESTATION_PATH])?;
let short = &commit[..commit.len().min(7)];
let message = format!("e2e attestation for {short}");
git_run(repo, &["commit", "-q", "-m", message.as_str()])?;
Ok(attestation)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verification {
Fresh,
Missing,
Stale {
attested: String,
latest: String,
},
}
pub fn verify(repo: &Path) -> Result<Verification> {
verify_scoped(repo, repo)
}
pub fn verify_scoped(repo: &Path, scope: &Path) -> Result<Verification> {
verify_since(repo, scope, None)
}
pub fn verify_since(repo: &Path, scope: &Path, base: Option<&str>) -> Result<Verification> {
verify_extra_scoped(repo, scope, base, &[], &[])
}
pub fn verify_extra_scoped(
repo: &Path,
scope: &Path,
base: Option<&str>,
extra_scopes: &[PathBuf],
excludes: &[PathBuf],
) -> Result<Verification> {
let path = repo.join(ATTESTATION_PATH);
let Ok(contents) = std::fs::read_to_string(&path) else {
return Ok(Verification::Missing);
};
let attestation: Attestation =
serde_json::from_str(&contents).context("parsing the attestation")?;
match latest_code_commit(repo, scope, base, extra_scopes, excludes)? {
None if base.is_some() => Ok(Verification::Fresh),
latest => {
let latest = latest.unwrap_or_default();
if attestation.commit == latest {
Ok(Verification::Fresh)
} else {
Ok(Verification::Stale {
attested: attestation.commit,
latest,
})
}
}
}
}
fn latest_code_commit(
repo: &Path,
scope: &Path,
base: Option<&str>,
extra_scopes: &[PathBuf],
excludes: &[PathBuf],
) -> Result<Option<String>> {
let pathspec = relative_pathspec(repo, scope);
let mut args: Vec<String> = vec!["log".into(), "-1".into(), "--format=%H".into()];
if let Some(base) = base {
args.push(format!("{base}..HEAD"));
}
args.push("--".into());
args.push(pathspec);
for extra in extra_scopes {
args.push(format!(":(top){}", extra.display()));
}
args.push(format!(":(exclude){ATTESTATION_PATH}"));
for exclude in excludes {
args.push(format!(":(top,exclude){}", exclude.display()));
}
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let out = git_capture(repo, &arg_refs)?;
Ok((!out.is_empty()).then_some(out))
}
fn relative_pathspec(repo: &Path, scope: &Path) -> String {
if scope == repo {
return ".".to_string();
}
match scope.strip_prefix(repo) {
Ok(rel) if !rel.as_os_str().is_empty() => rel.to_string_lossy().into_owned(),
_ => scope.to_string_lossy().into_owned(),
}
}
fn git_capture(repo: &Path, args: &[&str]) -> Result<String> {
let out = Command::new("git")
.args(args)
.current_dir(repo)
.output()
.with_context(|| format!("running `git {}`", args.join(" ")))?;
if !out.status.success() {
bail!(
"`git {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(String::from_utf8(out.stdout)?.trim().to_string())
}
fn git_run(repo: &Path, args: &[&str]) -> Result<()> {
let status = Command::new("git")
.args(args)
.current_dir(repo)
.status()
.with_context(|| format!("running `git {}`", args.join(" ")))?;
if !status.success() {
bail!("`git {}` failed", args.join(" "));
}
Ok(())
}