OXYGENHUB

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