ci/verbs/src/lib.rs
//! The CI runner's own verbs (`ci/runner.sh`), detached from dwarves' workspace:
//!
//! * `ci-verbs lockcheck --siblings <dir> [--repo <name>]... [--single <crate>]...`: THE
//! LOCK CHECK over a constellation of checked-out heads. Every `Cargo.lock` of every
//! checkout under `<dir>` is asked `cargo metadata --locked` in its own directory with
//! the sibling patches on, and no crate may appear at two versions where that is
//! refused. One receipt like every other ci run, repo `constellation`;
//! * `ci-verbs status --runs <dir> [--out <file>]`: the status page over every receipt
//! below `<dir>`;
//! * `ci-verbs scorecard --checkout <dir> [--out <dir>]`: the OpenSSF Scorecard of a checkout
//! in local mode, as JSON and a Markdown summary ([`scorecard`]);
//! * `ci-verbs openssf-badge --badge <repo>=<project>... [--checkout <dir>] [--out <dir>]`:
//! a checked copy of the checkout's OpenSSF Best Practices badge, which the forge
//! publishes in nordisk/ci-status for gunnar.rs to draw from its own origin ([`badge`]);
//! * `ci-verbs fuzz --checkout <dir>`: the nightly fuzz run of a cargo-fuzz crate, red on a
//! crash ([`fuzz`]);
//! * `ci-verbs coverage --checkout <dir>`: the nightly cargo llvm-cov numbers ([`coverage`]);
//! * `ci-verbs deps-update --checkout <dir> --author ..`: the weekly dependency update
//! proposal ([`deps`]);
//! * `ci-verbs bench-readme --checkout <dir> --host <ssh host> ..`: the weekly bench on the
//! bench host and the README speed tables regenerated from it, proposed as a `bench/` branch
//! the forge merges, red on a regression ([`bench`]).
//!
//! dwarves' `cargo xtask ci lockcheck|status` are a KNOWN TWIN of these two verbs, kept
//! for developers until the xtask hands over to this crate. The runner and every step are
//! nornir-xtask-support's.
use std::path::{Path, PathBuf};
use nornir_xtask_support::ci::{self, Plan, Step};
use nornir_xtask_support::lockcheck;
pub mod badge;
pub mod bench;
pub mod coverage;
pub mod deps;
pub mod fuzz;
pub mod scorecard;
/// The value after `flag`.
pub fn value(args: &[String], flag: &str) -> Option<String> {
args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned()
}
/// Every value after a `flag`.
pub fn values(args: &[String], flag: &str) -> Vec<String> {
let mut out = Vec::new();
let mut it = args.iter();
while let Some(a) = it.next() {
if a == flag {
if let Some(v) = it.next() {
out.push(v.clone());
}
}
}
out
}
/// `lockcheck`: `--siblings` else `default_siblings`.
pub fn lockcheck_verb(args: &[String], default_siblings: &Path) -> i32 {
let dir = value(args, "--siblings").map(PathBuf::from).unwrap_or_else(|| default_siblings.to_path_buf());
let dir = std::path::absolute(&dir).unwrap_or(dir);
let named = values(args, "--repo");
let single = lockcheck::single_from_args(args);
let d = dir.clone();
let step = Step::in_process("lockcheck", move |w| {
let single: Vec<&str> = single.iter().map(String::as_str).collect();
for repo in lockcheck::repos_under(&d, &named) {
let head = ci::head(&repo);
let _ = writeln!(w, "head {} {head}", repo.file_name().map(|n| n.to_string_lossy()).unwrap_or_default());
}
let red: Vec<String> =
lockcheck::check_constellation(&d, &named, &single, w)?.iter().filter(|f| f.is_red()).map(|f| f.to_string()).collect();
if red.is_empty() {
Ok(None)
} else {
Err(format!("{} red finding(s): {}", red.len(), red.join("; ")).into())
}
});
let plan = Plan { repo: "constellation".into(), root: dir, steps: vec![step] };
ci::main(&plan, &[])
}
/// `status`.
pub fn status_verb(args: &[String]) -> i32 {
let Some(runs) = value(args, "--runs") else {
eprintln!("usage: ci-verbs status --runs <dir> [--out <file>]");
return 2;
};
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
let md = ci::status_md(Path::new(&runs), now);
match value(args, "--out") {
Some(out) => match std::fs::write(&out, &md) {
Ok(()) => {
println!("ci status: {out}");
0
}
Err(e) => {
eprintln!("ci status: write {out}: {e}");
1
}
},
None => {
print!("{md}");
0
}
}
}
/// Dispatch: `lockcheck ...` | `status ...`.
pub fn run(args: &[String], default_siblings: &Path) -> i32 {
match args.first().map(String::as_str) {
Some("lockcheck") => lockcheck_verb(&args[1..], default_siblings),
Some("status") => status_verb(&args[1..]),
Some("scorecard") => scorecard::verb(&args[1..]),
Some("openssf-badge") => badge::verb(&args[1..]),
Some("fuzz") => fuzz::verb(&args[1..]),
Some("coverage") => coverage::verb(&args[1..]),
Some("deps-update") => deps::verb(&args[1..]),
Some("bench-readme") => bench::verb(&args[1..]),
_ => {
eprintln!("usage: ci-verbs lockcheck --siblings <dir> [--repo <name>]... [--single <crate>]... | status --runs <dir> [--out <file>] | scorecard --checkout <dir> [--out <dir>] [--bin <scorecard>] | openssf-badge --badge <repo>=<project>... [--checkout <dir>] [--out <dir>] | fuzz --checkout <dir> [--private <sibling>] [--seconds <n>] | coverage --checkout <dir> [--package <p>]... [--min <pct>] | deps-update --checkout <dir> --author \"<name> <email>\" | bench-readme --checkout <dir> --host <ssh host> --seeds <dir> --machine <name> --threshold <pct> --merge <branch> --author \"<name> <email>\"");
2
}
}
}