Add quire-ci binary that compiles and prints the pipeline
Reads .quire/ci.fnl, compiles it via quire-core, and prints the
job graph in topological order. Miette renders compile errors
with inline source labels.
Assisted-by: GLM-5.1 via pi
diff --git a/Cargo.lock b/Cargo.lock
index e846408..837024c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2127,6 +2127,8 @@ dependencies = [
name = "quire-ci"
version = "0.1.0"
dependencies = [
+ "fs-err",
+ "miette",
"quire-core",
]
diff --git a/quire-ci/Cargo.toml b/quire-ci/Cargo.toml
index 5257aa8..e28c06d 100644
--- a/quire-ci/Cargo.toml
+++ b/quire-ci/Cargo.toml
@@ -4,4 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
+fs-err = "*"
+miette = { version = "*", features = ["fancy"] }
quire-core = { path = "../quire-core" }
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index f328e4d..2d8a313 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -1 +1,68 @@
-fn main() {}
+use std::path::PathBuf;
+use std::process::ExitCode;
+
+use miette::IntoDiagnostic;
+use quire_core::ci::pipeline::CompileError;
+
+fn main() -> ExitCode {
+ miette::set_panic_hook();
+ match run() {
+ Ok(()) => ExitCode::SUCCESS,
+ Err(e) => {
+ eprintln!("{e:?}");
+ ExitCode::FAILURE
+ }
+ }
+}
+
+fn run() -> miette::Result<()> {
+ let path = find_ci_fnl()?;
+
+ let source = fs_err::read_to_string(&path).into_diagnostic()?;
+
+ let pipeline = match quire_core::ci::pipeline::compile(&source, &path.display().to_string()) {
+ Ok(p) => p,
+ Err(CompileError::Fennel(err)) => {
+ return Err(miette::Report::new_boxed(err));
+ }
+ Err(CompileError::Pipeline(err)) => {
+ return Err(miette::Report::new_boxed(err));
+ }
+ };
+
+ let jobs = pipeline.jobs();
+ if jobs.is_empty() {
+ println!("No jobs registered.");
+ return Ok(());
+ }
+
+ if let Some(image) = pipeline.image() {
+ println!("Image: {image}");
+ }
+
+ let topo = pipeline.topo_order();
+ println!("Jobs (topological order):");
+ for id in &topo {
+ let job = pipeline.job(id).expect("topo_order returns valid ids");
+ let inputs = job.inputs.join(", ");
+ println!(" {id} <- [{inputs}]");
+ }
+
+ println!("\nAll validations passed.");
+ Ok(())
+}
+
+/// Walk from cwd upward to find `.quire/ci.fnl`.
+fn find_ci_fnl() -> miette::Result<PathBuf> {
+ let cwd = std::env::current_dir().into_diagnostic()?;
+ let mut dir = cwd.as_path();
+ loop {
+ let candidate = dir.join(".quire").join("ci.fnl");
+ if candidate.is_file() {
+ return Ok(candidate);
+ }
+ dir = dir
+ .parent()
+ .ok_or_else(|| miette::miette!("no .quire/ci.fnl found in any parent directory"))?;
+ }
+}