xtask/tests/supply_chain.rs
//! **What CI is allowed to be, asserted from inside the tree.**
//!
//! The console's CI checked formatting, lints, tests and a bundle size, and
//! nothing at all about the supply chain it does that with. Three properties
//! were missing, and each is the kind that is easy to add once and lose in the
//! next edit — so they are asserted here rather than remembered:
//!
//! * **a dependency gate exists.** `deny.toml` at the root and a job that runs
//! `cargo deny` over it. gunnar has had this since 2026-08-31; the console,
//! which is the half facing the public internet, had neither;
//! * **actions are pinned by commit.** `actions/checkout@v4` is a pointer the
//! action's owner can move; a compromised release moves under this workflow
//! without a commit in this repository;
//! * **the workflow declares `permissions:`.** Without it `GITHUB_TOKEN`
//! carries the repository's default, which on an older repository is write,
//! handed to every step including `cargo install` and every build script in
//! the graph.
//!
//! # Why this counts what it found
//!
//! A scanner that matches nothing passes. If the `uses:` lines were renamed, or
//! the workflow deleted, a bare "no unpinned action was found" would be green
//! on an empty file — the same disease `readme_version.rs` names. So the census
//! is asserted to be non-zero and the count is printed in the failure.
use std::path::{Path, PathBuf};
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask/ has a parent")
.to_path_buf()
}
/// Every workflow file under `.github/workflows`, as (name, contents).
fn workflows(root: &Path) -> Vec<(String, String)> {
let dir = root.join(".github/workflows");
let mut out = Vec::new();
for entry in std::fs::read_dir(&dir).expect("`.github/workflows` is there") {
let path = entry.expect("a directory entry").path();
let is_yaml = matches!(
path.extension().and_then(|e| e.to_str()),
Some("yml") | Some("yaml")
);
if is_yaml {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_owned();
out.push((name, std::fs::read_to_string(&path).expect("a workflow")));
}
}
assert!(
!out.is_empty(),
"no workflow files: this guard found nothing"
);
out
}
/// The reference of every `uses:` line: what comes after the last `@`.
///
/// Comments are cut first, so the `# v4.4.0` that names the release a digest
/// belongs to does not become part of the reference.
fn used_actions(yaml: &str) -> Vec<(usize, String)> {
let mut out = Vec::new();
for (n, line) in yaml.lines().enumerate() {
let trimmed = line.trim_start().trim_start_matches("- ").trim_start();
let Some(rest) = trimmed.strip_prefix("uses:") else {
continue;
};
let value = rest.split('#').next().unwrap_or_default().trim();
if value.is_empty() {
continue;
}
out.push((n + 1, value.to_owned()));
}
out
}
fn is_forty_hex(s: &str) -> bool {
s.len() == 40 && s.bytes().all(|b| b.is_ascii_hexdigit())
}
/// ★ **No action runs from a pointer its owner can move.**
#[test]
fn every_action_is_pinned_to_a_commit() {
let root = workspace_root();
let mut census = 0usize;
let mut floating: Vec<String> = Vec::new();
for (name, yaml) in workflows(&root) {
for (line, value) in used_actions(&yaml) {
census += 1;
// A local action — `./.github/actions/x` — is this repository's own
// tree and is already pinned by the commit being built.
if value.starts_with("./") {
continue;
}
match value.rsplit_once('@') {
Some((_, reference)) if is_forty_hex(reference) => {}
_ => floating.push(format!("{name}:{line} {value}")),
}
}
}
assert!(
census > 0,
"no `uses:` line was found at all, so this guard proved nothing"
);
assert!(
floating.is_empty(),
"{} of {census} actions are pinned by a MOVING reference. A tag is a \
pointer the action's owner can move, so a coerced release runs here \
without a commit in this repository. Pin the commit and name the \
version in a trailing comment:\n {}",
floating.len(),
floating.join("\n ")
);
}
/// ★ **Every job's `GITHUB_TOKEN` is narrowed, at the top of the workflow.**
///
/// Top-level and not per job: a job added tomorrow inherits it rather than
/// having to remember it, which is the property worth asserting.
#[test]
fn every_workflow_narrows_its_token() {
for (name, yaml) in workflows(&workspace_root()) {
let declared = yaml
.lines()
.any(|line| line.starts_with("permissions:") || line.starts_with("permissions :"));
assert!(
declared,
"{name} has no top-level `permissions:` block, so its GITHUB_TOKEN \
is the repository's default — handed to every step, including \
`cargo install` and every build script in the dependency graph"
);
}
}
/// ★ **The dependency gate exists and CI runs it.**
///
/// Both halves. A `deny.toml` nothing invokes is a document; a `cargo deny`
/// with no policy beside it is a different gate every time the tool's defaults
/// move.
#[test]
fn the_dependency_gate_exists_and_is_run() {
let root = workspace_root();
let policy = root.join("deny.toml");
assert!(
policy.is_file(),
"no deny.toml at the workspace root: gunnar has had one since \
2026-08-31 and this is the half of the estate that faces the public \
internet"
);
let policy = std::fs::read_to_string(&policy).expect("deny.toml reads");
assert!(
policy.contains("[advisories]"),
"deny.toml carries no [advisories] section, which is the CVE half"
);
let invoked = workflows(&root)
.iter()
.any(|(_, yaml)| yaml.contains("cargo deny") || yaml.contains("cargo audit"));
assert!(
invoked,
"no workflow runs `cargo deny` or `cargo audit`, so the policy beside \
them is a document rather than a gate"
);
}