A pass on the same state is reused from any repository on the hub, public or with the same owner, after the fork line (refs #296) 91c306aa4706
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 616dce297726
- When
- · 2026-10-27 23:07 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
91c306aa47061a18f85e0109fa9d757a014b7267f8c14b4412a498cee9976e67- Depends on
- 42cd46c9b09e 42fa95540468 44a3d2ebaddd 5588bb3631d2 a7e6e8e6094c a9088323c390 b5668108487f
- Depended on by
- 06b756358f7e
- In bookmarks
- main
- In tags
- v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24
- Edits
- 7 atoms, 0 tree operations, 4 files
Browse the tree at this change
modified CHANGELOG.md +2 −0
| … | 3 unchanged lines |
||
| 4 | 4 | ||
| 5 | 5 | ## Unreleased |
|
| 6 | 6 | ||
| 7 | + | - **A pass on the same state is reused from any repository on the hub (#296).** After the repository's own runs and the line it was forked from, a push's job with no pass of its own on that state reuses one from any other repository holding the same state, public or with the same owner, newest first: a state is its changes, so whoever proved it, the proof holds. The run page and the check say whose pass it was. |
|
| 8 | + | ||
| 7 | 9 | ## 0.9.10 (2026-10-27) |
|
| 8 | 10 | ||
| 9 | 11 | - **Phone alerts (oxygenhub-android#16, oxygenhub-gcp decision 0008).** A phone registers its Cloud Messaging token and an X25519 public key with `PUT /api/v1/user/push`, using the token it signed in with (`DELETE` unregisters; revoking the token ends it too, migration 0101). Each notification an account is told then also goes to its phones, its title and link sealed to each phone's key (`flow_core::alert`), as a data-only, high-priority message, so Google carries only ciphertext. The hub sends as the service account its VM runs as, with the metadata server's token: no key. Off until `[push] fcm_project` (`OXYHUB_PUSH_FCM_PROJECT`) is set; a registration Cloud Messaging no longer knows is forgotten. Pins OxygenFlow main (sealed alerts). |
|
| … | 382 unchanged lines |
modified crates/hub/src/db/flow.rs +50 −0
| … | 325 unchanged lines |
||
| 326 | 326 | Ok(None) |
|
| 327 | 327 | } |
|
| 328 | 328 | ||
| 329 | + | /// A pass of job `name` with this spec and sandbox on `state` in any other |
|
| 330 | + | /// repository on the hub (#296): a state is its changes, so whoever proved |
|
| 331 | + | /// it, the proof holds here. Only from a repository that is public or has |
|
| 332 | + | /// this one's owner, newest first. After [`passed_upstream`], which |
|
| 333 | + | /// prefers the repository's own line. |
|
| 334 | + | pub async fn passed_elsewhere( |
|
| 335 | + | pool: &Pool, |
|
| 336 | + | repo: &super::repos::RepoRow, |
|
| 337 | + | state: &str, |
|
| 338 | + | name: &str, |
|
| 339 | + | spec: &str, |
|
| 340 | + | sandbox: &str, |
|
| 341 | + | ) -> sqlx::Result<Option<Reused>> { |
|
| 342 | + | #[derive(FromRow)] |
|
| 343 | + | struct Pass { |
|
| 344 | + | owner: String, |
|
| 345 | + | name: String, |
|
| 346 | + | number: i64, |
|
| 347 | + | receipt: String, |
|
| 348 | + | receipt_hash: String, |
|
| 349 | + | receipt_attest: String, |
|
| 350 | + | receipt_signer: String, |
|
| 351 | + | receipt_note: String, |
|
| 352 | + | } |
|
| 353 | + | let pass: Option<Pass> = sqlx::query_as( |
|
| 354 | + | "SELECT o.owner, o.name, r.number, j.receipt, j.receipt_hash, j.receipt_attest, j.receipt_signer, j.receipt_note \ |
|
| 355 | + | FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id JOIN repositories o ON o.id = r.repo_id \ |
|
| 356 | + | WHERE r.state = $1 AND j.name = $2 AND j.spec = $3 AND j.sandbox = $4 AND j.status = 'passed' \ |
|
| 357 | + | AND o.id <> $5 AND (o.visibility = 'public' OR o.owner = $6) \ |
|
| 358 | + | ORDER BY j.id DESC LIMIT 1", |
|
| 359 | + | ) |
|
| 360 | + | .bind(state) |
|
| 361 | + | .bind(name) |
|
| 362 | + | .bind(spec) |
|
| 363 | + | .bind(sandbox) |
|
| 364 | + | .bind(repo.id) |
|
| 365 | + | .bind(&repo.owner) |
|
| 366 | + | .fetch_optional(pool) |
|
| 367 | + | .await?; |
|
| 368 | + | Ok(pass.map(|p| Reused { |
|
| 369 | + | repo: Some(format!("{}/{}", p.owner, p.name)), |
|
| 370 | + | run_number: p.number, |
|
| 371 | + | receipt: p.receipt, |
|
| 372 | + | receipt_hash: p.receipt_hash, |
|
| 373 | + | receipt_attest: p.receipt_attest, |
|
| 374 | + | receipt_signer: p.receipt_signer, |
|
| 375 | + | receipt_note: p.receipt_note, |
|
| 376 | + | })) |
|
| 377 | + | } |
|
| 378 | + | ||
| 329 | 379 | /// Runs of a job whose peaks set its next need. |
|
| 330 | 380 | const NEED_FROM_RUNS: i64 = 5; |
|
| 331 | 381 | ||
| … | 1255 unchanged lines |
modified crates/hub/src/flow.rs +6 −1
| … | 837 unchanged lines |
||
| 838 | 838 | } |
|
| 839 | 839 | j.reused = |
|
| 840 | 840 | fdb::passed_on_state(&app.db, row.id, state, &j.name, &j.spec, &j.sandbox).await.map_err(|e| e.to_string())?; |
|
| 841 | - | // A fork starts green where upstream proved this state (#296). |
|
| 841 | + | // A fork starts green where upstream proved this state, and |
|
| 842 | + | // any repository where another on the hub did (#296). |
|
| 842 | 843 | if j.reused.is_none() && row.forked_from.is_some() { |
|
| 843 | 844 | j.reused = |
|
| 844 | 845 | fdb::passed_upstream(&app.db, row, state, &j.name, &j.spec, &j.sandbox).await.map_err(|e| e.to_string())?; |
|
| 845 | 846 | } |
|
| 847 | + | if j.reused.is_none() { |
|
| 848 | + | j.reused = |
|
| 849 | + | fdb::passed_elsewhere(&app.db, row, state, &j.name, &j.spec, &j.sandbox).await.map_err(|e| e.to_string())?; |
|
| 850 | + | } |
|
| 846 | 851 | } |
|
| 847 | 852 | } |
|
| 848 | 853 | let hash = hex::encode(p.hash()); |
|
| … | 2216 unchanged lines |
modified crates/hub/tests/flow.rs +32 −12
| … | 2038 unchanged lines |
||
| 2039 | 2039 | let checks = db::hooks::checks_for_state(&h.app.db, fork.id, &run.state).await.unwrap(); |
|
| 2040 | 2040 | assert!(checks.iter().any(|c| c.summary.contains("passed in andy/game run #")), "{checks:?}"); |
|
| 2041 | 2041 | ||
| 2042 | - | // A private upstream of another owner: a new fork runs its own. |
|
| 2043 | - | sqlx::query("UPDATE repositories SET visibility = 'private' WHERE id = $1").bind(up.id).execute(&h.app.db).await.unwrap(); |
|
| 2044 | - | create(&h.app, "bob", "game2").await; |
|
| 2045 | - | let fork2 = db::repos::get(&h.app.db, "bob", "game2").await.unwrap().unwrap(); |
|
| 2046 | - | db::repos::set_forked_from(&h.app.db, fork2.id, up.id).await.unwrap(); |
|
| 2047 | - | sqlx::query("UPDATE repositories SET flow = 'on' WHERE id = $1").bind(fork2.id).execute(&h.app.db).await.unwrap(); |
|
| 2048 | - | c.config.set("remote.origin.url", &format!("{}/bob/game2", h.base)).unwrap(); |
|
| 2049 | - | c.save_config().unwrap(); |
|
| 2050 | - | assert!(c.push("origin", false).unwrap().iter().all(|r| r.ok)); |
|
| 2051 | - | let run2 = runs_of(&h.app, "bob", "game2").await.remove(0); |
|
| 2052 | - | assert_eq!(run2.state, first.state); |
|
| 2053 | - | let jobs = db::flow::jobs(&h.app.db, run2.id).await.unwrap(); |
|
| 2042 | + | // Any repository on the hub holding the same state, not only forks: |
|
| 2043 | + | // carol's, which no one forked, starts green from a public pass. |
|
| 2044 | + | for who in ["carol"] { |
|
| 2045 | + | db::accounts::create_account(&h.app.db, who, &format!("{who}@example.com"), "x", "salt", false).await.unwrap(); |
|
| 2046 | + | } |
|
| 2047 | + | let push_to = |owner: &'static str, name: &'static str| { |
|
| 2048 | + | let (app, base) = (h.app.clone(), h.base.clone()); |
|
| 2049 | + | let path = work.path().to_path_buf(); |
|
| 2050 | + | async move { |
|
| 2051 | + | create(&app, owner, name).await; |
|
| 2052 | + | let r = db::repos::get(&app.db, owner, name).await.unwrap().unwrap(); |
|
| 2053 | + | sqlx::query("UPDATE repositories SET flow = 'on' WHERE id = $1").bind(r.id).execute(&app.db).await.unwrap(); |
|
| 2054 | + | let mut c = Repo::open(&path).unwrap(); |
|
| 2055 | + | c.config.set("remote.origin.url", &format!("{base}/{owner}/{name}")).unwrap(); |
|
| 2056 | + | c.save_config().unwrap(); |
|
| 2057 | + | assert!(c.push("origin", false).unwrap().iter().all(|r| r.ok)); |
|
| 2058 | + | let run = runs_of(&app, owner, name).await.remove(0); |
|
| 2059 | + | db::flow::jobs(&app.db, run.id).await.unwrap() |
|
| 2060 | + | } |
|
| 2061 | + | }; |
|
| 2062 | + | let jobs = push_to("carol", "tetris-clone").await; |
|
| 2063 | + | assert!(jobs.iter().all(|j| j.status == "passed" && j.reused_repo().is_some()), "{jobs:?}"); |
|
| 2064 | + | ||
| 2065 | + | // Every repository holding those passes private, and none of them |
|
| 2066 | + | // carol's: her next repository runs its own. |
|
| 2067 | + | sqlx::query("UPDATE repositories SET visibility = 'private' WHERE owner IN ('andy', 'bob')").execute(&h.app.db).await.unwrap(); |
|
| 2068 | + | sqlx::query("UPDATE repositories SET visibility = 'private' WHERE owner = 'carol'").execute(&h.app.db).await.unwrap(); |
|
| 2069 | + | sqlx::query("DELETE FROM flow_jobs WHERE run_id IN (SELECT id FROM flow_runs WHERE repo_id IN (SELECT id FROM repositories WHERE owner = 'carol'))") |
|
| 2070 | + | .execute(&h.app.db) |
|
| 2071 | + | .await |
|
| 2072 | + | .unwrap(); |
|
| 2073 | + | let jobs = push_to("carol", "another").await; |
|
| 2054 | 2074 | assert!(jobs.iter().all(|j| j.status == "queued" && j.reused_from().is_none()), "{jobs:?}"); |
|
| 2055 | 2075 | } |
|
| 2056 | 2076 | ||
| … | 4992 unchanged lines |