mod common;
#[path = "release_config/mutation_cleanup.rs"]
mod mutation_cleanup;
#[path = "release_config/mutation_policy.rs"]
mod mutation_policy;
fn dist_config() -> String {
common::without_comments("dist-workspace.toml")
}
fn rust_workflow() -> String {
common::without_comments(".github/workflows/rust.yml")
}
fn release_workflow() -> String {
common::without_comments(".github/workflows/release.yml")
}
fn release_build_setup() -> String {
common::without_comments(".github/build-setup.yml")
}
fn parsed_toml(relative: &str) -> toml::Table {
toml::from_str(&common::read(relative)).unwrap_or_else(|e| panic!("{relative} must parse: {e}"))
}
fn parsed_yaml(relative: &str) -> serde_yaml_ng::Value {
serde_yaml_ng::from_str(&common::read(relative))
.unwrap_or_else(|e| panic!("{relative} must parse: {e}"))
}
fn workflow_job<'a>(workflow: &'a str, name: &str) -> &'a str {
let marker = format!("\n {name}:\n");
let start = workflow
.find(&marker)
.unwrap_or_else(|| panic!("workflow must declare a {name} job"))
+ marker.len();
let tail = &workflow[start..];
let end = tail
.match_indices('\n')
.find_map(|(offset, _)| {
let next_line = &tail[offset + 1..];
(next_line.starts_with(" ") && !next_line.starts_with(" ")).then_some(offset)
})
.unwrap_or(tail.len());
&tail[..end]
}
const SAME_REPOSITORY_PR_GUARD: &str = "github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository";
const RUST_TOOLCHAIN_ACTION: &str =
"dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c";
const SETUP_ZIG_ACTION: &str = "mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29";
const INSTALL_ACTION: &str = "taiki-e/install-action@1ed6d7be6168f6c9046541087ff549b6bc581fdf";
const CARGO_ZIGBUILD_TOOL: &str = "cargo-zigbuild@0.23.3";
struct ReleaseTarget {
triple: &'static str,
runner: &'static str,
host: Option<&'static str>,
}
const RELEASE_TARGETS: [ReleaseTarget; 4] = [
ReleaseTarget {
triple: "aarch64-apple-darwin",
runner: "drep-macos",
host: None,
},
ReleaseTarget {
triple: "aarch64-unknown-linux-gnu",
runner: "drep-linux",
host: Some("x86_64-unknown-linux-gnu"),
},
ReleaseTarget {
triple: "x86_64-apple-darwin",
runner: "drep-macos",
host: None,
},
ReleaseTarget {
triple: "x86_64-unknown-linux-gnu",
runner: "drep-linux",
host: None,
},
];
#[test]
fn github_ci_uses_only_guarded_homelab_runners() {
let workflow = rust_workflow();
assert!(
workflow.contains("permissions:\n contents: read"),
"validation must explicitly request read-only repository contents"
);
let expected_guard = format!(" if: {SAME_REPOSITORY_PR_GUARD}");
for (job_name, runner) in [
("linux", "[self-hosted, linux, x64, drep-linux]"),
("test-macos", "[self-hosted, macos, arm64, drep-macos]"),
] {
let job = workflow_job(&workflow, job_name);
assert!(
job.contains(&format!("runs-on: {runner}")),
"{job_name} must run on its repository-scoped homelab runner"
);
assert!(
job.lines().any(|line| line == expected_guard),
"{job_name} must reject forked pull requests before using a homelab runner"
);
assert!(
job.contains("uses: Swatinem/rust-cache@") && job.contains("cache-bin: false"),
"{job_name} must not let rust-cache delete runner-provisioned Cargo binaries"
);
}
let linux = workflow_job(&workflow, "linux");
assert!(
linux.contains("cargo fmt --all --check")
&& linux.contains("cargo clippy --all-targets --all-features")
&& linux.contains("cargo test --all-targets --all-features")
&& linux.contains("cargo +1.88.0 check"),
"the single homelab-1 lane must retain format, clippy, test and MSRV gates"
);
let macos = workflow_job(&workflow, "test-macos");
assert!(
macos.contains("components: clippy"),
"the Mac test lane must install clippy because the suite exercises configured Rust compilers"
);
assert!(
!workflow.contains("\n lint:\n")
&& !workflow.contains("\n test-linux:\n")
&& !workflow.contains("\n msrv:\n"),
"serial homelab-1 validation must not repeat runner and checkout setup across jobs"
);
assert!(
!workflow.contains("ubuntu-latest") && !workflow.contains("macos-latest"),
"validation must not consume GitHub-hosted runner minutes"
);
}
#[test]
fn maintained_actions_are_pinned_to_full_commit_shas() {
let assert_pinned = |path| {
for line in common::read(path).lines() {
let trimmed = line.trim_start();
let action = trimmed
.strip_prefix("- uses: ")
.or_else(|| trimmed.strip_prefix("uses: "));
let Some(action) = action.filter(|action| !action.starts_with("./")) else {
continue;
};
let sha = action
.split_once('@')
.expect("action reference")
.1
.split_whitespace()
.next()
.expect("action revision");
assert!(
sha.len() == 40 && sha.bytes().all(|byte| byte.is_ascii_hexdigit()),
"{path}: action is not pinned to a full commit SHA: {action}"
);
}
};
for path in [
".github/workflows/rust.yml",
".github/workflows/mutants.yml",
".github/build-setup.yml",
] {
assert_pinned(path);
}
}
#[test]
fn every_supported_platform_is_built() {
let manifest = parsed_toml("dist-workspace.toml");
let targets = manifest["dist"]["targets"]
.as_array()
.expect("cargo-dist targets must be an array");
for target in RELEASE_TARGETS {
assert!(
targets
.iter()
.any(|value| value.as_str() == Some(target.triple)),
"a release no longer builds for {}",
target.triple
);
}
}
#[test]
fn every_release_job_uses_a_homelab_runner() {
let manifest = parsed_toml("dist-workspace.toml");
let dist = manifest["dist"]
.as_table()
.expect("cargo-dist configuration must be a table");
let runners = dist["github-custom-runners"]
.as_table()
.expect("cargo-dist custom runners must be a table");
for target in RELEASE_TARGETS {
match target.host {
Some(host) => {
let mapping = runners[target.triple]
.as_table()
.expect("a cross-runner mapping must be a table");
assert_eq!(mapping["runner"].as_str(), Some(target.runner));
assert_eq!(mapping["host"].as_str(), Some(host));
}
None => assert_eq!(runners[target.triple].as_str(), Some(target.runner)),
}
}
assert_eq!(runners["global"].as_str(), Some("drep-linux"));
assert_eq!(dist["pr-run-mode"].as_str(), Some("skip"));
let workflow = release_workflow();
let trigger = workflow
.split_once("\njobs:")
.map(|(trigger, _)| trigger)
.expect("the release workflow must declare jobs");
assert!(
!trigger.contains("pull_request"),
"the generated release workflow must be tag-only"
);
assert!(
workflow.contains("runs-on: \"drep-linux\"")
&& workflow.contains("runs-on: ${{ matrix.runner }}"),
"global jobs and target builds must use cargo-dist's homelab runner mapping"
);
assert!(
!workflow.lines().any(|line| {
let line = line.trim();
line.starts_with("runs-on:")
&& (line.contains("ubuntu-")
|| line.contains("macos-")
|| line.contains("windows-"))
}),
"release.yml must not contain a GitHub-hosted runs-on label"
);
}
#[test]
fn arm64_linux_cross_build_tools_are_reproducibly_provisioned() {
let manifest = parsed_toml("dist-workspace.toml");
assert_eq!(
manifest["dist"]["github-build-setup"].as_str(),
Some("../build-setup.yml"),
"cargo-dist must inject the repository-owned cross-build setup"
);
let setup = release_build_setup();
for target in RELEASE_TARGETS
.iter()
.filter(|target| target.host.is_some())
{
let condition = format!("contains(matrix.targets, '{}')", target.triple);
assert!(
setup.contains(&condition),
"the cross-build setup must select {}",
target.triple
);
}
assert!(
setup.contains(&format!("uses: {SETUP_ZIG_ACTION}"))
&& setup.contains("version: 0.16.0")
&& setup.contains(&format!("uses: {INSTALL_ACTION}"))
&& setup.contains(&format!("tool: {CARGO_ZIGBUILD_TOOL}")),
"only the arm64 Linux lane must install the pinned Zig cross-build tools"
);
let workflow = release_workflow();
let local_build = workflow_job(&workflow, "build-local-artifacts");
assert!(
local_build.contains(SETUP_ZIG_ACTION)
&& local_build.contains(INSTALL_ACTION)
&& local_build.contains(CARGO_ZIGBUILD_TOOL),
"the generated release workflow must include the configured cross-build setup"
);
}
#[test]
fn macos_release_builds_provision_their_rust_targets() {
let setup = release_build_setup();
assert!(
setup.contains("if: runner.os == 'macOS'")
&& setup.contains(&format!("uses: {RUST_TOOLCHAIN_ACTION}"))
&& setup.contains("toolchain: stable")
&& setup.contains("targets: ${{ join(matrix.targets, ',') }}"),
"Mac release builds must not depend on runner-global Rust PATH state"
);
let workflow = release_workflow();
let local_build = workflow_job(&workflow, "build-local-artifacts");
assert!(
local_build.contains(RUST_TOOLCHAIN_ACTION)
&& local_build.contains("join(matrix.targets, ',')"),
"the generated release workflow must include the Mac Rust bootstrap"
);
}
#[test]
fn linux_release_tls_is_self_contained_for_cross_compilation() {
let manifest = parsed_toml("Cargo.toml");
let native_features = manifest["dependencies"]["reqwest"]["features"]
.as_array()
.expect("reqwest features must be an array");
assert!(
native_features
.iter()
.all(|feature| feature.as_str() != Some("native-tls-vendored")),
"native targets must not compile a vendored OpenSSL"
);
let cross_features = manifest["target"]
["cfg(all(target_os = \"linux\", target_arch = \"aarch64\"))"]["dependencies"]
["reqwest"]["features"]
.as_array()
.expect("arm64 Linux reqwest features must be an array");
assert!(
cross_features
.iter()
.any(|feature| feature.as_str() == Some("native-tls-vendored")),
"Linux release builds must compile OpenSSL instead of depending on a target sysroot"
);
}
#[test]
fn the_homebrew_formula_is_pushed_to_the_tap() {
let config = dist_config();
assert!(
config.contains(r#"installers = ["shell", "homebrew"]"#),
"a release must publish both the shell installer and the formula"
);
assert!(
config.contains(r#"tap = "slb350/homebrew-tap""#),
"the formula has no tap to be pushed to"
);
assert!(
config.contains(r#"publish-jobs = ["homebrew"]"#),
"without the homebrew publish job the formula is built and never pushed"
);
assert!(
config.contains(r#"formula = "drep""#),
"the formula must be named for the binary, not for the crate"
);
}
#[test]
fn the_pinned_dist_version_is_the_one_ci_installs() {
let config = dist_config();
let pinned = config
.lines()
.find_map(|line| line.strip_prefix("cargo-dist-version = "))
.expect("dist-workspace.toml must pin a dist version")
.trim()
.trim_matches('"');
let workflow = release_workflow();
assert!(
workflow.contains(&format!("cargo-dist/releases/download/v{pinned}/")),
"release.yml installs a different dist than the config pins ({pinned}) - run `dist init`"
);
}
#[test]
fn release_workflow_transfers_artifacts_between_jobs() {
let workflow = release_workflow();
assert!(
workflow.contains("uses: actions/upload-artifact@"),
"release.yml must upload build artifacts"
);
assert!(
workflow.contains("uses: actions/download-artifact@"),
"release.yml must download artifacts between jobs"
);
}
#[test]
fn release_workflow_needs_no_manual_ci_exception() {
let config = dist_config();
assert!(
!config.contains("allow-dirty"),
"the generated workflow must not require a hand-maintained CI exception"
);
}
#[test]
fn dependabot_excludes_the_generated_release_workflow() {
let config = parsed_yaml(".github/dependabot.yml");
let updates = config["updates"]
.as_sequence()
.expect("Dependabot must declare update entries");
let excludes_generated_workflow = updates
.iter()
.filter_map(|update| update["exclude-paths"].as_sequence())
.flatten()
.any(|path| path.as_str() == Some(".github/workflows/release.yml"));
assert!(
excludes_generated_workflow,
"Dependabot must leave cargo-dist's generated release.yml to dist init"
);
}
#[test]
fn released_binaries_inherit_the_tuned_release_profile() {
let manifest = parsed_toml("Cargo.toml");
let profile = manifest["profile"]
.get("dist")
.and_then(toml::Value::as_table)
.expect("Cargo.toml must declare the profile dist builds with");
assert_eq!(
profile.keys().map(String::as_str).collect::<Vec<_>>(),
["inherits"],
"[profile.dist] must add nothing to [profile.release]"
);
assert_eq!(profile["inherits"].as_str(), Some("release"));
}