Skip to main content

bupha_dagger/
lib.rs

1//! bupha-runner-dagger — `dagger call <fn>` in the worktree. Dagger (and
2//! its container engine) is only needed when this runner is selected;
3//! the port knows nothing about it.
4
5use bupha_runner::{RunCtx, RunHandle, Runner};
6use serde::Deserialize;
7use std::io;
8use std::process::Command;
9
10/// dagger runner config — impl-owned fields, deserialized from
11/// `runner.extra`.
12#[derive(Debug, Deserialize)]
13pub struct DaggerCfg {
14    #[serde(default = "crate::default_function")]
15    pub function: String,
16    #[serde(default = "crate::default_shards")]
17    pub shards: String,
18    #[serde(default = "crate::default_jobs")]
19    pub jobs: String,
20    #[serde(default)]
21    pub dogfood_jobs: String,
22    #[serde(default)]
23    pub sequential: bool,
24    /// sccache conf passed as `--sccache-conf` (resolved from
25    /// `[cache]` by the composition root).
26    #[serde(default)]
27    pub sccache_conf: Option<String>,
28    #[serde(default)]
29    pub cli_source: Option<String>,
30    #[serde(default)]
31    pub extra_args: Vec<String>,
32}
33
34fn default_function() -> String {
35    "ci".into()
36}
37fn default_shards() -> String {
38    "1".into()
39}
40fn default_jobs() -> String {
41    "1".into()
42}
43
44pub struct DaggerRunner {
45    cfg: DaggerCfg,
46    /// `--cli-source <dir>` — for repos whose pipeline builds
47    /// against a sibling checkout.
48    cli_source: Option<String>,
49}
50
51impl DaggerRunner {
52    pub fn new(cfg: DaggerCfg, cli_source: Option<String>) -> Self {
53        Self { cfg, cli_source }
54    }
55}
56
57impl Runner for DaggerRunner {
58    fn name(&self) -> &'static str {
59        "dagger"
60    }
61
62    fn spawn(&self, ctx: &RunCtx) -> io::Result<Box<dyn RunHandle>> {
63        let c = &self.cfg;
64        let mut cmd = Command::new("dagger");
65        // Flags beyond shards/jobs/token are only emitted when set —
66        // `dagger call` rejects args the module's function doesn't
67        // declare, so the emitted set must match the module signature.
68        cmd.current_dir(ctx.workdir).args([
69            "call",
70            &c.function,
71            "--shards",
72            &c.shards,
73            "--jobs",
74            &c.jobs,
75            "--token",
76            &format!("env://{}", ctx.token_env),
77        ]);
78        if !c.dogfood_jobs.is_empty() {
79            cmd.args(["--dogfood-jobs", &c.dogfood_jobs]);
80        }
81        if let Some(conf) = c.sccache_conf.as_deref() {
82            cmd.args(["--sccache-conf", &format!("file://{conf}")]);
83        }
84        if c.sequential {
85            cmd.arg("--sequential");
86        }
87        if let Some(src) = &self.cli_source {
88            cmd.args(["--cli-source", src]);
89        }
90        cmd.args(&c.extra_args);
91        cmd.spawn().map(|c| Box::new(c) as Box<dyn RunHandle>)
92    }
93}