OXYGENHUB

A phone's runner follows the app's releases: never offered a runner build (refs andymccall/oxygenflow#84) 2f79b17e089a

Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key a93aecb664d7
When
· 2026-10-29 01:49 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
2f79b17e089af898bccb1236755e2c88c5868fac7edd5b4f5802c4544d1d3175
Depends on
42fa95540468 8501fd5815b9 b5668108487f c26267b3aba6
Depended on by
27322cd2755a 93f445fa50d8
In bookmarks
main
In tags
v0.9.16 v0.9.19 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24
Edits
3 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified CHANGELOG.md +4 −0
…
1 unchanged lines
2 2
3 3
OxygenHub runs at https://oxygenhub.io; each deploy's build is on the About page and at `/api/v1/version`.
4 4
5 +
## Unreleased
6 +
7 +
- **A phone's runner follows the app's releases (oxygenflow#84).** A runner on Android is part of the OxygenHub app, so the hub never offers it a runner build: with updates on, it is told it follows the app's releases (update the app to update it), and Admin › Runners › Updates says so.
8 +
5 9
## 0.9.15 (2026-10-28)
6 10
7 11
- **Run a pipeline's WASM jobs in your browser (oxygenflow#80, first part).** Flow › *run in this tab* (`/OWNER/NAME/flow/tab`) lists the jobs of a bookmark's pipeline whose sandbox is `runtime "wasm"` and runs them in this tab, as a runner's WASM tier does: a worker fetches the bookmark's files as a zip once, unpacks them in memory and runs each job's modules with a WASI preview 1 shim, the files at `/src` and as the current directory, the job's arguments and environment, its output shown as it comes, its exit code and time; Run all, Run and Stop per job. Nothing is claimed or reported: it shows the tier in a browser before a tab can take jobs as a runner.
…
416 unchanged lines
modified crates/hub/src/runner_updates.rs +9 −0
…
321 unchanged lines
322 322
    if !policy.updates {
323 323
        return Ok((None, format!("updates are off ({})", policy.from[0])));
324 324
    }
325 +
    // A phone's runner is part of the OxygenHub app (oxygenflow#84,
326 +
    // oxygenhub-android#7): it updates when the app does, never from a
327 +
    // runner build.
328 +
    if r.os == "android" {
329 +
        return Ok((
330 +
            None,
331 +
            format!("follows the OxygenHub app's releases: runs flow {} (update the app to update it)", version_of(&r.version)),
332 +
        ));
333 +
    }
325 334
    let Some(spec) = db::accounts::setting(pool, SETTING_REPO).await?.filter(|s| !s.trim().is_empty()) else {
326 335
        return Ok((None, "no runner releases repository is set (Admin › Runners › Updates)".into()));
327 336
    };
…
200 unchanged lines
modified crates/hub/tests/flow.rs +16 −0
…
7114 unchanged lines
7115 7115
    let (_, page) = web("POST", &format!("/admin/runners/updates/groups/{}/delete", group.id), Some(format!("_csrf={csrf}"))).await;
7116 7116
    assert!(page.contains("Test group canary removed"), "{page}");
7117 7117
    assert!(!ask(0).await.policy.updates, "back to the site's off");
7118 +
7119 +
    // A phone's runner follows the app's releases, never a runner build
7120 +
    // (oxygenflow#84): with updates on, it is offered nothing and told why.
7121 +
    let (_, page) = web("POST", "/admin/runners/token", Some(format!("_csrf={csrf}"))).await;
7122 +
    let i = page.find("--token ").unwrap() + "--token ".len();
7123 +
    let token: String = page[i..].chars().take_while(|c| !c.is_whitespace()).collect();
7124 +
    let identity = SecretKey::generate().identity("phone (OxygenFlow runner)", "", 1);
7125 +
    let info = serde_json::json!({ "arch": "aarch64", "os": "android", "version": "0.1.0 (app)", "max_jobs": 1 });
7126 +
    let reg = serde_json::json!({ "token": token, "name": "phone", "identity": hex::encode(identity.encode()), "info": info });
7127 +
    let (_, body) = call(&h.app, "POST", "/api/v1/flow/runners/register", None, Some(reg.to_string())).await;
7128 +
    let phone = serde_json::from_str::<serde_json::Value>(&body).unwrap()["credential"].as_str().unwrap().to_string();
7129 +
    web("POST", "/admin/runners/updates/policy", Some(format!("_csrf={csrf}&scope=site&updates=on"))).await;
7130 +
    let (st, body) = call(&h.app, "GET", "/api/v1/flow/runner/update", Some(&phone), None).await;
7131 +
    assert_eq!(st, StatusCode::OK, "{body}");
7132 +
    let a: flow_core::api::UpdateAnswer = serde_json::from_str(&body).unwrap();
7133 +
    assert!(a.offer.is_none() && a.note.contains("follows the OxygenHub app's releases"), "{a:?}");
7118 7134
}
7119 7135
7120 7136
/// oxygenflow#84: a version reaches a later wave once every runner of the
…
263 unchanged lines