Replace destructured runtime handle with ambient runtime accessor
Run-fns are now zero-arg (fn [] …) instead of (fn [{: sh : secret}] …).
The runtime is installed as a global Lua table whose __index metatable
dispatches runtime.sh, runtime.secret, and runtime.jobs to closures
over the active Runtime. One-arg run-fns are rejected at registration
with a clear error message. This lets helpers compose without threading
a handle through every call, and the runtime. prefix makes effect sites
grep-able. Foundation for runtime.mirror and future runtime-bound
primitives.
Assisted-by: GLM-5.1 via pi
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 480f22b..d6a7827 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -9,7 +9,6 @@ use std::rc::Rc;
use clap::Parser;
use miette::IntoDiagnostic;
-use mlua::IntoLua;
use quire_core::ci::pipeline::{self, Pipeline, RunFn};
use quire_core::ci::run::RunMeta;
use quire_core::ci::runtime::{Runtime, RuntimeError, RuntimeEvent, RuntimeHandle};
@@ -229,11 +228,10 @@ fn run_pipeline(
}));
}
- // Install the runtime handle on the Lua VM once for the whole run;
- // each job's run-fn receives `rt_value` as its sole argument.
+ // Install the ambient runtime on the Lua VM once for the whole run.
let lua = runtime.lua();
- let rt_value = RuntimeHandle(runtime.clone())
- .into_lua(lua)
+ RuntimeHandle(runtime.clone())
+ .install(lua)
.expect("install runtime on Lua VM");
let mut failed_job: Option<(String, RuntimeError)> = None;
@@ -255,7 +253,7 @@ fn run_pipeline(
runtime.enter_job(job_id);
let result: Result<(), RuntimeError> = match run_fn {
RunFn::Lua(f) => f
- .call::<mlua::Value>(rt_value.clone())
+ .call::<mlua::Value>(())
.map(|_| ())
.map_err(RuntimeError::from),
RunFn::Rust(f) => f(&runtime),
diff --git a/quire-core/src/ci/mirror.rs b/quire-core/src/ci/mirror.rs
index 04c214e..8d3da78 100644
--- a/quire-core/src/ci/mirror.rs
+++ b/quire-core/src/ci/mirror.rs
@@ -231,7 +231,6 @@ impl MirrorJob {
#[cfg(test)]
mod tests {
use super::*;
- use mlua::IntoLua;
use crate::ci::pipeline::{Diagnostic, RustRunFn, compile};
use crate::ci::run::RunMeta;
@@ -321,7 +320,7 @@ mod tests {
#[test]
fn mirror_after_appends_to_inputs() {
let source = r#"(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))
+(ci.job :build [:quire/push] (fn [] nil))
(ci.mirror "https://github.com/example/repo.git"
{:secret :github_token :tag (fn [_] "v1") :after [:build]})"#;
let inputs = mirror_job_inputs(source);
@@ -448,8 +447,8 @@ mod tests {
std::env::current_dir().expect("cwd"),
log_dir,
));
- let _ = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
+ RuntimeHandle(runtime.clone())
+ .install(runtime.lua())
.expect("install runtime");
(runtime, run_fn)
}
diff --git a/quire-core/src/ci/pipeline.rs b/quire-core/src/ci/pipeline.rs
index 42b9971..b43e7ae 100644
--- a/quire-core/src/ci/pipeline.rs
+++ b/quire-core/src/ci/pipeline.rs
@@ -53,6 +53,15 @@ pub enum DefinitionError {
#[label("here")]
span: SourceSpan,
},
+
+ #[error(
+ "Job '{job_id}' run-fn takes an argument — run-fns must be zero-arg `(fn [] …)`; use `runtime.sh`, `runtime.secret`, and `runtime.jobs` instead of destructuring a handle."
+ )]
+ ArityViolation {
+ job_id: String,
+ #[label("here")]
+ span: SourceSpan,
+ },
}
/// A post-graph structural error found after all jobs have been
@@ -480,7 +489,7 @@ mod tests {
#[test]
fn compile_registers_a_job() {
let source = r#"(local ci (require :quire.ci))
-(ci.job :test [:quire/push] (fn [_] nil))"#;
+(ci.job :test [:quire/push] (fn [] nil))"#;
let pipeline = compile(source, "ci.fnl").expect("compile should succeed");
let jobs = pipeline.jobs();
assert_eq!(jobs.len(), 1);
@@ -492,8 +501,8 @@ mod tests {
fn compile_registers_multiple_jobs() {
let source = r#"
(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))
-(ci.job :test [:build] (fn [_] nil))
+(ci.job :build [:quire/push] (fn [] nil))
+(ci.job :test [:build] (fn [] nil))
"#;
let pipeline = compile(source, "ci.fnl").expect("compile should succeed");
let jobs = pipeline.jobs();
@@ -507,11 +516,11 @@ mod tests {
#[test]
fn compile_captures_source_line() {
let source = "(local ci (require :quire.ci))
-(ci.job :first [:quire/push] (fn [_] nil))
-(ci.job :second [:quire/push] (fn [_] nil))
+(ci.job :first [:quire/push] (fn [] nil))
+(ci.job :second [:quire/push] (fn [] nil))
-(ci.job :sixth [:quire/push] (fn [_] nil))";
+(ci.job :sixth [:quire/push] (fn [] nil))";
let pipeline = compile(source, "ci.fnl").expect("compile should succeed");
let lines: Vec<usize> = pipeline
.jobs()
@@ -568,8 +577,8 @@ mod tests {
let jobs = registered_jobs(
r#"
(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))
-(ci.job :test [:build :quire/push] (fn [_] nil))
+(ci.job :build [:quire/push] (fn [] nil))
+(ci.job :test [:build :quire/push] (fn [] nil))
"#,
);
assert!(validate(&jobs).is_ok());
@@ -580,8 +589,8 @@ mod tests {
let jobs = registered_jobs(
r#"
(local ci (require :quire.ci))
-(ci.job :a [:b] (fn [_] nil))
-(ci.job :b [:a] (fn [_] nil))
+(ci.job :a [:b] (fn [] nil))
+(ci.job :b [:a] (fn [] nil))
"#,
);
let errs = validate(&jobs).unwrap_err();
@@ -596,9 +605,9 @@ mod tests {
let jobs = registered_jobs(
r#"
(local ci (require :quire.ci))
-(ci.job :a [:b :quire/push] (fn [_] nil))
-(ci.job :b [:a :quire/push] (fn [_] nil))
-(ci.job :clean [:quire/push] (fn [_] nil))
+(ci.job :a [:b :quire/push] (fn [] nil))
+(ci.job :b [:a :quire/push] (fn [] nil))
+(ci.job :clean [:quire/push] (fn [] nil))
"#,
);
let errs = validate(&jobs).unwrap_err();
@@ -622,10 +631,10 @@ mod tests {
let jobs = registered_jobs(
r#"
(local ci (require :quire.ci))
-(ci.job :a [:b :quire/push] (fn [_] nil))
-(ci.job :b [:a :quire/push] (fn [_] nil))
-(ci.job :c [:d :quire/push] (fn [_] nil))
-(ci.job :d [:c :quire/push] (fn [_] nil))
+(ci.job :a [:b :quire/push] (fn [] nil))
+(ci.job :b [:a :quire/push] (fn [] nil))
+(ci.job :c [:d :quire/push] (fn [] nil))
+(ci.job :d [:c :quire/push] (fn [] nil))
"#,
);
let errs = validate(&jobs).unwrap_err();
@@ -640,7 +649,7 @@ mod tests {
fn register_rejects_empty_inputs() {
let errors = registration_errors(
r#"(local ci (require :quire.ci))
-(ci.job :setup [] (fn [_] nil))"#,
+(ci.job :setup [] (fn [] nil))"#,
);
assert!(
errors.iter().any(
@@ -654,7 +663,7 @@ mod tests {
fn register_rejects_slash_in_job_id() {
let errors = registration_errors(
r#"(local ci (require :quire.ci))
-(ci.job :foo/bar [:quire/push] (fn [_] nil))"#,
+(ci.job :foo/bar [:quire/push] (fn [] nil))"#,
);
assert!(
errors.iter().any(
@@ -668,8 +677,8 @@ mod tests {
fn register_rejects_duplicate_job_id() {
let errors = registration_errors(
r#"(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))
-(ci.job :build [:quire/push] (fn [_] nil))"#,
+(ci.job :build [:quire/push] (fn [] nil))
+(ci.job :build [:quire/push] (fn [] nil))"#,
);
assert!(
errors.iter().any(
@@ -686,8 +695,8 @@ mod tests {
let jobs = registered_jobs(
r#"
(local ci (require :quire.ci))
-(ci.job :a [:b] (fn [_] nil))
-(ci.job :b [:a] (fn [_] nil))
+(ci.job :a [:b] (fn [] nil))
+(ci.job :b [:a] (fn [] nil))
"#,
);
let errs = validate(&jobs).unwrap_err();
@@ -708,7 +717,7 @@ mod tests {
// reaches the post-graph reachability check.
let jobs = registered_jobs(
r#"(local ci (require :quire.ci))
-(ci.job :orphan [:does-not-exist] (fn [_] nil))"#,
+(ci.job :orphan [:does-not-exist] (fn [] nil))"#,
);
let errs = validate(&jobs).unwrap_err();
assert!(
@@ -727,10 +736,10 @@ mod tests {
let jobs = registered_jobs(
r#"
(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [_] nil))
-(ci.job :b [:a] (fn [_] nil))
-(ci.job :c [:a] (fn [_] nil))
-(ci.job :d [:b :c] (fn [_] nil))"#,
+(ci.job :a [:quire/push] (fn [] nil))
+(ci.job :b [:a] (fn [] nil))
+(ci.job :c [:a] (fn [] nil))
+(ci.job :d [:b :c] (fn [] nil))"#,
);
assert!(validate(&jobs).is_ok());
}
@@ -744,7 +753,7 @@ mod tests {
let jobs = registered_jobs(
r#"
(local ci (require :quire.ci))
-(ci.job :orphan [:a :a] (fn [_] nil))"#,
+(ci.job :orphan [:a :a] (fn [] nil))"#,
);
let errs = validate(&jobs).unwrap_err();
assert!(
@@ -758,9 +767,9 @@ mod tests {
fn transitive_inputs_collects_direct_and_indirect() {
let pipeline = compile(
r#"(local ci (require :quire.ci))
-(ci.job :setup [:quire/push] (fn [_] nil))
-(ci.job :build [:setup] (fn [_] nil))
-(ci.job :test [:build :setup] (fn [_] nil))"#,
+(ci.job :setup [:quire/push] (fn [] nil))
+(ci.job :build [:setup] (fn [] nil))
+(ci.job :test [:build :setup] (fn [] nil))"#,
"ci.fnl",
)
.expect("compile should succeed");
@@ -791,7 +800,7 @@ mod tests {
fn transitive_inputs_excludes_self() {
let pipeline = compile(
r#"(local ci (require :quire.ci))
-(ci.job :only [:quire/push] (fn [_] nil))"#,
+(ci.job :only [:quire/push] (fn [] nil))"#,
"ci.fnl",
)
.expect("compile should succeed");
@@ -804,7 +813,7 @@ mod tests {
fn compile_registers_pipeline_image() {
let source = r#"(local ci (require :quire.ci))
(ci.image "alpine")
-(ci.job :build [:quire/push] (fn [_] nil))"#;
+(ci.job :build [:quire/push] (fn [] nil))"#;
let pipeline = compile(source, "ci.fnl").expect("compile should succeed");
assert_eq!(pipeline.image(), Some("alpine"));
}
@@ -812,7 +821,7 @@ mod tests {
#[test]
fn compile_succeeds_without_image() {
let source = r#"(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))"#;
+(ci.job :build [:quire/push] (fn [] nil))"#;
let pipeline = compile(source, "ci.fnl").expect("compile should succeed");
assert_eq!(pipeline.image(), None);
}
@@ -833,8 +842,8 @@ mod tests {
// errors short-circuit before structure checks run.
let result = compile(
r#"(local ci (require :quire.ci))
-(ci.job :setup [] (fn [_] nil))
-(ci.job :orphan [:does-not-exist] (fn [_] nil))"#,
+(ci.job :setup [] (fn [] nil))
+(ci.job :orphan [:does-not-exist] (fn [] nil))"#,
"ci.fnl",
);
let Err(CompileError::Pipeline(pe)) = result else {
@@ -861,7 +870,7 @@ mod tests {
let source = r#"(local ci (require :quire.ci))
(ci.image "alpine")
(ci.image "ubuntu")
-(ci.job :build [:quire/push] (fn [_] nil))"#;
+(ci.job :build [:quire/push] (fn [] nil))"#;
let result = compile(source, "ci.fnl");
assert!(result.is_err(), "duplicate image should fail");
let Err(e) = result else { unreachable!() };
diff --git a/quire-core/src/ci/registration.rs b/quire-core/src/ci/registration.rs
index d973f62..f0d7a14 100644
--- a/quire-core/src/ci/registration.rs
+++ b/quire-core/src/ci/registration.rs
@@ -52,7 +52,13 @@ pub fn register(fennel: &Fennel, source: &str, name: &str) -> CompileResult<Regi
image: image.clone(),
source: src.clone(),
},
- )
+ )?;
+ // Declare `runtime` as a known global so Fennel compilation
+ // doesn't reject `runtime.sh` / `runtime.secret` / `runtime.jobs`
+ // as unknown identifiers. The real value is installed at execution
+ // time by `RuntimeHandle::install`.
+ lua.globals().set("runtime", lua.create_table()?)?;
+ Ok(())
})?;
// Remove the Registration app data so `ci.image`/`ci.job` calls at
@@ -187,6 +193,26 @@ fn register_job(
return Ok(());
}
+ // Arity check: one-arg run-fns use the old `(fn [{: sh}] …)`
+ // pattern. Reject them so users get a clear message instead of
+ // a runtime-nil panic when the ambient runtime is absent.
+ //
+ // Fail open on `debug.getinfo` errors — if the debug library is
+ // unavailable or the call shape changes, treat the function as
+ // zero-arg and let the user surface any real arity mismatch at
+ // execution time.
+ let nparams: u32 = lua
+ .load("return debug.getinfo(...).nparams")
+ .call(&run_fn)
+ .unwrap_or(0);
+ if nparams != 0 {
+ let span = pipeline::span_for_line(&r.source, line);
+ r.errors
+ .borrow_mut()
+ .push(DefinitionError::ArityViolation { job_id: id, span });
+ return Ok(());
+ }
+
match Job::new(id, inputs, RunFn::Lua(run_fn), line, &r.source) {
Ok(job) => r.add_job(job, line),
Err(e) => r.errors.borrow_mut().push(e),
diff --git a/quire-core/src/ci/runtime.rs b/quire-core/src/ci/runtime.rs
index 1f0a60e..0a3c7bd 100644
--- a/quire-core/src/ci/runtime.rs
+++ b/quire-core/src/ci/runtime.rs
@@ -364,33 +364,29 @@ impl Runtime {
}
}
-/// `IntoLua` carrier for an `Rc<Runtime>`. Stows the Rc on the VM as
-/// app data and returns the handle table — `{sh, secret, jobs}`.
+/// Install the ambient `runtime` global on the Lua VM.
+///
+/// Stows the `Rc<Runtime>` as app data and sets a `runtime` global
+/// table holding `sh`, `secret`, and `jobs` — closures over the
+/// active runtime. An `__index` metatable raises a clear error for
+/// any other key. The active job slot is set/cleared by the executor
+/// around each run-fn invocation via [`Runtime::enter_job`] /
+/// [`Runtime::leave_job`].
pub struct RuntimeHandle(pub Rc<Runtime>);
-impl IntoLua for RuntimeHandle {
- // Errors raised by the closures below cross the mlua boundary via
- // `Error::external`, which erases them to
- // `Box<dyn Error + Send + Sync>`. The `std::error::Error` source
- // chain is preserved, but miette `Diagnostic` metadata (codes,
- // labels, source spans) does not survive the round trip — the
- // resulting `mlua::Error` becomes the `#[source]` of
- // `Error::JobFailed` at the executor, which only renders the chain
- // as plain `Display`. Don't reach for richer error types here
- // expecting them to render: rephrase the Display string to carry
- // what the user needs to see.
- fn into_lua(self, lua: &Lua) -> mlua::Result<mlua::Value> {
- // Pull the installed runtime out of `lua`'s app data, or
- // surface a Lua error. Every adapter below needs this.
+impl RuntimeHandle {
+ /// Install the ambient runtime on the Lua VM. Call once before
+ /// executing any run-fns.
+ pub fn install(self, lua: &Lua) -> mlua::Result<()> {
fn runtime(lua: &Lua) -> mlua::Result<mlua::AppDataRef<'_, Rc<Runtime>>> {
lua.app_data_ref::<Rc<Runtime>>()
.ok_or_else(|| mlua::Error::external("runtime not installed on Lua VM"))
}
lua.set_app_data(self.0);
- let table = lua.create_table()?;
+ let rt = lua.create_table()?;
- table.set(
+ rt.set(
"sh",
lua.create_function(|lua, (cmd, opts): (Cmd, Option<ShOpts>)| {
let rt = runtime(lua)?;
@@ -401,7 +397,7 @@ impl IntoLua for RuntimeHandle {
})?,
)?;
- table.set(
+ rt.set(
"secret",
lua.create_function(|lua, name: String| {
let rt = runtime(lua)?;
@@ -409,13 +405,15 @@ impl IntoLua for RuntimeHandle {
})?,
)?;
- table.set(
+ rt.set(
"jobs",
lua.create_function(|lua, name: String| {
let rt = runtime(lua)?;
let calling = rt.current_job.borrow();
let calling = calling.as_ref().ok_or_else(|| {
- mlua::Error::external("(jobs ...) called outside a job's run-fn")
+ mlua::Error::external(
+ "runtime accessed outside a job — primitives are only available while a run-fn is executing",
+ )
})?;
// Runtime::new builds a view for every job and
// enter_job is the only setter for current_job, so a
@@ -438,7 +436,22 @@ impl IntoLua for RuntimeHandle {
})?,
)?;
- table.into_lua(lua)
+ // Catch typos: any key other than sh/secret/jobs raises
+ // a clear error instead of returning nil.
+ let mt = lua.create_table()?;
+ mt.set(
+ "__index",
+ lua.create_function(
+ |_lua, (_table, key): (mlua::Table, String)| -> mlua::Result<mlua::Value> {
+ Err(mlua::Error::external(format!(
+ "unknown runtime primitive '{key}' — expected sh, secret, or jobs"
+ )))
+ },
+ )?,
+ )?;
+ rt.set_metatable(Some(mt))?;
+ lua.globals().set("runtime", rt)?;
+ Ok(())
}
}
@@ -608,8 +621,8 @@ mod tests {
RunFn::Rust(_) => panic!("expected RunFn::Lua for test setup"),
};
let runtime = Rc::new(Runtime::for_test(pipeline, secrets));
- let _ = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
+ RuntimeHandle(runtime.clone())
+ .install(runtime.lua())
.expect("install runtime");
(runtime, run_fn)
}
@@ -622,13 +635,10 @@ mod tests {
SecretString::from("ghp_test_value"),
);
let source = r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push] (fn [{: secret}] (secret :github_token)))"#;
- let (runtime, run_fn) = rt(source, secrets);
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
+(ci.job :grab [:quire/push] (fn [] (runtime.secret :github_token)))"#;
+ let (_runtime, run_fn) = rt(source, secrets);
let token: String = run_fn
- .call(handle)
+ .call::<String>(())
.expect("run_fn should return the secret value");
assert_eq!(token, "ghp_test_value");
}
@@ -636,12 +646,9 @@ mod tests {
#[test]
fn secret_errors_for_unknown_name() {
let source = r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push] (fn [{: secret}] (secret :missing)))"#;
- let (runtime, run_fn) = rt(source, HashMap::new());
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let err = run_fn.call::<mlua::Value>(handle).unwrap_err();
+(ci.job :grab [:quire/push] (fn [] (runtime.secret :missing)))"#;
+ let (_runtime, run_fn) = rt(source, HashMap::new());
+ let err = run_fn.call::<mlua::Value>(()).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("unknown secret") && msg.contains("missing"),
@@ -656,7 +663,7 @@ mod tests {
let (runtime, run_fn) = rt(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh ["echo" "hi"])))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
HashMap::new(),
);
runtime.set_event_callback(Box::new(move |event| match event {
@@ -669,10 +676,7 @@ mod tests {
}));
*runtime.current_job.borrow_mut() = Some("go".to_string());
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let _: mlua::Value = run_fn.call(handle).expect("sh call");
+ let _: mlua::Value = run_fn.call(()).expect("sh call");
let calls = received.borrow();
assert_eq!(calls.len(), 2, "expected 2 events, got: {calls:?}");
@@ -688,16 +692,13 @@ mod tests {
fn sh_writes_cri_log_inline() {
let (runtime, run_fn) = rt(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh ["echo" "hi"])))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
HashMap::new(),
);
let log_dir = runtime.log_dir().to_path_buf();
*runtime.current_job.borrow_mut() = Some("go".to_string());
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let _: mlua::Value = run_fn.call(handle).expect("sh call");
+ let _: mlua::Value = run_fn.call(()).expect("sh call");
let log_path = log_dir.join("jobs").join("go").join("sh-1.log");
assert!(log_path.exists(), "expected sh-1.log at {log_path:?}");
@@ -715,7 +716,7 @@ mod tests {
let (runtime, run_fn) = rt(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh ["echo" "hi"])))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
HashMap::new(),
);
runtime.set_event_callback(Box::new(move |_event| {
@@ -723,23 +724,17 @@ mod tests {
}));
// No enter_job — current_job stays None.
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let _: mlua::Value = run_fn.call(handle).expect("sh call");
+ let _: mlua::Value = run_fn.call(()).expect("sh call");
assert_eq!(*count.borrow(), 0, "callback should not fire outside a job");
}
/// Build a pipeline whose single job's run-fn invokes `(sh …)`,
- /// invoke it with the runtime handle, and decode the resulting Lua
+ /// invoke it with the ambient runtime, and decode the resulting Lua
/// table as ShOutput.
fn run_sh_via_job(source: &str) -> ShOutput {
let (runtime, run_fn) = rt(source, HashMap::new());
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let value: mlua::Value = run_fn.call(handle).expect("sh call should return a value");
+ let value: mlua::Value = run_fn.call(()).expect("sh call should return a value");
runtime.lua().from_value(value).expect("decode ShOutput")
}
@@ -752,20 +747,17 @@ mod tests {
);
let source = r#"(local ci (require :quire.ci))
(ci.job :go [:quire/push]
- (fn [{: sh : secret}]
- (let [tok (secret :github_token)]
- (sh ["echo" tok]))))"#;
+ (fn []
+ (let [tok (runtime.secret :github_token)]
+ (runtime.sh ["echo" tok]))))"#;
let (runtime, run_fn) = rt(source, secrets);
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
// Mark a current job so sh records into outputs. for_test seeds
// an empty inputs map, so enter_job would panic; bypass the
// assertion by writing the field directly.
*runtime.current_job.borrow_mut() = Some("go".to_string());
- let value: mlua::Value = run_fn.call(handle).expect("sh call");
+ let value: mlua::Value = run_fn.call(()).expect("sh call");
let returned: ShOutput = runtime.lua().from_value(value).expect("decode");
// The Lua caller still sees the raw value — echo printed it.
@@ -798,7 +790,7 @@ mod tests {
fn sh_runs_argv_and_captures_stdout() {
let r = run_sh_via_job(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh ["echo" "hello"])))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh ["echo" "hello"])))"#,
);
assert_eq!(r.exit, 0);
assert_eq!(r.stdout, "hello\n");
@@ -809,7 +801,7 @@ mod tests {
fn sh_runs_string_under_shell() {
let r = run_sh_via_job(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh "echo hello | tr a-z A-Z")))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh "echo hello | tr a-z A-Z")))"#,
);
assert_eq!(r.exit, 0);
assert_eq!(r.stdout, "HELLO\n");
@@ -819,7 +811,7 @@ mod tests {
fn sh_reports_nonzero_exit_without_erroring() {
let r = run_sh_via_job(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh "exit 7")))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh "exit 7")))"#,
);
assert_eq!(r.exit, 7);
}
@@ -833,8 +825,8 @@ mod tests {
let r = run_sh_via_job(
r#"(local ci (require :quire.ci))
(ci.job :go [:quire/push]
- (fn [{: sh}]
- (sh "echo $CI_SH_INHERITED_TEST $CI_SH_OVERRIDE_TEST"
+ (fn []
+ (runtime.sh "echo $CI_SH_INHERITED_TEST $CI_SH_OVERRIDE_TEST"
{:env {:CI_SH_OVERRIDE_TEST "from-opts"}})))"#,
);
assert_eq!(r.exit, 0);
@@ -843,15 +835,12 @@ mod tests {
#[test]
fn sh_rejects_unknown_opt_key() {
- let (runtime, run_fn) = rt(
+ let (_runtime, run_fn) = rt(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh "echo hi" {:cwdir "/tmp"})))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh "echo hi" {:cwdir "/tmp"})))"#,
HashMap::new(),
);
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let err = run_fn.call::<mlua::Value>(handle).unwrap_err();
+ let err = run_fn.call::<mlua::Value>(()).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("unknown field") && msg.contains("cwdir"),
@@ -861,15 +850,12 @@ mod tests {
#[test]
fn sh_rejects_non_sequence_table_as_cmd() {
- let (runtime, run_fn) = rt(
+ let (_runtime, run_fn) = rt(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh {:env {:FOO "bar"}})))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh {:env {:FOO "bar"}})))"#,
HashMap::new(),
);
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let err = run_fn.call::<mlua::Value>(handle).unwrap_err();
+ let err = run_fn.call::<mlua::Value>(()).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("sequence"),
@@ -879,15 +865,12 @@ mod tests {
#[test]
fn sh_rejects_empty_argv() {
- let (runtime, run_fn) = rt(
+ let (_runtime, run_fn) = rt(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh [])))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh [])))"#,
HashMap::new(),
);
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let err = run_fn.call::<mlua::Value>(handle).unwrap_err();
+ let err = run_fn.call::<mlua::Value>(()).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("empty"),
@@ -897,15 +880,12 @@ mod tests {
#[test]
fn sh_rejects_number_as_cmd() {
- let (runtime, run_fn) = rt(
+ let (_runtime, run_fn) = rt(
r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [{: sh}] (sh 42)))"#,
+(ci.job :go [:quire/push] (fn [] (runtime.sh 42)))"#,
HashMap::new(),
);
- let handle = RuntimeHandle(runtime.clone())
- .into_lua(runtime.lua())
- .expect("install runtime");
- let err = run_fn.call::<mlua::Value>(handle).unwrap_err();
+ let err = run_fn.call::<mlua::Value>(()).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("string or sequence"),
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index 922ee8c..f7471fb 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -310,7 +310,7 @@ mod tests {
#[test]
fn ci_pipeline_returns_pipeline_when_ci_fnl_present() {
let source = r#"(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))"#;
+(ci.job :build [:quire/push] (fn [] nil))"#;
let (_dir, quire, name) = bare_repo_with_ci(source);
let repo = quire.repo(&name).expect("repo");
let ci = repo.ci();
@@ -344,7 +344,7 @@ mod tests {
#[test]
fn ci_source_reads_file_at_sha() {
- let source = "(local ci (require :quire.ci))\n(ci.job :x [:quire/push] (fn [_] nil))";
+ let source = "(local ci (require :quire.ci))\n(ci.job :x [:quire/push] (fn [] nil))";
let (_dir, quire, name) = bare_repo_with_ci(source);
let repo = quire.repo(&name).expect("repo");
let ci = repo.ci();
@@ -359,7 +359,7 @@ mod tests {
#[test]
fn trigger_creates_run_and_completes() {
let source = r#"(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))"#;
+(ci.job :build [:quire/push] (fn [] nil))"#;
let (_dir, quire, name) = bare_repo_with_ci(source);
let repo = quire.repo(&name).expect("repo");
let sha = head_sha(&repo);
@@ -417,7 +417,7 @@ mod tests {
#[test]
fn trigger_errors_on_invalid_pipeline() {
- let source = "(local ci (require :quire.ci))\n(ci.job :a [] (fn [_] nil))";
+ let source = "(local ci (require :quire.ci))\n(ci.job :a [] (fn [] nil))";
let (_dir, quire, name) = bare_repo_with_ci(source);
let repo = quire.repo(&name).expect("repo");
let sha = head_sha(&repo);
@@ -480,7 +480,7 @@ mod tests {
#[test]
fn trigger_processes_multiple_refs() {
let source = r#"(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))"#;
+(ci.job :build [:quire/push] (fn [] nil))"#;
let (_dir, quire, name) = bare_repo_with_ci(source);
let repo = quire.repo(&name).expect("repo");
let sha = head_sha(&repo);
@@ -509,7 +509,7 @@ mod tests {
#[test]
fn ci_source_errors_on_invalid_sha() {
let source = r#"(local ci (require :quire.ci))
-(ci.job :build [:quire/push] (fn [_] nil))"#;
+(ci.job :build [:quire/push] (fn [] nil))"#;
let (_dir, quire, name) = bare_repo_with_ci(source);
let repo = quire.repo(&name).expect("repo");
let ci = repo.ci();
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index c150876..bc05621 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -10,13 +10,12 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use jiff::Timestamp;
-use mlua::IntoLua;
-
-use super::error::{Error, Result};
use quire_core::ci::pipeline::{Pipeline, RunFn};
use quire_core::ci::runtime::{Runtime, RuntimeHandle, ShOutput};
use quire_core::secret::SecretString;
+use super::error::{Error, Result};
+
pub use quire_core::ci::run::RunMeta;
/// How a run dispatches its pipeline.
@@ -235,8 +234,8 @@ impl Run {
));
let lua = runtime.lua();
- let rt_value = RuntimeHandle(runtime.clone())
- .into_lua(lua)
+ RuntimeHandle(runtime.clone())
+ .install(lua)
.expect("install runtime on Lua VM");
let mut failed_job: Option<(String, Error)> = None;
@@ -260,7 +259,7 @@ impl Run {
runtime.enter_job(job_id);
let result: Result<()> = (|| match run_fn {
RunFn::Lua(f) => {
- let _: mlua::Value = f.call(rt_value.clone())?;
+ f.call::<mlua::Value>(())?;
Ok(())
}
RunFn::Rust(f) => f(&runtime).map_err(Into::into),
@@ -298,7 +297,6 @@ impl Run {
// Drop the runtime *before* the final transition. In docker
// mode this fires `DockerLifecycle::drop`, which stamps
// `container_stopped_at` in the database.
- drop(rt_value);
let _ = lua; // release the Lua borrow tied to `runtime`.
drop(runtime);
@@ -887,7 +885,7 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :pwd [:quire/push] (fn [{: sh}] (sh ["ls"])))"#,
+(ci.job :pwd [:quire/push] (fn [] (runtime.sh ["ls"])))"#,
);
let outputs = run
@@ -915,8 +913,8 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [{: sh}] (sh ["echo" "from-a"])))
-(ci.job :b [:a] (fn [{: sh}] (sh ["echo" "from-b"])))"#,
+(ci.job :a [:quire/push] (fn [] (runtime.sh ["echo" "from-a"])))
+(ci.job :b [:a] (fn [] (runtime.sh ["echo" "from-b"])))"#,
);
let run_id = run.id().to_string();
@@ -951,8 +949,8 @@ mod tests {
let log_str = log.to_string_lossy();
let source = format!(
r#"(local ci (require :quire.ci))
-(ci.job :b [:a] (fn [{{: sh}}] (sh (.. "echo b >> {log}"))))
-(ci.job :a [:quire/push] (fn [{{: sh}}] (sh (.. "echo a >> {log}"))))"#,
+(ci.job :b [:a] (fn [] (runtime.sh (.. "echo b >> {log}"))))
+(ci.job :a [:quire/push] (fn [] (runtime.sh (.. "echo a >> {log}"))))"#,
log = log_str
);
let pipeline = load(&source);
@@ -977,8 +975,8 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [_] (error "boom")))
-(ci.job :b [:a] (fn [{: sh}] (sh ["echo" "should-not-run"])))"#,
+(ci.job :a [:quire/push] (fn [] (error "boom")))
+(ci.job :b [:a] (fn [] (runtime.sh ["echo" "should-not-run"])))"#,
);
let run_id = run.id().to_string();
@@ -1006,9 +1004,9 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
(ci.job :grab [:quire/push]
- (fn [{: sh : jobs}]
- (let [push (jobs :quire/push)]
- (sh ["echo" push.sha push.ref]))))"#,
+ (fn []
+ (let [push (runtime.jobs :quire/push)]
+ (runtime.sh ["echo" push.sha push.ref]))))"#,
);
let outputs = run
@@ -1033,11 +1031,11 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [_] nil))
+(ci.job :a [:quire/push] (fn [] nil))
(ci.job :b [:a]
- (fn [{: sh : jobs}]
- (let [push (jobs :quire/push)]
- (sh ["echo" push.sha]))))"#,
+ (fn []
+ (let [push (runtime.jobs :quire/push)]
+ (runtime.sh ["echo" push.sha]))))"#,
);
let outputs = run
@@ -1062,7 +1060,7 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push] (fn [{: jobs}] (jobs :nope)))"#,
+(ci.job :grab [:quire/push] (fn [] (runtime.jobs :nope)))"#,
);
let err = run
@@ -1092,8 +1090,8 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :peer [:quire/push] (fn [_] nil))
-(ci.job :grab [:quire/push] (fn [{: jobs}] (jobs :peer)))"#,
+(ci.job :peer [:quire/push] (fn [] nil))
+(ci.job :grab [:quire/push] (fn [] (runtime.jobs :peer)))"#,
);
let err = run
@@ -1122,7 +1120,7 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push] (fn [{: jobs}] (jobs :grab)))"#,
+(ci.job :grab [:quire/push] (fn [] (runtime.jobs :grab)))"#,
);
let err = run
@@ -1151,11 +1149,11 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [_] nil))
+(ci.job :a [:quire/push] (fn [] nil))
(ci.job :b [:a]
- (fn [{: sh : jobs}]
- (let [a-outputs (jobs :a)]
- (sh ["echo" (tostring a-outputs)]))))"#,
+ (fn []
+ (let [a-outputs (runtime.jobs :a)]
+ (runtime.sh ["echo" (tostring a-outputs)]))))"#,
);
let outputs = run
@@ -1179,7 +1177,7 @@ mod tests {
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :greet [:quire/push] (fn [{: sh}] (sh ["echo" "hello"])))"#,
+(ci.job :greet [:quire/push] (fn [] (runtime.sh ["echo" "hello"])))"#,
);
let run_id = run.id().to_string();
@@ -1224,8 +1222,8 @@ mod tests {
// `a` succeeds, `b` fails — log for `a` should still be written.
let pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [{: sh}] (sh ["echo" "from-a"])))
-(ci.job :b [:a] (fn [_] (error "boom")))"#,
+(ci.job :a [:quire/push] (fn [] (runtime.sh ["echo" "from-a"])))
+(ci.job :b [:a] (fn [] (error "boom")))"#,
);
let run_id = run.id().to_string();
@@ -1259,7 +1257,7 @@ mod tests {
r#"(local ci (require :quire.ci))
(ci.image "alpine")
(ci.job :bad [:quire/push]
- (fn [_]
+ (fn []
(ci.image "sneaky")))"#,
);
@@ -1292,7 +1290,7 @@ mod tests {
let mut pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :only [:quire/push] (fn [_] nil))"#,
+(ci.job :only [:quire/push] (fn [] nil))"#,
);
let called = Rc::new(Cell::new(false));
@@ -1320,7 +1318,7 @@ mod tests {
let mut pipeline = load(
r#"(local ci (require :quire.ci))
-(ci.job :boom [:quire/push] (fn [_] nil))"#,
+(ci.job :boom [:quire/push] (fn [] nil))"#,
);
pipeline.replace_first_run_fn(RunFn::Rust(Rc::new(|_rt| {