Environments: environment "prod" on a job f4e51e63582d
flow-core parses a job's environment (1 to 32 of a-z 0-9 -), hashed as job field 12. The hub records what each environment runs and names it in the job's identity tokens only once the job is approved. Spec 5.3.3; test. Refs #59
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
Claude Code <[email protected]> · delegate for changes, until 2026-11-13 19:00Z · key ea5f78160088
- When
- · 2026-10-16 04:30 UTC
- Signature
- ✔ signed by Claude Code
- Hash
f4e51e63582d7178ecf5dbcfde1b925a3ef48fe8649810cb68a2a71c46a11c0e- Depends on
- 46fe2c815cc3 a083cc802892 a67b97e9a717 d60aac6c3f8a e0918b887850 e20897216221 e2c125610f43 e576e08617dc
- Depended on by
- 28cc603f737d cd03b9844516 2be1fe4ad345
- In bookmarks
- main
- 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
- 7 atoms, 0 tree operations, 5 files
Browse the tree at this change
modified crates/flow-core/src/canonical.rs +4 −0
| … | 110 unchanged lines |
||
| 111 | 111 | if let Some(a) = &j.approval { |
|
| 112 | 112 | fields.push((11, Value::Array(vec![text(&a.by), Value::Bool(a.others_only), Value::Uint(a.expires_s)]))); |
|
| 113 | 113 | } |
|
| 114 | + | // The environment it deploys to; absent without one. |
|
| 115 | + | if let Some(e) = &j.environment { |
|
| 116 | + | fields.push((12, text(e))); |
|
| 117 | + | } |
|
| 114 | 118 | if !j.inputs.is_empty() { |
|
| 115 | 119 | fields.push(( |
|
| 116 | 120 | 10, |
|
| … | 47 unchanged lines |
modified crates/flow-core/src/pipeline.rs +17 −0
| … | 180 unchanged lines |
||
| 181 | 181 | /// "maintain"; others-only #true; expires "24h" }`. |
|
| 182 | 182 | #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 183 | 183 | pub approval: Option<Approval>, |
|
| 184 | + | /// The environment the job deploys to (oxygenflow#59), e.g. `prod`: the |
|
| 185 | + | /// hub records what each environment runs, and the job's identity token |
|
| 186 | + | /// names it once the job has been approved. |
|
| 187 | + | #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 188 | + | pub environment: Option<String>, |
|
| 184 | 189 | /// Secrets the job mounts, sorted by name (spec §9 `secret`): the hub |
|
| 185 | 190 | /// supplies each by name, the runner mounts it as a file. |
|
| 186 | 191 | #[serde(default, skip_serializing_if = "Vec::is_empty")] |
|
| … | 1240 unchanged lines |
||
| 1427 | 1432 | outputs: vec![], |
|
| 1428 | 1433 | inputs: vec![], |
|
| 1429 | 1434 | approval: None, |
|
| 1435 | + | environment: None, |
|
| 1430 | 1436 | runs_on: vec![], |
|
| 1431 | 1437 | timeout_s: None, |
|
| 1432 | 1438 | on: None, |
|
| … | 73 unchanged lines |
||
| 1506 | 1512 | } |
|
| 1507 | 1513 | "input" => parse_input(ctx, c, &mut job), |
|
| 1508 | 1514 | "approval" => job.approval = parse_approval(ctx, c), |
|
| 1515 | + | "environment" => { |
|
| 1516 | + | ctx.no_children(c); |
|
| 1517 | + | if let Some(e) = ctx.one_string(c) { |
|
| 1518 | + | let ok = !e.is_empty() && e.len() <= 32 && e.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'); |
|
| 1519 | + | if ok { |
|
| 1520 | + | job.environment = Some(e); |
|
| 1521 | + | } else { |
|
| 1522 | + | ctx.err(c.span(), format!("environment {e:?}: 1 to 32 of a-z 0-9 -, as \"prod\"")); |
|
| 1523 | + | } |
|
| 1524 | + | } |
|
| 1525 | + | } |
|
| 1509 | 1526 | "secret" => parse_secret(ctx, c, &mut job), |
|
| 1510 | 1527 | "identity" => parse_identity(ctx, c, &mut job), |
|
| 1511 | 1528 | n if LATER_JOB.contains(&n) => ctx.err(c.span(), format!("`{n}` is not in M0 yet")), |
|
| … | 32 unchanged lines |
modified crates/flow-core/src/tests.rs +9 −0
| … | 692 unchanged lines |
||
| 693 | 693 | has(" approval { expires \"800h\"; }\n", "from 1m to 720h"); |
|
| 694 | 694 | has(" approval { when \"later\"; }\n", "unknown approval setting"); |
|
| 695 | 695 | } |
|
| 696 | + | ||
| 697 | + | #[test] |
|
| 698 | + | fn a_job_names_the_environment_it_deploys_to() { |
|
| 699 | + | let job = |e: &str| doc(&format!("job \"apply\" {{\n sandbox \"alpine\"\n{e} exec \"apply\"\n}}\n")); |
|
| 700 | + | let p = Pipeline::parse(&job(" environment \"prod\"\n")).unwrap(); |
|
| 701 | + | assert_eq!(p.jobs["apply"].environment.as_deref(), Some("prod")); |
|
| 702 | + | assert_ne!(p.hash(), Pipeline::parse(&job("")).unwrap().hash(), "part of the job"); |
|
| 703 | + | assert!(problems(&job(" environment \"Prod env\"\n")).iter().any(|(_, m)| m.contains("1 to 32 of a-z 0-9 -"))); |
|
| 704 | + | } |
modified crates/flow/src/outputs.rs +1 −0
| … | 225 unchanged lines |
||
| 226 | 226 | outputs: vec![], |
|
| 227 | 227 | inputs: vec![], |
|
| 228 | 228 | approval: None, |
|
| 229 | + | environment: None, |
|
| 229 | 230 | runs_on: vec![], |
|
| 230 | 231 | timeout_s: None, |
|
| 231 | 232 | on: None, |
|
| … | 84 unchanged lines |
modified docs/spec.md +13 −0
| … | 347 unchanged lines |
||
| 348 | 348 | bookmark cancels the run as before, so a stale plan is never applied. The |
|
| 349 | 349 | approval is part of the job's hash (field 11). |
|
| 350 | 350 | ||
| 351 | + | ### 5.3.3 Environments (oxygenflow#59) |
|
| 352 | + | ||
| 353 | + | `environment "prod"` on a job says where it deploys (1 to 32 of `a-z`, |
|
| 354 | + | `0-9`, `-`; part of the job's hash, field 12). The hub keeps what each |
|
| 355 | + | environment of a repository runs, from the jobs that passed for it (state, |
|
| 356 | + | run, when, who approved), on the repository's Deployments page. The job's |
|
| 357 | + | identity tokens carry an `environment` claim only when the job was |
|
| 358 | + | approved (§5.3.2): a job with no approval block, or one not approved, gets |
|
| 359 | + | tokens without it. So a cloud's trust condition can require |
|
| 360 | + | `environment == "prod"`, and nothing unapproved deploys there, even from |
|
| 361 | + | the deploy bookmark. The approval block is the environment's protection |
|
| 362 | + | rule for now. |
|
| 363 | + | ||
| 351 | 364 | ### 5.4 The Build Cache (oxygenflow#28) |
|
| 352 | 365 | ||
| 353 | 366 | An `ephemeral://disk` mount may name a `cache`, and the files that key it: |
|
| … | 351 unchanged lines |