#![allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "tests and benches use unwrap and expect to keep fixture setup concise"
)]
use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/cli sits two levels below the repo root")
.to_path_buf()
}
fn package_name(manifest: &str) -> Option<String> {
manifest.lines().find_map(|line| {
let rest = line.trim().strip_prefix("name")?.trim_start();
let value = rest.strip_prefix('=')?.trim();
Some(value.trim_matches('"').to_string())
})
}
fn is_publish_false(manifest: &str) -> bool {
manifest.lines().any(|line| {
let t = line.trim();
t.starts_with("publish") && t.contains("false")
})
}
fn publishable_crates(root: &Path) -> BTreeSet<String> {
let mut set = BTreeSet::new();
for entry in fs::read_dir(root.join("crates")).expect("read crates/ dir") {
let manifest_path = entry.expect("crates/ dir entry").path().join("Cargo.toml");
let Ok(manifest) = fs::read_to_string(&manifest_path) else {
continue;
};
if is_publish_false(&manifest) {
continue;
}
set.insert(package_name(&manifest).unwrap_or_else(|| {
panic!("no package name in {}", manifest_path.display());
}));
}
set
}
fn release_publish_list(root: &Path) -> BTreeSet<String> {
release_publish_sequence(root).into_iter().collect()
}
fn release_publish_sequence(root: &Path) -> Vec<String> {
let yml = fs::read_to_string(root.join(".github/workflows/release.yml"))
.expect("read .github/workflows/release.yml");
yml.lines()
.find_map(|line| {
let spec = line
.trim()
.strip_prefix("for crate in ")?
.split(';')
.next()?;
Some(spec.split_whitespace().map(String::from).collect())
})
.unwrap_or_default()
}
fn local_publishable_dependencies(
manifest: &str,
publishable: &BTreeSet<String>,
) -> BTreeSet<String> {
manifest
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if !trimmed.starts_with("fallow-") {
return None;
}
let (name, _) = trimmed.split_once('=')?;
let name = name.trim();
publishable.contains(name).then(|| name.to_string())
})
.collect()
}
#[test]
fn release_publish_list_matches_publishable_workspace_crates() {
let root = repo_root();
let listed = release_publish_list(&root);
let publishable = publishable_crates(&root);
assert!(
!listed.is_empty(),
"could not parse the `for crate in ...; do` publish loop out of \
.github/workflows/release.yml; the loop format may have changed"
);
let missing_from_list: Vec<&String> = publishable.difference(&listed).collect();
let extra_in_list: Vec<&String> = listed.difference(&publishable).collect();
assert!(
missing_from_list.is_empty() && extra_in_list.is_empty(),
"release.yml crates.io publish list has drifted from the workspace's \
publishable crates.\n publishable but NOT in the list (add them in \
dependency order): {missing_from_list:?}\n listed but NOT publishable \
(drop them, or remove `publish = false`): {extra_in_list:?}"
);
}
#[test]
fn release_publish_list_orders_workspace_dependencies_first() {
let root = repo_root();
let publishable = publishable_crates(&root);
let sequence = release_publish_sequence(&root);
assert!(
!sequence.is_empty(),
"could not parse the `for crate in ...; do` publish loop out of \
.github/workflows/release.yml; the loop format may have changed"
);
for entry in fs::read_dir(root.join("crates")).expect("read crates/ dir") {
let manifest_path = entry.expect("crates/ dir entry").path().join("Cargo.toml");
let Ok(manifest) = fs::read_to_string(&manifest_path) else {
continue;
};
if is_publish_false(&manifest) {
continue;
}
let crate_name = package_name(&manifest).unwrap_or_else(|| {
panic!("no package name in {}", manifest_path.display());
});
let Some(crate_index) = sequence.iter().position(|name| name == &crate_name) else {
continue;
};
for dependency in local_publishable_dependencies(&manifest, &publishable) {
let dependency_index = sequence
.iter()
.position(|name| name == &dependency)
.unwrap_or_else(|| {
panic!("{crate_name} depends on publishable crate {dependency}, but it is missing from release.yml")
});
assert!(
dependency_index < crate_index,
"{crate_name} depends on {dependency}, so release.yml must publish {dependency} first"
);
}
}
}