Move Lua VM ownership from Pipeline to Runtime for execution
Runtime now owns the Fennel/Lua VM, constructed by consuming the
Pipeline. Source outputs (quire/push) are built as native Lua tables
instead of going through serde — removes the InputOutputs/PushOutputs
types entirely. Pipeline is now purely structural after parsing;
execution transfers its VM to the runtime.

Assisted-by: GLM-5.1 via pi
change nxqsrxnltvrrlntxzryuozkonvmmtyxo
commit f6e1e89ea4a397df3a853d3de584b263ab86b0f9
author Alpha Chen <alpha@kejadlen.dev>
date
parent rkpuqmkx
diff --git a/src/bin/quire/commands/ci.rs b/src/bin/quire/commands/ci.rs
index 1818805..9c32c77 100644
--- a/src/bin/quire/commands/ci.rs
+++ b/src/bin/quire/commands/ci.rs
@@ -73,17 +73,20 @@ pub async fn run(quire: &Quire, maybe_sha: Option<&str>) -> Result<()> {
         pushed_at: jiff::Timestamp::now(),
     };
 
+    let job_ids: Vec<String> = pipeline.jobs().iter().map(|j| j.id.clone()).collect();
+
     let mut run = runs.create(&meta)?;
     println!("Run {}: executing at {}", run.id(), commit.display);
 
-    let exec_result = run.execute(&pipeline, secrets);
+    let exec_result = run.execute(pipeline, secrets);
 
