lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 55b2f1eb69df0b2578f2e7c8426a8b0547c2d3c1
parent 431901f8c5524948c02946651bede377494f7aef
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Feb 2026 03:12:22 +0000

ci: drive blocking coverage gates from required crates contract


- add xtask coverage required-crates subcommand for contract-backed crate discovery
- replace hardcoded workflow blocking gates with required-crates driven looping
- validate required crate contract parsing for empty and duplicate entries in xtask tests
- simplify blocking artifact upload paths to include all generated blocking reports

Diffstat:
M.github/workflows/sdk-coverage-ci.yml | 103+++++++++++++------------------------------------------------------------------
Mcrates/xtask/src/coverage.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/xtask/src/main.rs | 1+
3 files changed, 83 insertions(+), 86 deletions(-)

diff --git a/.github/workflows/sdk-coverage-ci.yml b/.github/workflows/sdk-coverage-ci.yml @@ -63,89 +63,25 @@ jobs: --fail-under-branches 0 done - - name: enforce blocking core coverage gate + - name: enforce blocking required coverage gates run: | set -euo pipefail - core_dir="target/sdk-coverage/radroots_core" - cargo run -q -p xtask -- sdk coverage report \ - --scope "radroots-core-blocking" \ - --summary "${core_dir}/coverage-summary.json" \ - --lcov "${core_dir}/coverage-lcov.info" \ - --out "${core_dir}/coverage-gate-blocking.json" \ - --fail-under-exec-lines 100 \ - --fail-under-functions 100 \ - --fail-under-branches 100 \ - --require-branches + cargo run -q -p xtask -- sdk coverage required-crates > /tmp/radroots_required_coverage_crates.txt - - name: enforce blocking types coverage gate - run: | - set -euo pipefail - types_dir="target/sdk-coverage/radroots_types" - cargo run -q -p xtask -- sdk coverage report \ - --scope "radroots-types-blocking" \ - --summary "${types_dir}/coverage-summary.json" \ - --lcov "${types_dir}/coverage-lcov.info" \ - --out "${types_dir}/coverage-gate-blocking.json" \ - --fail-under-exec-lines 100 \ - --fail-under-functions 100 \ - --fail-under-branches 100 \ - --require-branches - - - name: enforce blocking events coverage gate - run: | - set -euo pipefail - events_dir="target/sdk-coverage/radroots_events" - cargo run -q -p xtask -- sdk coverage report \ - --scope "radroots-events-blocking" \ - --summary "${events_dir}/coverage-summary.json" \ - --lcov "${events_dir}/coverage-lcov.info" \ - --out "${events_dir}/coverage-gate-blocking.json" \ - --fail-under-exec-lines 100 \ - --fail-under-functions 100 \ - --fail-under-branches 100 \ - --require-branches - - - name: enforce blocking identity coverage gate - run: | - set -euo pipefail - identity_dir="target/sdk-coverage/radroots_identity" - cargo run -q -p xtask -- sdk coverage report \ - --scope "radroots-identity-blocking" \ - --summary "${identity_dir}/coverage-summary.json" \ - --lcov "${identity_dir}/coverage-lcov.info" \ - --out "${identity_dir}/coverage-gate-blocking.json" \ - --fail-under-exec-lines 100 \ - --fail-under-functions 100 \ - --fail-under-branches 100 \ - --require-branches - - - name: enforce blocking trade coverage gate - run: | - set -euo pipefail - trade_dir="target/sdk-coverage/radroots_trade" - cargo run -q -p xtask -- sdk coverage report \ - --scope "radroots-trade-blocking" \ - --summary "${trade_dir}/coverage-summary.json" \ - --lcov "${trade_dir}/coverage-lcov.info" \ - --out "${trade_dir}/coverage-gate-blocking.json" \ - --fail-under-exec-lines 100 \ - --fail-under-functions 100 \ - --fail-under-branches 100 \ - --require-branches - - - name: enforce blocking events-codec coverage gate - run: | - set -euo pipefail - events_codec_dir="target/sdk-coverage/radroots_events_codec" - cargo run -q -p xtask -- sdk coverage report \ - --scope "radroots-events-codec-blocking" \ - --summary "${events_codec_dir}/coverage-summary.json" \ - --lcov "${events_codec_dir}/coverage-lcov.info" \ - --out "${events_codec_dir}/coverage-gate-blocking.json" \ - --fail-under-exec-lines 100 \ - --fail-under-functions 100 \ - --fail-under-branches 100 \ - --require-branches + while IFS= read -r crate; do + [ -n "${crate}" ] || continue + safe_crate="${crate//-/_}" + crate_dir="target/sdk-coverage/${safe_crate}" + cargo run -q -p xtask -- sdk coverage report \ + --scope "${crate}-blocking" \ + --summary "${crate_dir}/coverage-summary.json" \ + --lcov "${crate_dir}/coverage-lcov.info" \ + --out "${crate_dir}/coverage-gate-blocking.json" \ + --fail-under-exec-lines 100 \ + --fail-under-functions 100 \ + --fail-under-branches 100 \ + --require-branches + done < /tmp/radroots_required_coverage_crates.txt - name: upload sdk coverage reports uses: actions/upload-artifact@v4 @@ -153,9 +89,4 @@ jobs: name: sdk-coverage-reports path: | target/sdk-coverage/**/coverage-gate-summary.json - target/sdk-coverage/radroots_core/coverage-gate-blocking.json - target/sdk-coverage/radroots_types/coverage-gate-blocking.json - target/sdk-coverage/radroots_events/coverage-gate-blocking.json - target/sdk-coverage/radroots_identity/coverage-gate-blocking.json - target/sdk-coverage/radroots_trade/coverage-gate-blocking.json - target/sdk-coverage/radroots_events_codec/coverage-gate-blocking.json + target/sdk-coverage/**/coverage-gate-blocking.json diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -4,6 +4,7 @@ use std::fs; use std::path::Path; use std::path::PathBuf; use std::process::Command; +use std::{collections::BTreeSet, io::Write}; use serde::Deserialize; use serde::Serialize; @@ -115,6 +116,16 @@ struct LlvmCovSummaryMetric { percent: f64, } +#[derive(Debug, Deserialize)] +struct CoverageRequiredContract { + required: CoverageRequiredList, +} + +#[derive(Debug, Deserialize)] +struct CoverageRequiredList { + crates: Vec<String>, +} + pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { let raw = fs::read_to_string(path) .map_err(|err| format!("failed to read summary {}: {err}", path.display()))?; @@ -133,6 +144,28 @@ pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { }) } +fn read_required_crates(path: &Path) -> Result<Vec<String>, String> { + let raw = fs::read_to_string(path) + .map_err(|err| format!("failed to read required crates {}: {err}", path.display()))?; + let parsed: CoverageRequiredContract = toml::from_str(&raw) + .map_err(|err| format!("failed to parse required crates {}: {err}", path.display()))?; + if parsed.required.crates.is_empty() { + return Err("coverage required crates list must not be empty".to_string()); + } + let mut seen = BTreeSet::new(); + for crate_name in &parsed.required.crates { + if crate_name.trim().is_empty() { + return Err("coverage required crates list includes an empty crate name".to_string()); + } + if !seen.insert(crate_name.clone()) { + return Err(format!( + "coverage required crates list includes duplicate crate {crate_name}" + )); + } + } + Ok(parsed.required.crates) +} + pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { let raw = fs::read_to_string(path) .map_err(|err| format!("failed to read lcov {}: {err}", path.display()))?; @@ -566,11 +599,27 @@ fn report_gate(args: &[String]) -> Result<(), String> { Ok(()) } +fn list_required_crates() -> Result<(), String> { + let root = workspace_root()?; + let required_path = root + .join("contract") + .join("coverage") + .join("required-crates.toml"); + let crates = read_required_crates(&required_path)?; + let mut stdout = std::io::stdout().lock(); + for crate_name in crates { + writeln!(stdout, "{crate_name}") + .map_err(|err| format!("failed to write required crates output: {err}"))?; + } + Ok(()) +} + pub fn run(args: &[String]) -> Result<(), String> { match args.first().map(String::as_str) { Some("help") => Ok(()), Some("run-crate") => run_crate(&args[1..]), Some("report") => report_gate(&args[1..]), + Some("required-crates") => list_required_crates(), Some(_) => Err("unknown sdk coverage subcommand".to_string()), None => Err("missing sdk coverage subcommand".to_string()), } @@ -682,4 +731,20 @@ mod tests { .any(|reason| reason == "branches=unavailable") ); } + + #[test] + fn reads_required_crates_and_rejects_duplicates() { + let path = temp_file_path("required_crates"); + fs::write(&path, "[required]\ncrates = [\"a\", \"b\"]\n").expect("write required crates"); + let crates = read_required_crates(&path).expect("parse required crates"); + assert_eq!(crates, vec!["a".to_string(), "b".to_string()]); + fs::remove_file(&path).expect("remove required crates"); + + let dup_path = temp_file_path("required_crates_dup"); + fs::write(&dup_path, "[required]\ncrates = [\"a\", \"a\"]\n") + .expect("write dup required crates"); + let err = read_required_crates(&dup_path).expect_err("duplicate required crates"); + assert!(err.contains("duplicate crate a")); + fs::remove_file(dup_path).expect("remove dup required crates"); + } } 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-manifest [--out <dir>]"); eprintln!(" cargo xtask sdk validate"); eprintln!(" cargo xtask sdk coverage run-crate --crate <crate> [--out <dir>]"); + eprintln!(" cargo xtask sdk coverage required-crates"); eprintln!( " cargo xtask sdk coverage report --scope <scope> --summary <file> --lcov <file> --out <file>" );