Files
portal/src/bin/question_lint.rs
T

77 lines
2.7 KiB
Rust
Raw Normal View History

//! Headless schema-check binary - loads content (from a Gitea repo URL
//! or a local directory) and validates it exactly the way
//! `content::watch_for_reload`/`main.rs`'s boot path do, with no NATS,
//! OIDC, web server, or JetStream connection involved. Built once by
//! portal's own deploy workflow and downloaded as a release artifact by
//! `questions`' own CI, rather than compiled there - keeps that repo's
//! CI coupling to "run a static binary," not "build a Rust workspace."
#![cfg(feature = "ssr")]
use portal::content;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut args = std::env::args().skip(1);
let mut repo: Option<String> = None;
let mut branch = "main".to_string();
let mut subdir = "questions".to_string();
let mut path: Option<String> = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"--repo" => repo = args.next(),
"--branch" => branch = args.next().unwrap_or(branch),
"--subdir" => subdir = args.next().unwrap_or(subdir),
"--path" => path = args.next(),
other => {
eprintln!("unknown argument: {other}");
std::process::exit(2);
}
}
}
let questions = if let Some(dir) = path {
load_from_dir(&dir)?
} else if let Some(repo_url) = repo {
content::load_questions_from_gitea(&repo_url, &branch, &subdir).await?
} else {
eprintln!(
"usage: question-lint --repo <gitea-url> [--branch main] [--subdir questions] | --path <local-dir>"
);
std::process::exit(2);
};
match content::validate_questions(&questions) {
Ok(()) => {
println!("OK: {} question(s) valid", questions.len());
Ok(())
}
Err(e) => {
eprintln!("FAIL: {e}");
std::process::exit(1);
}
}
}
/// The offline counterpart to `content::load_questions_from_gitea` -
/// same "every `*.yaml` file becomes a `Question` keyed by its own
/// `id`" shape, just reading a local checkout instead of Gitea's API,
/// for linting a branch that hasn't been pushed yet.
fn load_from_dir(
dir: &str,
) -> anyhow::Result<std::collections::HashMap<String, content::Question>> {
let mut out = std::collections::HashMap::new();
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
continue;
}
let raw = std::fs::read_to_string(&path)?;
let question: content::Question = serde_yaml::from_str(&raw)
.map_err(|e| anyhow::anyhow!("parsing {}: {e}", path.display()))?;
out.insert(question.id.clone(), question);
}
Ok(out)
}