commit 19b29d71693d85d174b8ee279e15b4f5d114f8c1
parent d6b89a3aabfc10b553bc588c5bec2834cd148c70
Author: triesap <tyson@radroots.org>
Date: Wed, 25 Feb 2026 15:21:18 +0000
xtask: add release preflight validation command
- add sdk release preflight command wiring in xtask cli usage and dispatch
- validate publish crate metadata completeness for release contract publish set
- validate required crate coverage summary status and 100/100/100 thresholds
- add xtask unit coverage for preflight metadata and summary validation helpers
Diffstat:
3 files changed, 232 insertions(+), 0 deletions(-)
diff --git a/crates/xtask/README.md b/crates/xtask/README.md
@@ -4,10 +4,13 @@
```bash
cargo run -q -p xtask -- sdk validate
+cargo run -q -p xtask -- sdk release preflight
```
Validates the sdk contract manifest, version policy, export mappings, and required artifacts.
+`sdk release preflight` validates contract parity plus release metadata and required coverage summaries for the publish set.
+
## export
```bash
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -183,6 +183,23 @@ fn workspace_package_names(workspace_root: &Path) -> Result<Vec<String>, String>
Ok(names)
}
+fn workspace_package_manifests(workspace_root: &Path) -> Result<BTreeMap<String, PathBuf>, String> {
+ let workspace_manifest =
+ parse_toml::<WorkspaceCargoManifest>(&workspace_root.join("Cargo.toml"))?;
+ let mut manifests = BTreeMap::new();
+ for member in workspace_manifest.workspace.members {
+ let member_manifest = workspace_root.join(&member).join("Cargo.toml");
+ let package_manifest = parse_toml::<PackageCargoManifest>(&member_manifest)?;
+ if manifests
+ .insert(package_manifest.package.name, member_manifest)
+ .is_some()
+ {
+ return Err("duplicate workspace package name in manifest map".to_string());
+ }
+ }
+ Ok(manifests)
+}
+
fn load_coverage_rollout(contract_root: &Path) -> Result<CoverageRolloutFile, String> {
parse_toml::<CoverageRolloutFile>(&contract_root.join("coverage").join("rollout.toml"))
}
@@ -279,6 +296,119 @@ fn collect_unique_set(items: &[String], field: &str) -> Result<BTreeSet<String>,
Ok(set)
}
+fn package_field_configured(table: &toml::value::Table, field: &str) -> bool {
+ let Some(value) = table.get(field) else {
+ return false;
+ };
+ match value {
+ toml::Value::String(raw) => !raw.trim().is_empty(),
+ toml::Value::Table(inner) => inner
+ .get("workspace")
+ .and_then(toml::Value::as_bool)
+ .is_some_and(|configured| configured),
+ _ => false,
+ }
+}
+
+fn validate_publish_package_metadata(
+ workspace_root: &Path,
+ publish_crates: &BTreeSet<String>,
+) -> Result<(), String> {
+ let manifests = workspace_package_manifests(workspace_root)?;
+ for crate_name in publish_crates {
+ let manifest_path = manifests
+ .get(crate_name)
+ .ok_or_else(|| format!("publish crate {} has no workspace manifest", crate_name))?;
+ let parsed = parse_toml::<toml::Value>(manifest_path)?;
+ let package = parsed
+ .get("package")
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("{} missing [package] table", manifest_path.display()))?;
+
+ if !package_field_configured(package, "description") {
+ return Err(format!(
+ "publish crate {} must define a non-empty package.description",
+ crate_name
+ ));
+ }
+ for field in ["repository", "homepage", "documentation", "readme"] {
+ if !package_field_configured(package, field) {
+ return Err(format!(
+ "publish crate {} must configure package.{}",
+ crate_name, field
+ ));
+ }
+ }
+ }
+ Ok(())
+}
+
+fn parse_coverage_percent(raw: &str, field: &str, crate_name: &str) -> Result<f64, String> {
+ raw.parse::<f64>()
+ .map_err(|e| format!("parse {} for {}: {e}", field, crate_name))
+}
+
+fn load_coverage_refresh_rows(
+ workspace_root: &Path,
+) -> Result<BTreeMap<String, (String, f64, f64, f64)>, String> {
+ let report_path = workspace_root
+ .join("target")
+ .join("coverage")
+ .join("coverage-refresh.tsv");
+ let raw = fs::read_to_string(&report_path)
+ .map_err(|e| format!("read {}: {e}", report_path.display()))?;
+ let mut rows = BTreeMap::new();
+ for line in raw.lines().skip(1) {
+ let trimmed = line.trim();
+ if trimmed.is_empty() {
+ continue;
+ }
+ let parts = trimmed.split('\t').collect::<Vec<_>>();
+ if parts.len() < 5 {
+ return Err(format!(
+ "coverage row must have at least 5 columns in {}: {}",
+ report_path.display(),
+ trimmed
+ ));
+ }
+ let crate_name = parts[0].to_string();
+ let status = parts[1].to_string();
+ let exec = parse_coverage_percent(parts[2], "exec", &crate_name)?;
+ let func = parse_coverage_percent(parts[3], "func", &crate_name)?;
+ let branch = parse_coverage_percent(parts[4], "branch", &crate_name)?;
+ rows.insert(crate_name, (status, exec, func, branch));
+ }
+ Ok(rows)
+}
+
+fn validate_required_coverage_summary(
+ workspace_root: &Path,
+ required_crates: &BTreeSet<String>,
+) -> Result<(), String> {
+ let rows = load_coverage_refresh_rows(workspace_root)?;
+ for crate_name in required_crates {
+ let (status, exec, func, branch) = rows.get(crate_name).ok_or_else(|| {
+ format!(
+ "required coverage crate {} missing from coverage-refresh.tsv",
+ crate_name
+ )
+ })?;
+ if status != "pass" {
+ return Err(format!(
+ "required coverage crate {} has non-pass status {}",
+ crate_name, status
+ ));
+ }
+ if *exec < 100.0 || *func < 100.0 || *branch < 100.0 {
+ return Err(format!(
+ "required coverage crate {} must be 100/100/100, found {}/{}/{}",
+ crate_name, exec, func, branch
+ ));
+ }
+ }
+ Ok(())
+}
+
const CORE_UNIT_DIMENSION_ENUM: &str = "RadrootsCoreUnitDimension";
const CORE_UNIT_DIMENSION_ORDER: [&str; 3] = ["Count", "Mass", "Volume"];
@@ -633,6 +763,18 @@ fn validate_release_publish_policy(
Ok(())
}
+pub fn validate_release_preflight(workspace_root: &Path) -> Result<(), String> {
+ let bundle = load_contract_bundle(workspace_root)?;
+ validate_contract_bundle(&bundle)?;
+ let release = load_release_contract(&bundle.root)?;
+ let required = load_coverage_required(&bundle.root)?;
+ let publish_crates = collect_unique_set(&release.publish.crates, "publish.crates")?;
+ let required_crates = collect_unique_set(&required.required.crates, "required.crates")?;
+ validate_publish_package_metadata(workspace_root, &publish_crates)?;
+ validate_required_coverage_summary(workspace_root, &required_crates)?;
+ Ok(())
+}
+
pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, String> {
let root = contract_root(workspace_root);
let manifest = parse_toml::<ContractManifest>(&root.join("manifest.toml"))?;
@@ -768,7 +910,9 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> {
mod tests {
use super::*;
use std::collections::BTreeSet;
+ use std::fs;
use std::path::PathBuf;
+ use std::time::{SystemTime, UNIX_EPOCH};
fn workspace_root() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -778,6 +922,16 @@ mod tests {
.expect("canonical workspace root")
}
+ fn temp_root(prefix: &str) -> PathBuf {
+ let nanos = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("clock")
+ .as_nanos();
+ let root = std::env::temp_dir().join(format!("radroots_xtask_{prefix}_{nanos}"));
+ fs::create_dir_all(&root).expect("create temp root");
+ root
+ }
+
#[test]
fn validate_current_contract_bundle() {
let root = workspace_root();
@@ -912,4 +1066,63 @@ pub enum RadrootsCoreUnitDimension {
.collect::<BTreeSet<_>>();
assert_eq!(required_names, rollout_required);
}
+
+ #[test]
+ fn package_field_configured_accepts_workspace_table() {
+ let mut package = toml::value::Table::new();
+ let mut repository = toml::value::Table::new();
+ repository.insert("workspace".to_string(), toml::Value::Boolean(true));
+ package.insert("repository".to_string(), toml::Value::Table(repository));
+ assert!(package_field_configured(&package, "repository"));
+ }
+
+ #[test]
+ fn validate_required_coverage_summary_enforces_strict_threshold() {
+ let root = temp_root("coverage_summary");
+ let coverage_dir = root.join("target").join("coverage");
+ fs::create_dir_all(&coverage_dir).expect("create coverage dir");
+ fs::write(
+ coverage_dir.join("coverage-refresh.tsv"),
+ "crate\tstatus\texec\tfunc\tbranch\treport\nradroots-core\tpass\t100.0\t100.0\t100.0\tfile\n",
+ )
+ .expect("write coverage file");
+ let required = ["radroots-core".to_string()]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ validate_required_coverage_summary(&root, &required).expect("coverage summary");
+ let _ = fs::remove_dir_all(&root);
+ }
+
+ #[test]
+ fn validate_publish_package_metadata_requires_description() {
+ let root = temp_root("publish_metadata");
+ fs::create_dir_all(root.join("crates").join("a")).expect("create crate dir");
+ fs::write(
+ root.join("Cargo.toml"),
+ r#"[workspace]
+members = ["crates/a"]
+"#,
+ )
+ .expect("write workspace manifest");
+ fs::write(
+ root.join("crates").join("a").join("Cargo.toml"),
+ r#"[package]
+name = "radroots-a"
+version = "0.1.0"
+edition = "2024"
+repository = { workspace = true }
+homepage = { workspace = true }
+documentation = "https://docs.rs/radroots-a"
+readme = { workspace = true }
+"#,
+ )
+ .expect("write package manifest");
+ let publish = ["radroots-a".to_string()]
+ .into_iter()
+ .collect::<BTreeSet<_>>();
+ let err =
+ validate_publish_package_metadata(&root, &publish).expect_err("missing description");
+ assert!(err.contains("package.description"));
+ let _ = fs::remove_dir_all(&root);
+ }
}
diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs
@@ -17,6 +17,7 @@ fn usage() {
eprintln!(" cargo xtask sdk export-ts-wasm [--out <dir>]");
eprintln!(" cargo xtask sdk export-manifest [--out <dir>]");
eprintln!(" cargo xtask sdk validate");
+ eprintln!(" cargo xtask sdk release preflight");
eprintln!(" cargo xtask sdk coverage run-crate --crate <crate> [--out <dir>]");
eprintln!(" cargo xtask sdk coverage required-crates");
eprintln!(" cargo xtask sdk coverage workspace-crates");
@@ -147,6 +148,20 @@ fn validate_contract() -> Result<(), String> {
Ok(())
}
+fn release_preflight() -> Result<(), String> {
+ let root = workspace_root()?;
+ contract::validate_release_preflight(&root)?;
+ eprintln!("validated release preflight for contract 0.1.0");
+ Ok(())
+}
+
+fn run_release(args: &[String]) -> Result<(), String> {
+ match args.first().map(String::as_str) {
+ Some("preflight") => release_preflight(),
+ _ => Err("unknown release subcommand".to_string()),
+ }
+}
+
fn run_sdk(args: &[String]) -> Result<(), String> {
match args.first().map(String::as_str) {
Some("export-ts") => export_ts(&args[1..]),
@@ -156,6 +171,7 @@ fn run_sdk(args: &[String]) -> Result<(), String> {
Some("export-ts-wasm") => export_ts_wasm(&args[1..]),
Some("export-manifest") => export_manifest(&args[1..]),
Some("validate") => validate_contract(),
+ Some("release") => run_release(&args[1..]),
Some("coverage") => coverage::run(&args[1..]),
_ => Err("unknown sdk subcommand".to_string()),
}