OXYGENHUB

A claim carries the hub's variables for the job c58efc6a74db

The hub holds variables per site, organisation and repository
(oxygenhub#102); they come with the claim as env and join the job's
environment under FLOW_* and the pipeline's own env, so they are part
of the receipt. flow run --var NAME=VALUE does the same locally.
Secrets are not variables: spec §9 mounts them. refs oxygenhub#102
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-04 15:52 UTC
Signature
✔ signed by Andy McCall
Hash
c58efc6a74dbe2b12cb67d5b277a357fb1e1054e40a6fa99a5ea02ff2d37c6a6
Depends on
02b127d2c721 a72abeeedd78 c3db750ed96e c4285535b03a e0918b887850 f98a8f7881e0
Depended on by
ea56c56eaf90 720eb8191059 e2c125610f43
In bookmarks
main newer-pipeline push-uses-login
In tags
v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.5.0 v0.5.1 v0.5.2 v0.5.3
Edits
15 atoms, 0 tree operations, 5 files

Browse the tree at this change

modified CHANGELOG.md +2 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **Variables from the hub (oxygenhub#102).** A claim carries `env`, the variables the hub holds for the repository; they join the job's environment under `FLOW_*` and the pipeline's own `env`. `flow run --var NAME=VALUE` does the same locally.
8 +
7 9
- **A job says how long it may run (#23).** `timeout "90m"` on a job, or on a sandbox for its jobs (hours, minutes, seconds; a minute to a day). The hub caps it at the site's maximum.
8 10
9 11
- **A pinned file is re-read from the disk before its hash is called wrong (#22).**
…
8 unchanged lines
modified crates/flow-core/src/api.rs +12 −0
…
133 unchanged lines
134 134
    /// The run's number within its repository.
135 135
    #[serde(default)]
136 136
    pub run_number: i64,
137 +
    /// Variables the hub holds for the repository (its site's, its
138 +
    /// organisation's and its own, inner overriding outer; oxygenhub#102).
139 +
    /// Set under `FLOW_*` and under the pipeline's own `env`, so they are
140 +
    /// part of the job's environment and of its receipt. Never secrets:
141 +
    /// those are mounted, not put in the environment (spec §9).
142 +
    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
143 +
    pub env: std::collections::BTreeMap<String, String>,
137 144
}
138 145
139 146
impl Claim {
…
16 unchanged lines
156 163
        if self.run_number > 0 {
157 164
            v.push(("FLOW_RUN".into(), self.run_number.to_string()));
158 165
        }
166 +
        for (k, val) in &self.env {
167 +
            if !v.iter().any(|(n, _)| n == k) {
168 +
                v.push((k.clone(), val.clone()));
169 +
            }
170 +
        }
159 171
        v
160 172
    }
161 173
}
…
36 unchanged lines
modified crates/flow-core/src/tests.rs +4 −2
…
286 unchanged lines
287 287
        state: "cc".into(),
288 288
        bookmark: "feature/x".into(),
289 289
        run_number: 3,
290 +
        env: [("CC".to_string(), "clang".to_string()), ("FLOW_REPO".to_string(), "spoofed".to_string())].into_iter().collect(),
290 291
    };
291 292
    let env = job.with_flow_env(&claim.flow_env()).env;
292 -
    assert_eq!(env["FLOW_REPO"], "andy/demo");
293 +
    assert_eq!(env["CC"], "clang", "the hub's variables reach the job");
294 +
    assert_eq!(env["FLOW_REPO"], "andy/demo", "a hub variable cannot pose as a FLOW_ fact");
293 295
    assert_eq!(env["FLOW_HEADS"], "aa+bb");
294 296
    assert_eq!(env["FLOW_STATE"], "cc");
295 297
    assert_eq!(env["FLOW_RUN"], "3", "the run's number in its repository, not the hub's id");
296 298
    assert_eq!(env["FLOW_JOB"], "build");
297 299
    assert_eq!(env["FLOW_BOOKMARK"], "pinned-by-the-pipeline", "the pipeline's own value wins");
298 300
    // Unknown values (an older hub) are left out, not empty.
299 -
    let old = crate::api::Claim { state: String::new(), bookmark: String::new(), run_number: 0, ..claim };
301 +
    let old = crate::api::Claim { state: String::new(), bookmark: String::new(), run_number: 0, env: Default::default(), ..claim };
300 302
    let names: Vec<String> = old.flow_env().into_iter().map(|(k, _)| k).collect();
301 303
    assert_eq!(names, ["FLOW_REPO", "FLOW_HEADS", "FLOW_JOB"]);
302 304
}
…
113 unchanged lines
modified crates/flow/src/main.rs +19 −4
…
72 unchanged lines
73 73
        /// Minutes before a job is stopped
