OXYGENHUB

Releases carry their source (oh-VERSION-src.tar.gz, subrepos inside) and assets have a fixed address, /OWNER/NAME/releases/download/TAG/FILE (refs andymccall/oxygen#157) 93f445fa50d8

Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 293da4873c0b
When
· 2026-10-29 11:47 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
93f445fa50d829625f9666e9f92b352e428b7105d5ce7944d3132902d7da49c8
Depends on
2f79b17e089a 380996db84e5 3f4b0ea6eabf 5533e91a2aa4 913e5ad27b17 99ca93d60bca b5668108487f d23464c0ac60 df940d75cd3d e15409084ed6
Depended on by
806615f974c0
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
6 atoms, 0 tree operations, 6 files

Browse the tree at this change

modified .flow/pipeline.kdl +3 −0
…
154 unchanged lines
155 155
        artifact "oh_arm64.deb" {
156 156
            path "/src/target/dist-flat/oh_arm64.deb"
157 157
        }
158 +
        artifact "oh-src.tar.gz" {
159 +
            path "/src/target/dist-flat/oh-src.tar.gz"
160 +
        }
158 161
        artifact "oh_riscv64.deb" {
159 162
            path "/src/target/dist-flat/oh_riscv64.deb"
160 163
        }
…
8 unchanged lines
modified CHANGELOG.md +2 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **A release's asset at a fixed address (oxygen#157).** `/OWNER/NAME/releases/download/TAG/FILE` redirects to the asset, an address known before the release exists (as GitHub's), so a packager's recipe can name it.
8 +
- **Releases carry their source (oxygen#157).** Each release now has `oh-VERSION-src.tar.gz`: the oxygenhub tree with its subrepos inside, so oh builds from it alone, reproducible and listed in SHA256SUMS, for packagers (haikuports' oh recipe).
7 9
- **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 10
9 11
## 0.9.15 (2026-10-28)
…
418 unchanged lines
modified crates/hub/src/web/mod.rs +1 −0
…
343 unchanged lines
344 344
        .route("/{owner}/{name}/releases", get(releases::list))
345 345
        .route("/{owner}/{name}/releases/new", get(releases::new_form).post(releases::create))
346 346
        .route("/{owner}/{name}/releases/tag/{*tag}", get(releases::show))
347 +
        .route("/{owner}/{name}/releases/download/{tag}/{file}", get(releases::download_by_tag))
347 348
        .route("/{owner}/{name}/releases/trust/{*tag}", get(release_trust::page))
348 349
        .route("/{owner}/{name}/releases/edit/{id}", post(releases::edit))
349 350
        .route(
…
617 unchanged lines
modified crates/hub/src/web/releases.rs +16 −0
…
390 unchanged lines
391 391
    Ok(Redirect::to(&format!("/{owner}/{name}/releases/tag/{}", r.tag)).into_response())
392 392
}
393 393
394 +
/// `GET /{owner}/{name}/releases/download/{tag}/{file}` (oxygen#157): a
395 +
/// release's asset by its tag and file name, a URL known before the release
396 +
/// exists (as GitHub's), for packagers' recipes; redirected to the asset.
397 +
pub async fn download_by_tag(
398 +
    State(app): State<Shared>,
399 +
    viewer: Viewer,
400 +
    Path((owner, name, tag, file)): Path<(String, String, String, String)>,
401 +
) -> Result<Response, Error> {
402 +
    let shell = Shell::new(&app, &format!("/{owner}/{name}/releases"), &viewer);
403 +
    let (_, row, _) = lookup(&app, &shell, &viewer, &owner, &name).await?;
404 +
    let e = |e| Error::from_db(shell.clone(), e);
405 +
    let release = rdb::get(&app.db, row.id, &tag).await.map_err(e)?.ok_or_else(|| Error::not_found(shell.clone()))?;
406 +
    let asset = rdb::asset(&app.db, release.id, &file).await.map_err(e)?.ok_or_else(|| Error::not_found(shell.clone()))?;
407 +
    Ok(Redirect::to(&format!("/{owner}/{name}/asset/{}/{}", asset.id, asset.name)).into_response())
408 +
}
409 +
394 410
pub async fn download(
395 411
    State(app): State<Shared>,
396 412
    viewer: Viewer,
…
121 unchanged lines
modified crates/hub/tests/releases.rs +6 −0
…
213 unchanged lines
214 214
    assert_eq!(v["assets"][0]["hash"], hex::encode(blake3::hash(b"\x7fELF binary").as_bytes()));
215 215
    let got = send(app, "GET", &url, None, None, None, vec![]).await;
216 216
    assert_eq!(got.body, b"\x7fELF binary");
217 +
    // By tag and name too, at a URL known before the release exists, for
218 +
    // packagers' recipes (oxygen#157): redirected to the asset.
219 +
    let by_tag = send(app, "GET", "/andy/demo/releases/download/v0.2/demo-linux.bin", None, None, None, vec![]).await;
220 +
    assert_eq!(by_tag.location.as_deref(), Some(url.as_str()));
221 +
    let none = send(app, "GET", "/andy/demo/releases/download/v0.2/nope.bin", None, None, None, vec![]).await;
222 +
    assert_eq!(none.status, StatusCode::NOT_FOUND);
217 223
    // A real build is megabytes: the API takes assets up to the configured
218 224
    // limit, not the web framework's 2 MiB default.
219 225
    let big = vec![7u8; 5 << 20];
…
359 unchanged lines
modified scripts/dist.sh +11 −0
…
145 unchanged lines
146 146
  tar -C "$OUT/stage" -czf "$OUT/$name.tar.gz" "$name"
147 147
fi
148 148
149 +
# The source as released (oxygen#157): the oxygenhub tree with its subrepos (deps/), so oh builds from it alone,
150 +
# under oxygenhub-VERSION/, for packagers (haikuports) to fetch from the
151 +
# release at a fixed checksum. Reproducible: sorted, fixed times and
152 +
# owners, gzip without a timestamp.
153 +
if [ -z "${ONLY:-}" ] || [[ src == $ONLY ]]; then
154 +
  echo "== src"
155 +
  tar --sort=name --mtime='1970-01-01 00:00:00Z' --owner=0 --group=0 --numeric-owner \
156 +
    --mode='u+rwX,go+rX,go-w' --exclude=./target --exclude=.oxy \
157 +
    --transform "s|^\.|oxygenhub-$VERSION|" -cf - . | gzip -9n >"$OUT/$BIN-$VERSION-src.tar.gz"
158 +
fi
159 +
149 160
rm -rf "$OUT/stage"
150 161
(cd "$OUT" && shopt -s nullglob && sha256sum -- *.tar.gz *.zip *.deb >SHA256SUMS)
151 162
# DIST_FLAT=DIR: the same files again under names without the version, for
…
12 unchanged lines