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 |