-    for job in pipeline.jobs() {
-        let outputs = run.outputs(&job.id);
+    // Pipeline was consumed by execute. Output display uses run.outputs()
+    for job_id in &job_ids {
+        let outputs = run.outputs(job_id);
         if outputs.is_empty() {
             continue;
         }
-        println!("\n==> {}", job.id);
+        println!("\n==> {}", job_id);
         for o in &outputs {
             if !o.stdout.is_empty() {
                 print!("{}", o.stdout);
diff --git a/src/ci/lua.rs b/src/ci/lua.rs
index c993570..fccd1cc 100644
--- a/src/ci/lua.rs
+++ b/src/ci/lua.rs
@@ -8,7 +8,7 @@
 //! each `run-fn` at execute time.
 
 use std::cell::RefCell;
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::rc::Rc;
 
 use mlua::{IntoLua, Lua, LuaSerdeExt};
@@ -93,29 +93,9 @@ fn register_job(
     Ok(())
 }
 
-/// Outputs of the `:quire/push` source, exposed to `(jobs :quire/push)`.
-/// For v1: `:sha`, `:ref`, `:pushed-at`. Branch/tag/etc are deferred.
-#[derive(Clone, Debug, serde::Serialize)]
-pub(super) struct PushOutputs {
-    pub sha: String,
-    #[serde(rename = "ref")]
-    pub r#ref: String,
-    #[serde(rename = "pushed-at")]
-    pub pushed_at: jiff::Timestamp,
-}
-
-/// What `(jobs name)` resolves to. One variant per kind of input;
-/// `untagged` so the on-the-Lua-side shape is the inner struct
-/// directly, not a wrapper table with a tag field.
-#[derive(Clone, Debug, serde::Serialize)]
-#[serde(untagged)]
-pub(super) enum InputOutputs {
-    Push(PushOutputs),
-}
-
-/// Per-execution runtime: holds the secrets exposed to the job, the
-/// per-job `(jobs name)` views, the current-job cursor, and the
-/// per-job captured `sh` outputs.
+/// Per-execution runtime: owns the Lua VM, holds the secrets exposed
+/// to the job, the per-job `(jobs name)` views, the current-job
+/// cursor, and the per-job captured `sh` outputs.
 ///
 /// `inputs` is keyed by the calling job; each inner map covers
 /// exactly the names that job may read. Reachability is implicit in
@@ -135,23 +115,53 @@ pub(super) enum InputOutputs {
 /// runs the command but doesn't record (the cursor lookup misses).
 /// `(secret …)` and `(jobs …)` require a runtime — without one, calls
 /// error.
-#[derive(Debug, Default)]
 pub(super) struct Runtime {
+    fennel: Fennel,
     secrets: HashMap<String, SecretString>,
-    inputs: HashMap<String, HashMap<String, Option<InputOutputs>>>,
+    inputs: HashMap<String, HashMap<String, Option<mlua::Value>>>,
     current_job: RefCell<Option<String>>,
     outputs: RefCell<HashMap<String, Vec<ShOutput>>>,
 }
 
 impl Runtime {
-    /// Build a fresh runtime. `inputs` is the precomputed per-job
-    /// `(jobs name)` view from [`Pipeline::transitive_inputs`] and
-    /// the source outputs.
+    /// Build a fresh runtime owning `fennel` (the Lua VM).
+    ///
+    /// `meta` provides the push data for `:quire/push` source outputs.
+    /// `transitive` maps each job to its set of reachable input names;
+    /// the runtime builds per-job views from this and the source values.
     pub(super) fn new(
+        fennel: Fennel,
         secrets: HashMap<String, SecretString>,
-        inputs: HashMap<String, HashMap<String, Option<InputOutputs>>>,
+        meta: &super::run::RunMeta,
+        transitive: &HashMap<String, HashSet<String>>,
     ) -> Self {
+        let lua = fennel.lua();
+
+        // Build the push outputs as a Lua table.
+        let push = lua.create_table().expect("create push table");
+        push.set("sha", meta.sha.as_str()).expect("set sha");
+        push.set("ref", meta.r#ref.as_str()).expect("set ref");
+        push.set("pushed-at", meta.pushed_at.to_string().as_str())
+            .expect("set pushed-at");
+        let push_value = push.into_lua(lua).expect("push table to value");
+
+        // Build per-job input views from transitive reachability.
+        let mut inputs = HashMap::new();
+        for (job_id, reachable) in transitive {
+            let mut view = HashMap::new();
+            for name in reachable {
+                let value = if name == "quire/push" {
+                    Some(push_value.clone())
+                } else {
+                    None
+                };
+                view.insert(name.clone(), value);
+            }
+            inputs.insert(job_id.clone(), view);
+        }
+
         Self {
+            fennel,
             secrets,
             inputs,
             current_job: RefCell::new(None),
@@ -159,6 +169,11 @@ impl Runtime {
         }
     }
 
+    /// Borrow the underlying Lua VM.
+    pub(super) fn lua(&self) -> &Lua {
+        self.fennel.lua()
+    }
+
     /// Mark `id` as the currently executing job. `(sh …)` invocations
     /// from this job's `run_fn` will record output under `id`, and
     /// `(jobs …)` lookups will resolve against `id`'s view.
@@ -179,6 +194,20 @@ impl Runtime {
     }
 }
 
+#[cfg(test)]
+impl Runtime {
+    /// Minimal constructor for tests — no inputs, no source outputs.
+    fn for_test(fennel: Fennel, secrets: HashMap<String, SecretString>) -> Self {
+        Self {
+            fennel,
+            secrets,
+            inputs: HashMap::new(),
+            current_job: RefCell::new(None),
+            outputs: RefCell::new(HashMap::new()),
+        }
+    }
+}
+
 /// `IntoLua` carrier for an `Rc<Runtime>`. Stows the Rc on the VM as
 /// app data and returns the handle table — `{sh, secret, jobs}`.
 pub(super) struct RuntimeHandle(pub Rc<Runtime>);
@@ -194,11 +223,11 @@ impl IntoLua for RuntimeHandle {
     }
 }
 
-/// Body of `(jobs name)`. Returns the typed outputs the calling job's
-/// view has for `name`, serialized to a Lua value via serde. Reachable
-/// names without recorded outputs come back as `Nil`. Errors if `name`
-/// is outside the calling job's view, if the calling job tries to read
-/// its own outputs, or if the runtime isn't installed.
+/// Body of `(jobs name)`. Returns the outputs the calling job's
+/// view has for `name` as a Lua value. Reachable names without
+/// recorded outputs come back as `nil`. Errors if `name` is outside
+/// the calling job's view, if the calling job tries to read its own
+/// outputs, or if the runtime isn't installed.
 fn lookup_input(lua: &Lua, name: String) -> mlua::Result<mlua::Value> {
     let rt = lua
         .app_data_ref::<Rc<Runtime>>()
@@ -211,7 +240,7 @@ fn lookup_input(lua: &Lua, name: String) -> mlua::Result<mlua::Value> {
         mlua::Error::external(format!("no inputs view for calling job '{calling}'"))
     })?;
     match view.get(&name) {
-        Some(Some(value)) => lua.to_value(value),
+        Some(Some(value)) => Ok(value.clone()),
         Some(None) => Ok(mlua::Value::Nil),
         None if name == *calling => Err(mlua::Error::external(format!(
             "Job '{calling}' cannot read its own outputs"
@@ -377,13 +406,18 @@ mod tests {
     use super::super::pipeline::Pipeline;
     use super::*;
 
-    /// Install a runtime with the given secrets on `pipeline`'s VM and
-    /// return the runtime handle. Mirrors what `Run::execute` does so
-    /// tests can drive a `run_fn` directly.
-    fn rt(pipeline: &Pipeline, secrets: HashMap<String, SecretString>) -> mlua::Value {
-        RuntimeHandle(Rc::new(Runtime::new(secrets, HashMap::new())))
-            .into_lua(pipeline.fennel().lua())
-            .expect("install runtime")
+    /// Extract the first job's `run_fn` from the pipeline, consume the
+    /// pipeline for its VM, build a minimal runtime, and return the
+    /// runtime handle plus the run_fn.
+    fn rt(source: &str, secrets: HashMap<String, SecretString>) -> (Rc<Runtime>, mlua::Function) {
+        let pipeline = Pipeline::load(source, "ci.fnl").expect("load should succeed");
+        let run_fn = pipeline.jobs()[0].run_fn.clone();
+        let fennel = pipeline.into_fennel();
+        let runtime = Rc::new(Runtime::for_test(fennel, secrets));
+        let _ = RuntimeHandle(runtime.clone())
+            .into_lua(runtime.lua())
+            .expect("install runtime");
+        (runtime, run_fn)
     }
 
     #[test]
@@ -395,10 +429,12 @@ mod tests {
         );
         let source = r#"(local ci (require :quire.ci))
 (ci.job :grab [:quire/push] (fn [{: secret}] (secret :github_token)))"#;
-        let pipeline = Pipeline::load(source, "ci.fnl").expect("load should succeed");
-        let token: String = pipeline.jobs()[0]
-            .run_fn
-            .call(rt(&pipeline, secrets))
+        let (runtime, run_fn) = rt(source, secrets);
+        let handle = RuntimeHandle(runtime.clone())
+            .into_lua(runtime.lua())
+            .expect("install runtime");
+        let token: String = run_fn
+            .call(handle)
             .expect("run_fn should return the secret value");
         assert_eq!(token, "ghp_test_value");
     }
@@ -407,11 +443,11 @@ mod tests {
     fn secret_errors_for_unknown_name() {
         let source = r#"(local ci (require :quire.ci))
 (ci.job :grab [:quire/push] (fn [{: secret}] (secret :missing)))"#;
-        let pipeline = Pipeline::load(source, "ci.fnl").expect("load should succeed");
-        let err = pipeline.jobs()[0]
-            .run_fn
-            .call::<mlua::Value>(rt(&pipeline, HashMap::new()))
-            .unwrap_err();
+        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();
         let msg = err.to_string();
         assert!(
             msg.contains("unknown secret") && msg.contains("missing"),
@@ -421,18 +457,14 @@ mod tests {
 
     /// Build a pipeline whose single job's run-fn invokes `(sh …)`,
     /// invoke it with the runtime handle, and decode the resulting Lua
-    /// table as ShOutput through the pipeline's VM via `lua.from_value`.
+    /// table as ShOutput.
     fn run_sh_via_job(source: &str) -> ShOutput {
-        let pipeline = Pipeline::load(source, "ci.fnl").expect("load should succeed");
-        let value: mlua::Value = pipeline.jobs()[0]
-            .run_fn
-            .call(rt(&pipeline, HashMap::new()))
-            .expect("sh call should return a value");
-        pipeline
-            .fennel()
-            .lua()
-            .from_value(value)
-            .expect("decode 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");
+        runtime.lua().from_value(value).expect("decode ShOutput")
     }
 
     #[test]
@@ -499,16 +531,15 @@ mod tests {
 
     #[test]
     fn sh_rejects_unknown_opt_key() {
-        let pipeline = Pipeline::load(
+        let (runtime, run_fn) = rt(
             r#"(local ci (require :quire.ci))
 (ci.job :go [:quire/push] (fn [{: sh}] (sh "echo hi" {:cwdir "/tmp"})))"#,
-            "ci.fnl",
-        )
-        .expect("load should succeed");
-        let err = pipeline.jobs()[0]
-            .run_fn
-            .call::<mlua::Value>(rt(&pipeline, HashMap::new()))
-            .unwrap_err();
+            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 msg = err.to_string();
         assert!(
             msg.contains("unknown field") && msg.contains("cwdir"),
@@ -518,16 +549,15 @@ mod tests {
 
     #[test]
     fn sh_rejects_non_sequence_table_as_cmd() {
-        let pipeline = Pipeline::load(
+        let (runtime, run_fn) = rt(
             r#"(local ci (require :quire.ci))
 (ci.job :go [:quire/push] (fn [{: sh}] (sh {:env {:FOO "bar"}})))"#,
-            "ci.fnl",
-        )
-        .expect("load should succeed");
-        let err = pipeline.jobs()[0]
-            .run_fn
-            .call::<mlua::Value>(rt(&pipeline, HashMap::new()))
-            .unwrap_err();
+            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 msg = err.to_string();
         assert!(
             msg.contains("sequence"),
@@ -537,16 +567,15 @@ mod tests {
 
     #[test]
     fn sh_rejects_empty_argv() {
-        let pipeline = Pipeline::load(
+        let (runtime, run_fn) = rt(
             r#"(local ci (require :quire.ci))
 (ci.job :go [:quire/push] (fn [{: sh}] (sh [])))"#,
-            "ci.fnl",
-        )
-        .expect("load should succeed");
-        let err = pipeline.jobs()[0]
-            .run_fn
-            .call::<mlua::Value>(rt(&pipeline, HashMap::new()))
-            .unwrap_err();
+            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 msg = err.to_string();
         assert!(
             msg.contains("empty"),
diff --git a/src/ci/pipeline.rs b/src/ci/pipeline.rs
index e8b77d0..fb1bdac 100644
--- a/src/ci/pipeline.rs
+++ b/src/ci/pipeline.rs
@@ -101,10 +101,13 @@ impl Pipeline {
             .map(|&idx| &self.jobs[self.graph[idx]])
     }
 
-    /// Borrow the underlying Fennel/Lua VM. Used by the executor to
-    /// install runtime state on the VM before invoking job `run_fn`s.
-    pub(crate) fn fennel(&self) -> &Fennel {
-        &self.fennel
+    /// Consume the pipeline and return its Fennel/Lua VM.
+    ///
+    /// The job functions (`run_fn`) are `'static` handles into this VM,
+    /// so they remain callable after extraction as long as the VM stays
+    /// alive.
+    pub(crate) fn into_fennel(self) -> Fennel {
+        self.fennel
     }
 
     /// Return job IDs in topological order — dependencies before
diff --git a/src/ci/run.rs b/src/ci/run.rs
index fa55ae9..ec5f75c 100644
--- a/src/ci/run.rs
+++ b/src/ci/run.rs
@@ -12,7 +12,7 @@ use std::rc::Rc;
 use jiff::Timestamp;
 use mlua::IntoLua;
 
-use super::lua::{InputOutputs, PushOutputs, Runtime, RuntimeHandle, ShOutput};
+use super::lua::{Runtime, RuntimeHandle, ShOutput};
 use super::pipeline::Pipeline;
 use crate::secret::SecretString;
 use crate::{Error, Result};
@@ -221,7 +221,8 @@ pub struct Run {
     /// secrets exposed to the script, tracks the currently-running
     /// job, and accumulates per-job captured `sh` output. Replaced
     /// fresh each `execute` call so secrets are scoped to that call.
-    runtime: Rc<Runtime>,
+    /// `None` before `execute` is called.
+    runtime: Option<Rc<Runtime>>,
 }
 
 impl Run {
@@ -250,7 +251,7 @@ impl Run {
             base,
             state,
             id,
-            runtime: Rc::new(Runtime::default()),
+            runtime: None,
         };
         run.read_meta()?;
         run.read_times()?;
@@ -259,59 +260,76 @@ impl Run {
 
     /// Drive `pipeline` to completion through this run.
     ///
-    /// Constructs a fresh [`Runtime`] with `secrets`, the source
-    /// outputs (`:quire/push` from `meta.yml`), and the per-job
-    /// transitive-input sets; installs it on the pipeline's Lua VM,
-    /// topo-sorts the jobs, transitions Pending → Active, then
-    /// invokes each `run_fn` in dependency order with the runtime
-    /// handle as its sole argument. `(sh …)` calls record their
-    /// captured output under the current job — readable via
-    /// [`Run::outputs`] after `execute` returns. The run finishes
-    /// in `Complete` if every job's `run_fn` returned without error,
+    /// Consumes the pipeline, taking ownership of its Lua VM. Constructs
+    /// a fresh [`Runtime`] with `secrets`, the source outputs
+    /// (`:quire/push` from `meta.yml`), and the per-job transitive-input
+    /// sets; installs it on the VM, topo-sorts the jobs, transitions
+    /// Pending → Active, then invokes each `run_fn` in dependency order
+    /// with the runtime handle as its sole argument. `(sh …)` calls
+    /// record their captured output under the current job — readable via
+    /// [`Run::outputs`] after `execute` returns. The run finishes in
+    /// `Complete` if every job's `run_fn` returned without error,
     /// otherwise `Failed`.
     ///
     /// Source-ref filtering (e.g. running only `quire/push`-reachable
     /// jobs) is not yet implemented; for now every validated job runs.
     pub fn execute(
         &mut self,
-        pipeline: &Pipeline,
+        pipeline: Pipeline,
         secrets: HashMap<String, SecretString>,
     ) -> Result<()> {
-        let lua = pipeline.fennel().lua();
         let meta = self.read_meta()?;
-        let sources = source_outputs(&meta);
-        let inputs = build_inputs_views(&pipeline.transitive_inputs(), &sources);
-        self.runtime = Rc::new(Runtime::new(secrets, inputs));
-        let rt_value = RuntimeHandle(self.runtime.clone())
-            .into_lua(lua)
-            .expect("install runtime on Lua VM");
 
-        let order: Vec<String> = pipeline
+        // Extract execution plan before consuming the pipeline for its VM.
+        let topo: Vec<String> = pipeline
             .topo_order()
             .into_iter()
             .map(String::from)
             .collect();
+        let transitive = pipeline.transitive_inputs();
+        let run_fns: Vec<(String, mlua::Function)> = topo
+            .iter()
+            .map(|id| {
+                let job = pipeline
+                    .job(id)
+                    .expect("topo_order returned a job id not in pipeline");
+                (job.id.clone(), job.run_fn.clone())
+            })
+            .collect();
 
-        self.transition(RunState::Active)?;
+        let fennel = pipeline.into_fennel();
+        let runtime = Rc::new(Runtime::new(fennel, secrets, &meta, &transitive));
+        self.runtime = Some(runtime.clone());
+
+        let lua = runtime.lua();
+        let rt_value = RuntimeHandle(runtime.clone())
+            .into_lua(lua)
+            .expect("install runtime on Lua VM");
 
-        for job_id in &order {
-            let job = pipeline
-                .job(job_id)
-                .expect("topo_order returned a job id not in pipeline");
+        self.transition(RunState::Active)?;
 
-            self.runtime.enter_job(&job.id);
-            let result = job.run_fn.call::<mlua::Value>(rt_value.clone());
-            self.runtime.leave_job();
+        for (job_id, run_fn) in &run_fns {
+            self.runtime
+                .as_ref()
+                .expect("runtime installed")
+                .enter_job(job_id);
+            let result = run_fn.call::<mlua::Value>(rt_value.clone());
+            self.runtime
+                .as_ref()
+                .expect("runtime installed")
+                .leave_job();
 
             if let Err(e) = result {
+                lua.remove_app_data::<Rc<Runtime>>();
                 self.transition(RunState::Failed)?;
                 return Err(Error::JobFailed {
-                    job: job.id.clone(),
+                    job: job_id.clone(),
                     source: Box::new(e),
                 });
             }
         }
 
+        lua.remove_app_data::<Rc<Runtime>>();
         self.transition(RunState::Complete)?;
         Ok(())
     }
@@ -320,7 +338,10 @@ impl Run {
     /// most recent `execute` call. Empty if the job hasn't run or
     /// produced no output.
     pub fn outputs(&self, job_id: &str) -> Vec<ShOutput> {
-        self.runtime.outputs(job_id)
+        self.runtime
+            .as_ref()
+            .map(|rt| rt.outputs(job_id))
+            .unwrap_or_default()
     }
 
     /// Transition the run from its current state to a new state.
@@ -387,44 +408,6 @@ impl Run {
     }
 }
 
-/// Build the source-ref outputs read by `(jobs name)` for source
-/// names. For v1, only `:quire/push` is exposed, derived from the run
-/// meta — `:sha`, `:ref`, `:pushed-at`. Branch/tag/etc are deferred.
-/// Pure Rust data; `lookup_input` serializes to Lua via serde.
-fn source_outputs(meta: &RunMeta) -> HashMap<String, InputOutputs> {
-    let push = PushOutputs {
-        sha: meta.sha.clone(),
-        r#ref: meta.r#ref.clone(),
-        pushed_at: meta.pushed_at,
-    };
-    let mut inputs = HashMap::new();
-    inputs.insert("quire/push".to_string(), InputOutputs::Push(push));
-    inputs
-}
-
-/// Materialize the per-job `(jobs name)` views from the pipeline's
-/// transitive-input map and the source outputs.
-///
-/// For each job J, the view holds every name reachable from J as a
-/// key. Source values populate the source entries; everything else
-/// sits as `None` so future job-to-job outputs can drop in without
-/// changing the lookup contract.
-fn build_inputs_views(
-    transitive_inputs: &HashMap<String, std::collections::HashSet<String>>,
-    sources: &HashMap<String, InputOutputs>,
-) -> HashMap<String, HashMap<String, Option<InputOutputs>>> {
-    transitive_inputs
-        .iter()
-        .map(|(job_id, reachable)| {
-            let view = reachable
-                .iter()
-                .map(|name| (name.clone(), sources.get(name).cloned()))
-                .collect();
-            (job_id.clone(), view)
-        })
-        .collect()
-}
-
 /// Write a serializable value to a YAML file atomically (temp file + rename).
 pub(crate) fn write_yaml<T: serde::Serialize>(path: &Path, value: &T) -> Result<()> {
     let tmp_path = path.with_extension("yml.tmp");
@@ -607,7 +590,7 @@ mod tests {
             base: PathBuf::from("/tmp/quire-test-runs/test.git"),
             state: RunState::Pending,
             id: uuid::Uuid::now_v7().to_string(),
-            runtime: Rc::new(Runtime::default()),
+            runtime: None,
         };
 
         let result = run.transition(RunState::Active);
@@ -714,7 +697,7 @@ mod tests {
 (ci.job :b [:a] (fn [{: sh}] (sh ["echo" "from-b"])))"#,
         );
 
-        run.execute(&pipeline, HashMap::new()).expect("execute");
+        run.execute(pipeline, HashMap::new()).expect("execute");
 
         assert_eq!(run.state(), RunState::Complete);
 
@@ -744,7 +727,7 @@ mod tests {
         );
         let pipeline = load(&source);
 
-        run.execute(&pipeline, HashMap::new()).expect("execute");
+        run.execute(pipeline, HashMap::new()).expect("execute");
 
         let contents = fs_err::read_to_string(&log).expect("read log");
         assert_eq!(contents, "a\nb\n");
@@ -763,7 +746,7 @@ mod tests {
         );
 
         let err = run
-            .execute(&pipeline, HashMap::new())
+            .execute(pipeline, HashMap::new())
             .expect_err("expected failure");
         assert!(matches!(err, Error::JobFailed { ref job, .. } if job == "a"));
         assert_eq!(run.state(), RunState::Failed);
@@ -787,7 +770,7 @@ mod tests {
       (sh ["echo" push.sha push.ref]))))"#,
         );
 
-        run.execute(&pipeline, HashMap::new()).expect("execute");
+        run.execute(pipeline, HashMap::new()).expect("execute");
 
         let outputs = run.outputs("grab");
         assert_eq!(outputs.len(), 1);
@@ -811,7 +794,7 @@ mod tests {
       (sh ["echo" push.sha]))))"#,
         );
 
-        run.execute(&pipeline, HashMap::new()).expect("execute");
+        run.execute(pipeline, HashMap::new()).expect("execute");
 
         let outputs = run.outputs("b");
         assert_eq!(outputs.len(), 1);
@@ -830,7 +813,7 @@ mod tests {
         );
 
         let err = run
-            .execute(&pipeline, HashMap::new())
+            .execute(pipeline, HashMap::new())
             .expect_err("expected failure");
         match err {
             Error::JobFailed { job, source } => {
@@ -859,7 +842,7 @@ mod tests {
         );
 
         let err = run
-            .execute(&pipeline, HashMap::new())
+            .execute(pipeline, HashMap::new())
             .expect_err("expected failure");
         match err {
             Error::JobFailed { source, .. } => {
@@ -885,7 +868,7 @@ mod tests {
         );
 
         let err = run
-            .execute(&pipeline, HashMap::new())
+            .execute(pipeline, HashMap::new())
             .expect_err("expected failure");
         match err {
             Error::JobFailed { source, .. } => {