use std::process::Command;
use crate::{
CliResult,
output::{CliError, Report},
project::{Project, check_wasm_target},
};
pub const STEPS: [(&str, &[&str]); 4] = [
("cargo fmt --check", &["fmt", "--check"]),
("cargo clippy --all-targets -- -D warnings", &["clippy", "--all-targets", "--", "-D", "warnings"]),
("cargo test", &["test"]),
("cargo check --target wasm32-unknown-unknown", &["check", "--target", "wasm32-unknown-unknown"]),
];
pub const I18N_STEP: &str = "ocre i18n missing";
pub fn run(project: &Project, signoff: bool, json: bool) -> CliResult {
let mut report = Report::new("ci");
let mut failure = None;
for (name, args) in STEPS {
if args[0] == "check"
&& let Err(err) = check_wasm_target()
{
failure = Some(err);
break;
}
let status = Command::new("cargo").args(args).current_dir(&project.root).stdout(output(json)).status();
match status {
Ok(status) if status.success() => report.ran.push(format!("{name}: ok")),
Ok(status) => {
failure = Some(CliError::new(format!("{name} failed ({status})")).hint(fix(name)));
break;
}
Err(err) => {
return Err(CliError::new(format!("could not run cargo: {err}"))
.hint("install Rust with rustup: https://rustup.rs"));
}
}
}
if failure.is_none() && has_locales(project) {
match crate::i18n::missing(project) {
Ok(_) => report.ran.push(format!("{I18N_STEP}: ok")),
Err(err) => failure = Some(err),
}
}
if failure.is_none() && signoff {
match gh_signoff(project, json) {
Ok(()) => report.ran.push("gh signoff: ok".to_owned()),
Err(err) => failure = Some(err),
}
}
report.failure = failure;
Ok(report)
}
pub fn has_locales(project: &Project) -> bool {
std::fs::read_to_string(project.root.join("src/lib.rs"))
.is_ok_and(|lib| crate::i18n::declared(&lib).is_some_and(|codes| !codes.is_empty()))
}
fn fix(step: &str) -> &'static str {
match step {
"cargo fmt --check" => "run `cargo fmt`, then `ocre ci` again",
"cargo clippy --all-targets -- -D warnings" => {
"fix the warnings above (`rustup component add clippy` if cargo has no clippy command)"
}
_ => "the cargo output above names the failure",
}
}
fn gh_signoff(project: &Project, json: bool) -> Result<(), CliError> {
let status =
Command::new("gh").arg("signoff").current_dir(&project.root).stdout(output(json)).status().map_err(|_| {
CliError::new("gh not found").hint(
"install the GitHub CLI (https://cli.github.com), then `gh extension install basecamp/gh-signoff`",
)
})?;
if status.success() {
Ok(())
} else {
Err(CliError::new(format!("gh signoff failed ({status})")).hint(
"install the extension with `gh extension install basecamp/gh-signoff`, push the branch, and log in with `gh auth login`",
))
}
}
fn output(json: bool) -> std::process::Stdio {
if json { std::process::Stdio::from(std::io::stderr()) } else { std::process::Stdio::inherit() }
}