Move runtime under quire.ci, mutate the stub in place
`RuntimeHandle::install` used to create a fresh table and assign it to
the `runtime` global, hiding that the global and
`package.loaded["quire.runtime"]` had drifted apart since registration.
Now one stub lives at `package.loaded["quire.ci"].runtime` — seeded as
a placeholder by `Fennel::new`, carried forward when registration
overwrites `quire.ci`, mutated by `install`, and cleared by `uninstall`.
Destructuring `(local {: runtime} (require :quire.ci))` works because
the captured reference is the same table the install populates. No
runtime global, no separate `quire.runtime` module; `defrun`, stdlib,
and the mirror shim all reach the runtime through `(require :quire.ci)`.
Assisted-by: Claude Opus 4.7 via Claude Code
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index c4eda5b..818b79c 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -344,7 +344,7 @@ fn run_pipeline(
}
}
- lua.remove_app_data::<Rc<Runtime>>();
+ RuntimeHandle::uninstall(lua).expect("uninstall runtime");
if let Some((_, err)) = failed_job {
return Err(err.into());
diff --git a/quire-core/src/ci/macros.fnl b/quire-core/src/ci/macros.fnl
index c7251df..831a3b5 100644
--- a/quire-core/src/ci/macros.fnl
+++ b/quire-core/src/ci/macros.fnl
@@ -2,9 +2,9 @@
;;
;; `defrun` is sugar for the common run-fn shape: a zero-arg function
;; whose body needs `sh` / `secret` / `jobs` / `mirror` from the
-;; ambient `runtime` global. Writing `(let [{: sh} runtime] …)` at the
-;; top of every job becomes the macro itself, with the destructure
-;; pattern as the apparent argument list.
+;; runtime. Writing `(let [{: sh} (. (require :quire.ci) :runtime)] …)`
+;; at the top of every job becomes the macro itself, with the
+;; destructure pattern as the apparent argument list.
;;
;; (defrun [{: sh : jobs}]
;; (let [push (jobs :quire/push)]
@@ -13,7 +13,7 @@
;; expands to:
;;
;; (fn []
-;; (let [{: sh : jobs} runtime]
+;; (let [{: sh : jobs} (. (require :quire.ci) :runtime)]
;; (let [push (jobs :quire/push)]
;; (sh ["cargo" "test"]))))
;;
@@ -30,7 +30,7 @@
`(fn []
,(unpack body))
`(fn []
- (let [,(. arglist 1) runtime]
+ (let [,(. arglist 1) (. (require :quire.ci) :runtime)]
,(unpack body))))))
{: defrun}
diff --git a/quire-core/src/ci/mirror.rs b/quire-core/src/ci/mirror.rs
index 08b1582..d9f468a 100644
--- a/quire-core/src/ci/mirror.rs
+++ b/quire-core/src/ci/mirror.rs
@@ -95,14 +95,13 @@ pub fn register(lua: &Lua, (url, opts): (String, mlua::Table)) -> mlua::Result<(
let refs = Rc::new(refs);
let run_fn = lua.create_function(move |lua, ()| {
- let mirror: mlua::Function = lua
+ let loaded: mlua::Table = lua
.globals()
.get::<mlua::Table>("package")?
- .get::<mlua::Table>("loaded")?
- .get::<mlua::Table>("quire.stdlib")?
- .get("mirror")?;
+ .get::<mlua::Table>("loaded")?;
+ let mirror: mlua::Function = loaded.get::<mlua::Table>("quire.stdlib")?.get("mirror")?;
- let runtime: mlua::Table = lua.globals().get("runtime")?;
+ let runtime: mlua::Table = loaded.get::<mlua::Table>("quire.ci")?.get("runtime")?;
let push: mlua::Table = runtime.get::<mlua::Function>("jobs")?.call("quire/push")?;
let pushed_ref: String = push.get("ref")?;
diff --git a/quire-core/src/ci/pipeline.rs b/quire-core/src/ci/pipeline.rs
index b43e7ae..140fb12 100644
--- a/quire-core/src/ci/pipeline.rs
+++ b/quire-core/src/ci/pipeline.rs
@@ -55,7 +55,7 @@ pub enum DefinitionError {
},
#[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."
+ "Job '{job_id}' run-fn takes an argument — run-fns must be zero-arg `(fn [] …)`; bind the runtime via `(local runtime (require :quire.runtime))` or destructure it from `(require :quire.ci)` instead of destructuring a handle."
)]
ArityViolation {
job_id: String,
diff --git a/quire-core/src/ci/registration.rs b/quire-core/src/ci/registration.rs
index 3832410..ee84ead 100644
--- a/quire-core/src/ci/registration.rs
+++ b/quire-core/src/ci/registration.rs
@@ -53,11 +53,6 @@ pub fn register(fennel: &Fennel, source: &str, name: &str) -> CompileResult<Regi
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(())
})?;
@@ -111,6 +106,18 @@ impl IntoLua for Registration {
table.set("job", lua.create_function(register_job)?)?;
table.set("image", lua.create_function(register_image)?)?;
table.set("mirror", lua.create_function(mirror::register)?)?;
+ // Carry forward the runtime stub from the placeholder
+ // `quire.ci` table seeded by `Fennel::new`. `register_module`
+ // overwrites `package.loaded["quire.ci"]` with the value we
+ // return; reusing the existing stub keeps references captured
+ // before registration (and any held by previously preloaded
+ // modules like `quire.stdlib`) pointing at the same Lua table
+ // that `RuntimeHandle::install` mutates.
+ let package: mlua::Table = lua.globals().get("package")?;
+ let loaded: mlua::Table = package.get("loaded")?;
+ let placeholder: mlua::Table = loaded.get("quire.ci")?;
+ let runtime: mlua::Table = placeholder.get("runtime")?;
+ table.set("runtime", runtime)?;
table.into_lua(lua)
}
}
diff --git a/quire-core/src/ci/runtime.rs b/quire-core/src/ci/runtime.rs
index 1234446..4fdb60a 100644
--- a/quire-core/src/ci/runtime.rs
+++ b/quire-core/src/ci/runtime.rs
@@ -364,19 +364,28 @@ impl Runtime {
}
}
-/// Install the ambient `runtime` global on the Lua VM.
+/// Install the runtime primitives 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`].
+/// Stows the `Rc<Runtime>` as app data and populates the stub seeded
+/// at `package.loaded["quire.runtime"]` by
+/// [`crate::fennel::Fennel::new`] with `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`].
+///
+/// The stub is mutated in place rather than replaced so that
+/// references captured during registration — e.g.
+/// `(require :quire.runtime)` directly, or
+/// `(local {: runtime} (require :quire.ci))` — see the populated
+/// table at call time. There is no `runtime` global; user code must
+/// reach the primitives via one of the require paths.
pub struct RuntimeHandle(pub Rc<Runtime>);
impl RuntimeHandle {
- /// Install the ambient runtime on the Lua VM. Call once before
- /// executing any run-fns.
+ /// Install the runtime on the Lua VM. Call once before executing
+ /// any run-fns; pair with [`RuntimeHandle::uninstall`] when the
+ /// run is done.
pub fn install(self, lua: &Lua) -> mlua::Result<()> {
fn runtime(lua: &Lua) -> mlua::Result<mlua::AppDataRef<'_, Rc<Runtime>>> {
lua.app_data_ref::<Rc<Runtime>>()
@@ -384,7 +393,7 @@ impl RuntimeHandle {
}
lua.set_app_data(self.0);
- let rt = lua.create_table()?;
+ let rt: mlua::Table = runtime_stub(lua)?;
rt.set(
"sh",
@@ -450,17 +459,38 @@ impl RuntimeHandle {
)?,
)?;
rt.set_metatable(Some(mt))?;
- lua.globals().set("runtime", rt.clone())?;
- // Mirror into `package.loaded["quire.runtime"]` so library
- // code can do `(let [{: sh} (require :quire.runtime)] …)`.
- // Same table — destructures and ambient access stay in sync.
- let package: mlua::Table = lua.globals().get("package")?;
- let loaded: mlua::Table = package.get("loaded")?;
- loaded.set("quire.runtime", rt)?;
+ Ok(())
+ }
+
+ /// Tear down the ambient runtime: clear `sh`/`secret`/`jobs` from
+ /// the runtime table, drop the metatable, and remove the
+ /// `Rc<Runtime>` app data. Idempotent — calling twice is a no-op.
+ ///
+ /// In practice the Lua VM is dropped right after a run, so this
+ /// is hygiene rather than necessity; pair it with `install` so
+ /// the install/uninstall lifecycle is explicit at the call site.
+ pub fn uninstall(lua: &Lua) -> mlua::Result<()> {
+ let rt: mlua::Table = runtime_stub(lua)?;
+ rt.set("sh", mlua::Value::Nil)?;
+ rt.set("secret", mlua::Value::Nil)?;
+ rt.set("jobs", mlua::Value::Nil)?;
+ rt.set_metatable(None)?;
+ lua.remove_app_data::<Rc<Runtime>>();
Ok(())
}
}
+/// The runtime stub at `package.loaded["quire.ci"].runtime`. Seeded
+/// as a placeholder by `Fennel::new`, threaded through
+/// `registration::register`, mutated by `install`, cleared by
+/// `uninstall`.
+fn runtime_stub(lua: &Lua) -> mlua::Result<mlua::Table> {
+ let package: mlua::Table = lua.globals().get("package")?;
+ let loaded: mlua::Table = package.get("loaded")?;
+ let ci: mlua::Table = loaded.get("quire.ci")?;
+ ci.get::<mlua::Table>("runtime")
+}
+
/// The two valid shapes of `cmd` for `(sh cmd …)`. A bare string
/// runs under `sh -c`; a sequence runs as argv with no shell.
///
@@ -640,8 +670,8 @@ mod tests {
"github_token".to_string(),
SecretString::from("ghp_test_value"),
);
- let source = r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push] (fn [] (runtime.secret :github_token)))"#;
+ let source = r#"(local {: job : runtime} (require :quire.ci))
+(job :grab [:quire/push] (fn [] (runtime.secret :github_token)))"#;
let (_runtime, run_fn) = rt(source, secrets);
let token: String = run_fn
.call::<String>(())
@@ -649,10 +679,55 @@ mod tests {
assert_eq!(token, "ghp_test_value");
}
+ #[test]
+ fn runtime_destructured_from_quire_ci_resolves_after_install() {
+ // (local {: runtime} (require :quire.ci)) must bind to the
+ // same table that `RuntimeHandle::install` mutates in place,
+ // so `runtime.secret` works inside a run-fn.
+ let mut secrets = HashMap::new();
+ secrets.insert("k".to_string(), SecretString::from("v"));
+ let source = r#"(local {: job : runtime} (require :quire.ci))
+(job :grab [:quire/push] (fn [] (runtime.secret :k)))"#;
+ let (_runtime, run_fn) = rt(source, secrets);
+ let token: String = run_fn.call::<String>(()).expect("run_fn");
+ assert_eq!(token, "v");
+ }
+
+ #[test]
+ fn uninstall_clears_runtime_table_and_app_data() {
+ let source = r#"(local {: job : runtime} (require :quire.ci))
+(job :grab [:quire/push] (fn [] (runtime.secret :anything)))"#;
+ let (runtime, _run_fn) = rt(source, HashMap::new());
+ let lua = runtime.lua();
+ RuntimeHandle::uninstall(lua).expect("uninstall");
+
+ let rt: mlua::Table = runtime_stub(lua).expect("runtime stub");
+ assert!(matches!(
+ rt.get::<mlua::Value>("sh").expect("sh"),
+ mlua::Value::Nil
+ ));
+ assert!(matches!(
+ rt.get::<mlua::Value>("secret").expect("secret"),
+ mlua::Value::Nil
+ ));
+ assert!(matches!(
+ rt.get::<mlua::Value>("jobs").expect("jobs"),
+ mlua::Value::Nil
+ ));
+ assert!(rt.metatable().is_none(), "metatable should be cleared");
+ assert!(
+ lua.app_data_ref::<Rc<Runtime>>().is_none(),
+ "app data should be removed"
+ );
+
+ // Idempotent.
+ RuntimeHandle::uninstall(lua).expect("uninstall twice");
+ }
+
#[test]
fn secret_errors_for_unknown_name() {
- let source = r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push] (fn [] (runtime.secret :missing)))"#;
+ let source = r#"(local {: job : runtime} (require :quire.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();
@@ -668,8 +743,8 @@ mod tests {
let received_clone = received.clone();
let (runtime, run_fn) = rt(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
HashMap::new(),
);
runtime.set_event_callback(Box::new(move |event| match event {
@@ -697,8 +772,8 @@ mod tests {
#[test]
fn sh_writes_cri_log_inline() {
let (runtime, run_fn) = rt(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
HashMap::new(),
);
let log_dir = runtime.log_dir().to_path_buf();
@@ -721,8 +796,8 @@ mod tests {
let count_clone = count.clone();
let (runtime, run_fn) = rt(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh ["echo" "hi"])))"#,
HashMap::new(),
);
runtime.set_event_callback(Box::new(move |_event| {
@@ -751,8 +826,8 @@ mod tests {
"github_token".to_string(),
SecretString::from("ghp_long_secret_value"),
);
- let source = r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push]
+ let source = r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push]
(fn []
(let [tok (runtime.secret :github_token)]
(runtime.sh ["echo" tok]))))"#;
@@ -795,8 +870,8 @@ mod tests {
#[test]
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 [] (runtime.sh ["echo" "hello"])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh ["echo" "hello"])))"#,
);
assert_eq!(r.exit, 0);
assert_eq!(r.stdout, "hello\n");
@@ -806,8 +881,8 @@ mod tests {
#[test]
fn sh_runs_string_under_shell() {
let r = run_sh_via_job(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [] (runtime.sh "echo hello | tr a-z A-Z")))"#,
+ r#"(local {: job : runtime} (require :quire.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");
@@ -816,8 +891,8 @@ mod tests {
#[test]
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 [] (runtime.sh "exit 7")))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh "exit 7")))"#,
);
assert_eq!(r.exit, 7);
}
@@ -829,8 +904,8 @@ mod tests {
std::env::set_var("CI_SH_INHERITED_TEST", "from-parent");
}
let r = run_sh_via_job(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push]
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push]
(fn []
(runtime.sh "echo $CI_SH_INHERITED_TEST $CI_SH_OVERRIDE_TEST"
{:env {:CI_SH_OVERRIDE_TEST "from-opts"}})))"#,
@@ -842,8 +917,8 @@ mod tests {
#[test]
fn sh_rejects_unknown_opt_key() {
let (_runtime, run_fn) = rt(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [] (runtime.sh "echo hi" {:cwdir "/tmp"})))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh "echo hi" {:cwdir "/tmp"})))"#,
HashMap::new(),
);
let err = run_fn.call::<mlua::Value>(()).unwrap_err();
@@ -857,8 +932,8 @@ mod tests {
#[test]
fn sh_rejects_non_sequence_table_as_cmd() {
let (_runtime, run_fn) = rt(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [] (runtime.sh {:env {:FOO "bar"}})))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh {:env {:FOO "bar"}})))"#,
HashMap::new(),
);
let err = run_fn.call::<mlua::Value>(()).unwrap_err();
@@ -872,8 +947,8 @@ mod tests {
#[test]
fn sh_rejects_empty_argv() {
let (_runtime, run_fn) = rt(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [] (runtime.sh [])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh [])))"#,
HashMap::new(),
);
let err = run_fn.call::<mlua::Value>(()).unwrap_err();
@@ -887,8 +962,8 @@ mod tests {
#[test]
fn sh_rejects_number_as_cmd() {
let (_runtime, run_fn) = rt(
- r#"(local ci (require :quire.ci))
-(ci.job :go [:quire/push] (fn [] (runtime.sh 42)))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :go [:quire/push] (fn [] (runtime.sh 42)))"#,
HashMap::new(),
);
let err = run_fn.call::<mlua::Value>(()).unwrap_err();
@@ -971,9 +1046,9 @@ mod tests {
);
let source = format!(
- r#"(local ci (require :quire.ci))
+ r#"(local {{: job : runtime}} (require :quire.ci))
(local {{: mirror}} (require :quire.stdlib))
-(ci.job :go [:quire/push]
+(job :go [:quire/push]
(fn []
(let [auth (runtime.secret :github_token)]
(mirror {{:url "{url}"
@@ -996,9 +1071,9 @@ mod tests {
#[test]
fn stdlib_mirror_errors_on_missing_required_opt() {
- let source = r#"(local ci (require :quire.ci))
+ let source = r#"(local {: job} (require :quire.ci))
(local {: mirror} (require :quire.stdlib))
-(ci.job :go [:quire/push]
+(job :go [:quire/push]
(fn []
(mirror {:auth-header "x" :sha "x" :tag "v1" :git-dir "/tmp"})))"#;
let (_runtime, run_fn) = rt(source, HashMap::new());
diff --git a/quire-core/src/ci/stdlib.fnl b/quire-core/src/ci/stdlib.fnl
index c028d12..c851c79 100644
--- a/quire-core/src/ci/stdlib.fnl
+++ b/quire-core/src/ci/stdlib.fnl
@@ -1,7 +1,7 @@
;; quire.stdlib — helpers callable from inside any run-fn via
;; `(require :quire.stdlib)`. Each function pulls its runtime
-;; primitives from `(require :quire.runtime)` at call time so the
-;; binding always tracks the currently-installed runtime.
+;; primitives from `(. (require :quire.ci) :runtime)` at call time so
+;; the binding always tracks the currently-installed runtime.
(local M {})
@@ -34,7 +34,7 @@
;; non-zero git exits. `lambda` checks the required bindings for nil
;; at the call site.
(λ M.mirror [{: url : auth-header : sha : tag : git-dir :refs ?refs}]
- (let [{: sh} (require :quire.runtime)
+ (let [{: sh} (. (require :quire.ci) :runtime)
refs (or ?refs [])
;; Pass http.extraHeader via GIT_CONFIG_* env (git 2.31+)
;; instead of `-c http.extraHeader=…` in argv. Keeps the auth
diff --git a/quire-core/src/fennel.rs b/quire-core/src/fennel.rs
index 1c1c991..ce141fd 100644
--- a/quire-core/src/fennel.rs
+++ b/quire-core/src/fennel.rs
@@ -83,17 +83,21 @@ impl Fennel {
lua.globals().set("fennel", fennel_module)?;
- // Stub `runtime` so the embedded stdlib (and user pipelines)
- // compile under Fennel's strict-globals check. Mirror it into
- // `package.loaded["quire.runtime"]` so library code can write
- // `(require :quire.runtime)` instead of touching the global.
- // Both slots are replaced at execution time by
- // `RuntimeHandle::install`.
+ // Seed a placeholder `quire.ci` module exposing only an empty
+ // `runtime` stub. The stub is the canonical runtime table:
+ // `RuntimeHandle::install` mutates it in place and `uninstall`
+ // clears it. `registration::register` overwrites the rest of
+ // `quire.ci` (job/image/mirror) but carries this same stub
+ // forward as the new module's `runtime` field, so references
+ // captured before, during, and after registration all point at
+ // the same Lua table. There is no `quire.runtime` module — all
+ // access flows through `(require :quire.ci) → .runtime`.
let stub: mlua::Table = lua.create_table()?;
- lua.globals().set("runtime", stub.clone())?;
+ let placeholder: mlua::Table = lua.create_table()?;
+ placeholder.set("runtime", stub)?;
let package: mlua::Table = lua.globals().get("package")?;
let loaded: mlua::Table = package.get("loaded")?;
- loaded.set("quire.runtime", stub)?;
+ loaded.set("quire.ci", placeholder)?;
let f = Self { lua };
f.preload_stdlib()?;
@@ -485,14 +489,19 @@ mod tests {
}
#[test]
- fn defrun_destructures_from_ambient_runtime() {
+ fn defrun_destructures_from_quire_ci_runtime() {
let f = fennel();
- // Replace the stub with a runtime table whose `sh` records calls.
+ // Populate the runtime stub with a `sh` that records calls.
+ // defrun expands to `(let [<pat> (. (require :quire.ci) :runtime)] …)`,
+ // so the destructure pulls `sh` straight from this table.
let calls: std::rc::Rc<std::cell::RefCell<Vec<String>>> =
std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let cb_calls = calls.clone();
- let rt = f.lua().create_table().expect("rt");
+ let package: mlua::Table = f.lua().globals().get("package").expect("package");
+ let loaded: mlua::Table = package.get("loaded").expect("package.loaded");
+ let ci: mlua::Table = loaded.get("quire.ci").expect("quire.ci placeholder");
+ let rt: mlua::Table = ci.get("runtime").expect("quire.ci.runtime stub");
rt.set(
"sh",
f.lua()
@@ -503,7 +512,6 @@ mod tests {
.expect("create sh"),
)
.expect("set sh");
- f.lua().globals().set("runtime", rt).expect("set runtime");
let source = r#"
(import-macros {: defrun} :quire.ci)
@@ -553,21 +561,30 @@ mod tests {
}
#[test]
- fn quire_runtime_module_preloaded_to_stub() {
+ fn quire_ci_placeholder_exposes_empty_runtime_stub() {
let f = fennel();
- // Before install, `(require :quire.runtime)` returns the stub
- // table — same reference as the `runtime` global.
- let stub_via_require: mlua::Table = f
+ // Before registration runs, `(require :quire.ci)` returns a
+ // placeholder table with only an empty `runtime` stub —
+ // primitives are nil until `RuntimeHandle::install` populates
+ // them. There is no `quire.runtime` module and no `runtime`
+ // global; the placeholder is the only access path.
+ let stub: mlua::Table = f
.lua()
- .load(r#"return require("quire.runtime")"#)
+ .load(r#"return require("quire.ci").runtime"#)
.eval()
- .expect("require quire.runtime");
- let stub_via_global: mlua::Table =
- f.lua().globals().get("runtime").expect("runtime global");
- assert!(
- stub_via_require.equals(&stub_via_global).unwrap(),
- "require and global should resolve to the same stub"
- );
+ .expect("require quire.ci.runtime");
+ assert!(matches!(
+ stub.get::<mlua::Value>("sh").expect("sh"),
+ mlua::Value::Nil
+ ));
+ let global: mlua::Value = f.lua().globals().get("runtime").expect("globals lookup");
+ assert!(matches!(global, mlua::Value::Nil));
+ let quire_runtime: mlua::Value = f
+ .lua()
+ .load(r#"return package.loaded["quire.runtime"]"#)
+ .eval()
+ .expect("eval");
+ assert!(matches!(quire_runtime, mlua::Value::Nil));
}
#[test]
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 3fac96a..f5f94f7 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -290,7 +290,7 @@ impl Run {
// jobs that did run before the failure are useful context.
let outputs = runtime.take_outputs();
let timings = runtime.take_sh_timings();
- lua.remove_app_data::<Rc<Runtime>>();
+ RuntimeHandle::uninstall(lua).expect("uninstall runtime");
self.write_sh_records(&outputs, &timings)?;
@@ -1032,8 +1032,8 @@ mod tests {
fs_err::write(workspace.join("marker"), "x").expect("write marker");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :pwd [:quire/push] (fn [] (runtime.sh ["ls"])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :pwd [:quire/push] (fn [] (runtime.sh ["ls"])))"#,
);
let outputs = run
@@ -1060,9 +1060,9 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [] (runtime.sh ["echo" "from-a"])))
-(ci.job :b [:a] (fn [] (runtime.sh ["echo" "from-b"])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :a [:quire/push] (fn [] (runtime.sh ["echo" "from-a"])))
+(job :b [:a] (fn [] (runtime.sh ["echo" "from-b"])))"#,
);
let run_id = run.id().to_string();
@@ -1096,9 +1096,9 @@ mod tests {
let log = quire.base_dir().join("order.log");
let log_str = log.to_string_lossy();
let source = format!(
- r#"(local ci (require :quire.ci))
-(ci.job :b [:a] (fn [] (runtime.sh (.. "echo b >> {log}"))))
-(ci.job :a [:quire/push] (fn [] (runtime.sh (.. "echo a >> {log}"))))"#,
+ r#"(local {{: job : runtime}} (require :quire.ci))
+(job :b [:a] (fn [] (runtime.sh (.. "echo b >> {log}"))))
+(job :a [:quire/push] (fn [] (runtime.sh (.. "echo a >> {log}"))))"#,
log = log_str
);
let pipeline = load(&source);
@@ -1122,9 +1122,9 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [] (error "boom")))
-(ci.job :b [:a] (fn [] (runtime.sh ["echo" "should-not-run"])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :a [:quire/push] (fn [] (error "boom")))
+(job :b [:a] (fn [] (runtime.sh ["echo" "should-not-run"])))"#,
);
let run_id = run.id().to_string();
@@ -1150,8 +1150,8 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push]
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :grab [:quire/push]
(fn []
(let [push (runtime.jobs :quire/push)]
(runtime.sh ["echo" push.sha push.ref]))))"#,
@@ -1178,9 +1178,9 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [] nil))
-(ci.job :b [:a]
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :a [:quire/push] (fn [] nil))
+(job :b [:a]
(fn []
(let [push (runtime.jobs :quire/push)]
(runtime.sh ["echo" push.sha]))))"#,
@@ -1207,8 +1207,8 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push] (fn [] (runtime.jobs :nope)))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :grab [:quire/push] (fn [] (runtime.jobs :nope)))"#,
);
let err = run
@@ -1237,9 +1237,9 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :peer [:quire/push] (fn [] nil))
-(ci.job :grab [:quire/push] (fn [] (runtime.jobs :peer)))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :peer [:quire/push] (fn [] nil))
+(job :grab [:quire/push] (fn [] (runtime.jobs :peer)))"#,
);
let err = run
@@ -1267,8 +1267,8 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :grab [:quire/push] (fn [] (runtime.jobs :grab)))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :grab [:quire/push] (fn [] (runtime.jobs :grab)))"#,
);
let err = run
@@ -1296,9 +1296,9 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :a [:quire/push] (fn [] nil))
-(ci.job :b [:a]
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :a [:quire/push] (fn [] nil))
+(job :b [:a]
(fn []
(let [a-outputs (runtime.jobs :a)]
(runtime.sh ["echo" (tostring a-outputs)]))))"#,
@@ -1324,8 +1324,8 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :greet [:quire/push] (fn [] (runtime.sh ["echo" "hello"])))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :greet [:quire/push] (fn [] (runtime.sh ["echo" "hello"])))"#,
);
let run_id = run.id().to_string();
@@ -1369,9 +1369,9 @@ 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 [] (runtime.sh ["echo" "from-a"])))
-(ci.job :b [:a] (fn [] (error "boom")))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :a [:quire/push] (fn [] (runtime.sh ["echo" "from-a"])))
+(job :b [:a] (fn [] (error "boom")))"#,
);
let run_id = run.id().to_string();
@@ -1402,11 +1402,11 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.image "alpine")
-(ci.job :bad [:quire/push]
+ r#"(local {: job : image} (require :quire.ci))
+(image "alpine")
+(job :bad [:quire/push]
(fn []
- (ci.image "sneaky")))"#,
+ (image "sneaky")))"#,
);
let err = run
@@ -1437,8 +1437,8 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let mut pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :only [:quire/push] (fn [] nil))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :only [:quire/push] (fn [] nil))"#,
);
let called = Rc::new(Cell::new(false));
@@ -1465,8 +1465,8 @@ mod tests {
let run = runs.create(&test_meta()).expect("create");
let mut pipeline = load(
- r#"(local ci (require :quire.ci))
-(ci.job :boom [:quire/push] (fn [] nil))"#,
+ r#"(local {: job : runtime} (require :quire.ci))
+(job :boom [:quire/push] (fn [] nil))"#,
);
pipeline.replace_first_run_fn(RunFn::Rust(Rc::new(|_rt| {