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:
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>"
);