#[test]
fn every_workflow_action_is_pinned_to_a_full_commit_sha() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workflows_dir = std::path::Path::new(manifest_dir).join(".github/workflows");
let mut files_scanned = 0usize;
let mut uses_lines_checked = 0usize;
let entries = std::fs::read_dir(&workflows_dir)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", workflows_dir.display()));
for entry in entries {
let entry = entry.unwrap_or_else(|e| panic!("failed to read directory entry: {e}"));
let path = entry.path();
let ext = path.extension().and_then(|e| e.to_str());
if ext != Some("yml") && ext != Some("yaml") {
continue;
}
files_scanned += 1;
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
for (line_num, line) in content.lines().enumerate() {
if !line.contains("uses:") {
continue;
}
uses_lines_checked += 1;
let before_comment = line.split('#').next().unwrap_or(line);
let after_at = before_comment.rsplit_once('@').unwrap_or_else(|| {
panic!(
"{}:{}: `uses:` line has no `@` ref separator: {line:?}",
path.display(),
line_num + 1
)
});
let ref_str = after_at.1.trim();
let is_40_hex = ref_str.len() == 40 && ref_str.chars().all(|c| c.is_ascii_hexdigit());
assert!(
is_40_hex,
"{}:{}: action ref {:?} is not a full 40-hex commit SHA (line: {:?})",
path.display(),
line_num + 1,
ref_str,
line
);
}
}
assert!(
files_scanned >= 1,
"no workflow files found under {}; glob is broken",
workflows_dir.display()
);
assert!(
uses_lines_checked >= 1,
"no `uses:` lines found across scanned workflow files; scan is broken"
);
}