use std::path::Path;
fn narrowed_deny_calls(name: &str, text: &str) -> Vec<String> {
let mut found = Vec::new();
for (n, line) in text.lines().enumerate() {
let Some(at) = line.find("cargo deny").or_else(|| line.find("cargo-deny")) else { continue };
let words: Vec<&str> = line[at..].split_whitespace().collect();
let Some(check) = words.iter().position(|w| *w == "check") else { continue };
let named: Vec<&str> = words[check + 1..].iter().copied().filter(|w| !w.starts_with('-')).collect();
if !named.is_empty() && ["advisories", "bans", "licenses"].iter().any(|c| !named.contains(c)) {
found.push(format!("{name}:{}: {} runs only {}", n + 1, line.trim(), named.join(" ")));
}
}
found
}
fn workflows() -> Vec<(String, String)> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(".github/workflows");
let mut found: Vec<(String, String)> = std::fs::read_dir(&dir)
.expect("reading .github/workflows")
.map(|e| e.expect("a workflow entry").path())
.filter(|p| p.extension().is_some_and(|x| x == "yml" || x == "yaml"))
.map(|p| {
(
p.file_name().expect("a file name").to_string_lossy().into_owned(),
std::fs::read_to_string(&p).expect("reading a workflow"),
)
})
.collect();
found.sort();
found
}
#[test]
fn no_cargo_deny_call_is_narrowed_past_advisories() {
let all = workflows();
assert!(all.iter().any(|(n, _)| n == "ci.yml"), "found no ci.yml among {:?}", all.iter().map(|w| &w.0));
let found: Vec<String> = all.iter().flat_map(|(n, t)| narrowed_deny_calls(n, t)).collect();
assert!(found.is_empty(), "run a bare `cargo deny check` instead:\n{}", found.join("\n"));
}
#[test]
fn ci_and_release_gate_on_cargo_deny_and_cargo_fmt() {
let all = workflows();
for name in ["ci.yml", "release.yml"] {
let (_, text) = all.iter().find(|(n, _)| n == name).unwrap_or_else(|| panic!("no {name}"));
assert!(text.contains("run: cargo deny check"), "{name} runs no cargo deny check");
for gate in ["cargo fmt --all --check\n", "cargo fmt --all --check --manifest-path fuzz/Cargo.toml"] {
assert!(text.contains(gate), "{name} does not run {}", gate.trim());
}
}
}
#[test]
fn a_licenses_only_call_is_flagged() {
let found = narrowed_deny_calls("ci.yml", "steps:\n - run: cargo deny check licenses\n");
assert_eq!(found, vec!["ci.yml:2: - run: cargo deny check licenses runs only licenses"]);
}
#[test]
fn a_call_missing_advisories_is_flagged() {
assert_eq!(narrowed_deny_calls("x", "run: cargo deny --locked check bans licenses").len(), 1);
}
#[test]
fn bare_and_complete_calls_pass() {
assert!(narrowed_deny_calls("x", "run: cargo deny check").is_empty());
assert!(narrowed_deny_calls("x", "run: cargo deny check advisories bans licenses").is_empty());
assert!(narrowed_deny_calls("x", "run: cargo deny check --hide-inclusion-graph").is_empty());
assert!(narrowed_deny_calls("x", "- uses: taiki-e/install-action@cargo-deny").is_empty());
}
#[test]
fn the_hyphenated_binary_is_checked_too() {
assert_eq!(narrowed_deny_calls("x", "run: cargo-deny check licenses").len(), 1);
}
fn top_level_block<'a>(text: &'a str, key: &str) -> Option<Vec<&'a str>> {
let mut lines = text.lines();
lines.by_ref().find(|l| l.trim_end() == format!("{key}:") || l.starts_with(&format!("{key}: ")))?;
Some(lines.take_while(|l| l.is_empty() || l.starts_with(' ') || l.starts_with('#')).collect())
}
fn jobs(text: &str) -> Vec<(String, Vec<String>)> {
let mut found: Vec<(String, Vec<String>)> = Vec::new();
for line in top_level_block(text, "jobs").unwrap_or_default() {
let indent = line.len() - line.trim_start().len();
let Some((key, _)) = line.trim().split_once(':') else { continue };
if line.trim_start().starts_with('#') || key.starts_with('-') {
continue;
}
match indent {
2 => found.push((key.to_string(), Vec::new())),
4 => {
if let Some(job) = found.last_mut() {
job.1.push(key.to_string());
}
}
_ => {}
}
}
found
}
fn unbounded(name: &str, text: &str) -> Vec<String> {
let mut found = Vec::new();
if !top_level_block(text, "concurrency").is_some_and(|b| b.iter().any(|l| l.trim().starts_with("group:"))) {
found.push(format!("{name}: no top-level concurrency group"));
}
if top_level_block(text, "permissions").is_none() {
found.push(format!("{name}: no top-level permissions"));
}
let all = jobs(text);
if all.is_empty() {
found.push(format!("{name}: no jobs found"));
}
for (job, keys) in all {
if !keys.iter().any(|k| k == "timeout-minutes" || k == "uses") {
found.push(format!("{name}: job {job} sets no timeout-minutes"));
}
}
found
}
#[test]
fn every_workflow_is_bounded() {
let all = workflows();
assert!(all.len() >= 5, "found only {:?}", all.iter().map(|w| &w.0).collect::<Vec<_>>());
let found: Vec<String> = all.iter().flat_map(|(n, t)| unbounded(n, t)).collect();
assert!(
found.is_empty(),
"set these (agents/dependencies-and-ci.md, \"CI jobs are bounded\"):\n{}",
found.join("\n")
);
}
#[test]
fn a_bounded_workflow_passes() {
let text = concat!(
"name: X\non:\n push:\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n",
"permissions:\n contents: read\njobs:\n a:\n runs-on: ubuntu-latest\n timeout-minutes: 5\n",
" steps:\n - run: echo timeout-minutes: 1\n b:\n uses: ./.github/workflows/y.yml\n",
);
assert_eq!(unbounded("x.yml", text), Vec::<String>::new());
assert_eq!(
jobs(text),
vec![
("a".into(), vec!["runs-on".into(), "timeout-minutes".into(), "steps".into()]),
("b".into(), vec!["uses".into()])
]
);
}
#[test]
fn an_unbounded_workflow_is_caught() {
let text = "name: X\nconcurrency: fuzz\njobs:\n a:\n runs-on: ubuntu-latest\n steps:\n - run: x\n timeout-minutes: 5\n";
assert_eq!(
unbounded("x.yml", text),
vec![
"x.yml: no top-level concurrency group",
"x.yml: no top-level permissions",
"x.yml: job a sets no timeout-minutes"
]
);
assert_eq!(
unbounded("y.yml", "name: Y\n"),
vec!["y.yml: no top-level concurrency group", "y.yml: no top-level permissions", "y.yml: no jobs found"]
);
}
#[test]
fn the_toolchain_is_pinned_and_every_workflow_installs_the_pin() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let pin = std::fs::read_to_string(root.join("rust-toolchain.toml")).expect("rust-toolchain.toml");
let table: toml::Table = pin.parse().expect("rust-toolchain.toml parses");
let channel = table["toolchain"]["channel"].as_str().expect("a channel");
let parts: Vec<&str> = channel.split('.').collect();
assert!(
parts.len() == 3 && parts.iter().all(|p| p.parse::<u32>().is_ok()),
"channel {channel:?} is not an exact x.y.z"
);
let components = table["toolchain"]["components"].as_array().expect("components");
for wanted in ["clippy", "rustfmt"] {
assert!(components.iter().any(|c| c.as_str() == Some(wanted)), "rust-toolchain.toml lacks {wanted}");
}
for (name, text) in workflows() {
for moving in ["dtolnay/rust-toolchain@stable", "dtolnay/rust-toolchain@beta"] {
assert!(!text.contains(moving), "{name} installs {moving} over the pin; run `rustup toolchain install`");
}
}
}
#[test]
fn dependencies_are_kept_current_by_the_upkeep_job() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
for name in ["dependabot.yml", "dependabot.yaml"] {
assert!(!root.join(".github").join(name).exists(), ".github/{name}: the workon-deps upkeep job does this");
}
let agents = std::fs::read_to_string(root.join("AGENTS.md")).expect("AGENTS.md");
assert!(agents.contains("`workon-deps`"), "AGENTS.md does not name the workon-deps upkeep job");
}
#[test]
fn ci_builds_docs_checks_unused_dependencies_and_the_msrv() {
let all = workflows();
let (_, ci) = all.iter().find(|(n, _)| n == "ci.yml").expect("ci.yml");
assert!(
ci.contains("run: cargo doc --locked --no-deps") && ci.contains("RUSTDOCFLAGS: -D warnings"),
"ci.yml builds no docs with warnings denied"
);
assert!(ci.contains("run: cargo machete"), "ci.yml runs no cargo machete");
assert!(ci.contains("check --locked") && ci.contains("rust-version"), "ci.yml checks no MSRV build");
}