use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;
fn workflows_directory() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join(".github")
.join("workflows")
}
fn workflow_paths() -> Vec<PathBuf> {
let directory = workflows_directory();
let mut paths: Vec<PathBuf> = fs::read_dir(&directory)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", directory.display()))
.map(|entry| {
entry
.unwrap_or_else(|error| {
panic!("failed to inspect {}: {error}", directory.display())
})
.path()
})
.filter(|path| {
path.is_file()
&& matches!(
path.extension().and_then(|extension| extension.to_str()),
Some("yml" | "yaml")
)
})
.collect();
paths.sort();
paths
}
fn external_action_refs() -> Vec<(String, String)> {
let mut refs = Vec::new();
for path in workflow_paths() {
let workflow = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_else(|| panic!("workflow path has no UTF-8 file name: {}", path.display()));
let source = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
for line in source.lines() {
let trimmed = line.trim_start();
let value = trimmed
.strip_prefix("uses: ")
.or_else(|| trimmed.strip_prefix("- uses: "));
if let Some(action_ref) = value.filter(|value| !value.starts_with("./")) {
refs.push((workflow.to_owned(), action_ref.trim().to_string()));
}
}
}
refs
}
#[test]
fn external_workflow_actions_use_the_supported_node_24_majors() {
let refs = external_action_refs();
assert!(
!refs.is_empty(),
"the external-action inventory parsed no entries; the test must not pass vacuously"
);
let allowed = BTreeSet::from([
"actions/cache@v6",
"actions/checkout@v7",
"actions/download-artifact@v8",
"actions/setup-node@v7",
"actions/upload-artifact@v7",
"Swatinem/rust-cache@v2",
"taiki-e/install-action@nextest",
"mozilla-actions/sccache-action@v0.0.11",
"rust-lang/crates-io-auth-action@v1",
]);
for (workflow, action_ref) in &refs {
assert!(
allowed.contains(action_ref.as_str()),
"{workflow} uses `{action_ref}`, which is not one of the audited Node 24 action majors; update the official-action audit and this allow-list together"
);
}
let found: BTreeSet<&str> = refs
.iter()
.map(|(_, action_ref)| action_ref.as_str())
.collect();
assert_eq!(
found, allowed,
"the audited action inventory changed; removing an action must not make its version assertion disappear unnoticed"
);
}
#[test]
fn nextest_installation_is_isolated_between_ephemeral_runners() {
let path = workflows_directory().join("ci.yml");
let source = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()))
.replace("\r\n", "\n");
let isolated_install = r#"uses: taiki-e/install-action@nextest
# install-action uses ~/.install-action/tmp for downloads. Multiple
# ephemeral runners on one physical Windows host share the account's
# real HOME, so concurrent CI/release jobs otherwise overwrite the
# same `tmp` file and one of their checksum checks fails. Both values
# are needed: main.sh reads HOME, while the action's Windows wrapper
# looks for its startup sentinel below USERPROFILE.
env:
HOME: ${{ runner.temp }}/install-action-home
USERPROFILE: ${{ runner.temp }}/install-action-home"#;
assert!(
source.contains(isolated_install),
"ci.yml must give install-action a per-runner HOME and USERPROFILE; its default ~/.install-action/tmp is shared by concurrent ephemeral runners on one Windows account"
);
}