OXYGENHUB

The nightly gc keeps Flow's artifacts 4597750166e7

Artifacts are blobs in the repository's store named only by the
database (flow_artifacts.blob), so gc found them unreachable and pruned
each one once core.gc-grace had passed: coverage files, test reports
and dist files from any run older than 14 days. gc_all now lists every
artifact blob a repository's runs keep and passes them to gc_keeping as
roots; a repository whose listing fails is skipped, not collected. The
artifact test runs a gc with no grace and still downloads its artifact
(and fails without this). Pins oxygenflow 9c2b3731 (oxygen 3db00e05).

closes #126
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-10 23:18 UTC
Signature
✔ signed by Andy McCall
Hash
4597750166e7b5ed0afe68bc4ad9c93621c8970937ee152a9c03721c19bf382c
Depends on
10708ab8f54d 380996db84e5 42fa95540468 8a3334b7a364 8aafd2023dec a3c8288b0141 b1e92a6233ee b5668108487f
Depended on by
e4203e141e90 5d90e0e045e2 c8df33a6829f a36bab6b9ffc
In bookmarks
api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
In tags
v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
7 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 +
- **Flow's artifacts survive the nightly gc (#126).** They are named only in the hub's database, so garbage collection found them unreachable and would have deleted each one 14 days after its run: coverage files, test reports and dist files alike. The nightly gc now keeps every artifact a repository's runs name.
8 +
7 9
- **Transfer a project (#125).** A board's Settings move it to another owner you may make boards under, such as one of your organisations; so do the API (`owner` on `PATCH /projects/{owner}/{n}`) and `oh project transfer N --to OWNER`. It takes the next number there, with its columns, cards and privacy, and its old address redirects to it.
8 10
9 11
## 0.4.1 (2026-10-10)
…
149 unchanged lines
modified crates/hub/src/db/flow.rs +11 −0
…
838 unchanged lines
839 839
}
840 840
841 841
/// Job `job_id`'s artifacts, by name.
842 +
/// Every artifact blob a repository's runs keep, for garbage collection to
843 +
/// keep too (#126): the database is the only thing that names them.
844 +
pub async fn artifact_blobs(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<String>> {
845 +
    sqlx::query_scalar(
846 +
        "SELECT DISTINCT a.blob FROM flow_artifacts a JOIN flow_jobs j ON j.id = a.job_id JOIN flow_runs r ON r.id = j.run_id WHERE r.repo_id = ?",
847 +
    )
848 +
    .bind(repo_id)
849 +
    .fetch_all(pool)
850 +
    .await
851 +
}
852 +
842 853
pub async fn artifacts(pool: &Pool, job_id: i64) -> sqlx::Result<Vec<Artifact>> {
843 854
    sqlx::query_as("SELECT * FROM flow_artifacts WHERE job_id = ? ORDER BY name").bind(job_id).fetch_all(pool).await
844 855
}
…
27 unchanged lines
modified crates/hub/src/maintenance.rs +14 −2
…
38 unchanged lines
39 39
            continue;
40 40
        }
41 41
        let label = slug.clone();
42 +
        // Flow's artifacts are named only in the database: kept as roots,
43 +
        // or gc would prune them after the grace period (#126). A listing
44 +
        // that fails skips this repository rather than risk them.
45 +
        let keep: Vec<oxy_core::hash::ObjectRef> = match db::flow::artifact_blobs(&app.db, row.id).await {
46 +
            Ok(blobs) => blobs.iter().filter_map(|b| oxy_core::hash::ObjectRef::from_hex(b)).collect(),
47 +
            Err(e) => {
48 +
                tracing::warn!("gc: {slug}: listing its artifacts: {e}; skipped");
49 +
                continue;
50 +
            }
51 +
        };
42 52
        let started = std::time::Instant::now();
43 -
        let result =
44 -
            app.repos.write(&name, move |repo, _| Ok(repo.gc_with(false, &mut |phase| tracing::debug!("gc {label}: {phase}"))?)).await;
53 +
        let result = app
54 +
            .repos
55 +
            .write(&name, move |repo, _| Ok(repo.gc_keeping(false, &keep, &mut |phase| tracing::debug!("gc {label}: {phase}"))?))
56 +
            .await;
45 57
        let d = match result {
46 58
            Ok(r) => Done {
47 59
                summary: format!(
…
46 unchanged lines
modified crates/hub/tests/flow.rs +13 −0
…
890 unchanged lines
891 891
    assert_eq!((st, got == body), (StatusCode::OK, true));
892 892
    assert_eq!(get_bytes(&h.app, &format!("/andy/demo/flow/{}/artifacts/build/none", run.number)).await.0, StatusCode::NOT_FOUND);
893 893
894 +
    // The nightly gc keeps it (#126): only the database names it, so with
895 +
    // no grace period a gc that did not root it would prune it at once.
896 +
    {
897 +
        let name = RepoName::parse("andy/demo").unwrap();
898 +
        let mut hosted = oxy_core::repo::Repo::open(&h.app.repos.dir(&name)).unwrap();
899 +
        hosted.config.set("core.gc-grace", "0").unwrap();
900 +
        hosted.save_config().unwrap();
901 +
    }
902 +
    let done = oxygenhub::maintenance::gc_all(&h.app).await;
903 +
    assert!(done.iter().all(|d| !d.failed), "{done:?}");
904 +
    let (st, got) = get_bytes(&h.app, &link).await;
905 +
    assert_eq!((st, got == body), (StatusCode::OK, true), "the artifact survives gc");
906 +
894 907
    // A writer attaches the artifact to a release from the run page; the
895 908
    // release then names the run and job that built it, as does the API.
896 909
    let form = |f: String| {
…
1456 unchanged lines
replaced deps/oxygenflow +0 −0

No line changes.