74 74
        #[arg(long, default_value_t = 60)]
75 75
        timeout: u64,
76 +
        /// A variable for every job, as the hub's settings would give it;
77 +
        /// the pipeline's own `env` wins
78 +
        #[arg(long = "var", value_name = "NAME=VALUE")]
79 +
        vars: Vec<String>,
76 80
    },
77 81
    /// Claim and run jobs for the hub in flow.toml's [runner] (or the
78 82
    /// runner.toml `flow runner register` wrote)
…
63 unchanged lines
142 146
                }
143 147
            }
144 148
        }
145 -
        Cmd::Run { jobs, dir, timeout } => {
149 +
        Cmd::Run { jobs, dir, timeout, vars } => {
146 150
            let home = Home::locate(cli.home.as_deref())?;
147 -
            run_local(&home, &dir, &jobs, Duration::from_secs(timeout * 60))
151 +
            let vars = vars
152 +
                .iter()
153 +
                .map(|e| {
154 +
                    e.split_once('=')
155 +
                        .map(|(k, v)| (k.to_string(), v.to_string()))
156 +
                        .ok_or_else(|| anyhow::anyhow!("--var {e:?}: expected NAME=VALUE"))
157 +
                })
158 +
                .collect::<Result<Vec<_>>>()?;
159 +
            run_local(&home, &dir, &jobs, Duration::from_secs(timeout * 60), &vars)
148 160
        }
149 161
        Cmd::Runner { cmd: Some(RunnerCmd::Register { hub, token, name, ca, label }) } => {
150 162
            let home = Home::locate(cli.home.as_deref())?;
…
123 unchanged lines
274 286
    Ok(())
275 287
}
276 288
277 -
fn run_local(home: &Home, dir: &Path, only: &[String], timeout: Duration) -> Result<()> {
289 +
fn run_local(home: &Home, dir: &Path, only: &[String], timeout: Duration, vars: &[(String, String)]) -> Result<()> {
278 290
    let p = load(&dir.join(PIPELINE_PATH))?;
279 291
    // The named jobs and everything they depend on.
280 292
    let mut wanted: BTreeSet<String> = only.iter().cloned().collect();
…
35 unchanged lines
316 328
        std::fs::create_dir_all(&js)?;
317 329
        // Each job starts from the same files: jobs do not share writes.
318 330
        workspace::image(&scratch.join("tree"), &js.join("workspace.ext4"))?;
319 -
        let job = &job.in_sandbox(sb).with_flow_env(&[("FLOW_JOB".into(), job.name.clone()), ("FLOW_RUN".into(), "local".into())]);
331 +
        let job = &job
332 +
            .in_sandbox(sb)
333 +
            .with_flow_env(&[("FLOW_JOB".into(), job.name.clone()), ("FLOW_RUN".into(), "local".into())])
334 +
            .with_flow_env(vars);
320 335
        let out = microvm::run(home, sb, job, &js.join("workspace.ext4"), &js, timeout, &mut |r: Record| print_record(&r))?;
321 336
        let _ = std::fs::remove_dir_all(&js);
322 337
        match out.exit_code {
…
42 unchanged lines
modified docs/m0.md +5 −2
…
119 unchanged lines
120 120
   is told what it builds: `FLOW_REPO`, `FLOW_STATE` (the hub's state hash),
121 121
   `FLOW_HEADS` (full hashes joined with `+`), `FLOW_BOOKMARK`, `FLOW_RUN`
122 122
   (the run's number in its repository) and `FLOW_JOB`; `flow run` sets
123 -
   `FLOW_JOB` and `FLOW_RUN=local`. A variable the pipeline sets keeps its
124 -
   value. They are part of the job's environment, so of its receipt.
123 +
   `FLOW_JOB` and `FLOW_RUN=local`. After them come the **variables** the
124 +
   hub holds for the repository (site, organisation, repository; the
125 +
   claim's `env`; `flow run --var NAME=VALUE` locally), never secrets. A
126 +
   variable the pipeline sets keeps its value. They are part of the job's
127 +
   environment, so of its receipt.
125 128
5. `flow-init` frames the child's output on the serial console as
126 129
   `\x1eO <line>` (stdout) and `\x1eE <line>` (stderr), then
127 130
   `\x1eX <code>`, and powers off. The kernel runs `quiet` so the host sees
…
88 unchanged lines