feat: add yt update self-replacing binary updater #24

Merged
David merged 2 commits from feat/self-update into main 2026-05-14 17:31:46 +02:00
Owner

Summary

Adds yt update. Downloads the platform-appropriate prebuilt binary from the Generic Package registry and atomically replaces the running executable.

yt update                  # download + replace
yt update --dry-run        # print the URL, do nothing
yt update --url <override> # one-off URL override (mostly for tests / forks)

Configuring the URL

Per your question on this PR's parent message: the URL is the package directory base, not the repo URL. Specifically:

https://dev.a8n.run/api/packages/pandoras-box/generic/youtrack-cli

yt update appends /latest/yt-<platform> to that base. Today: yt-linux-x86_64 or yt-windows-x86_64.exe.

Plumbing: a Forgejo Actions variable (vars.YOUTRACK_CLI_UPDATE_URL) flows into the workflow env, then into a --build-arg on docker buildx build, then into ARG + ENV in oci-build/Dockerfile{,.windows}, then build.rs reads it and emits cargo:rustc-env=YOUTRACK_CLI_UPDATE_URL=<value>, then commands/update.rs reads it via env!. If the Forgejo variable is unset, build.rs falls back to the in-source default above so local cargo builds and forks both work without ceremony.

How the binary swap works

Atomic replacement uses the self-replace crate. Windows locks the running .exe, so a naive std::fs::rename over the current binary breaks on that platform; self-replace handles both unix and windows correctly. The flow: download to <current_exe>.new, chmod 0755 on unix, call self_replace::self_replace, clean up the staging file.

Platform detection uses cfg(target_os) + cfg(target_arch). Today we only ship Linux x86_64 and Windows x86_64; other targets get a clear "no prebuilt binary for target_os=X target_arch=Y" instead of a silent broken URL.

Verification

  • cargo fmt --check clean.
  • cargo clippy --all-targets -- -D warnings clean.
  • cargo test --all-targets: 181 tests pass (6 new).
  • cargo run -- update --help shows --url and --dry-run.
  • cargo run -- update --dry-run prints Updating from https://dev.a8n.run/api/packages/pandoras-box/generic/youtrack-cli/latest/yt-linux-x86_64 and exits 0 without making an HTTP call.

Coverage on this PR: pure URL construction (with and without trailing slashes), baked-base-URL is non-empty (regression guard on build.rs), dry-run makes no HTTP call (wiremock-free test that would fail if a request leaked out), 404 response surfaces in the error. The self-replace path is intentionally not unit-tested because mucking with the test runner's own binary is bad form; real verification is yt update against the live registry.

Setting the Forgejo variable

In dev.a8n.run/pandoras-box/youtrack-cli/settings/actions/variables, add a repo variable named YOUTRACK_CLI_UPDATE_URL set to:

https://dev.a8n.run/api/packages/pandoras-box/generic/youtrack-cli

(Or leave it unset; the in-source default points at the same URL, so it only matters for forks pointing at a different registry.)

Test plan

  • After merge, the Linux + Windows CI workflows build with YOUTRACK_CLI_UPDATE_URL baked in. yt --version doesn't change; yt update --dry-run prints the URL.
  • On a Linux host with the published yt: yt update, then yt --version shows the build date moving to the newest CI run.
  • On a Windows host: same.
  • yt update --url https://example.com/nothing/here returns a clear 404 error.
  • On an unsupported target (e.g. macOS, which we don't publish), the command errors with the unsupported-platform message instead of trying to download a nonexistent file.

Sequence

This is part 2 of 3 for today's items. Part 1 (TODO.md + version metadata) is PR #23. Part 3 (per-instance config redesign) is next; it'll be a bigger PR since it reworks the on-disk layout that landed in PR #19.

## Summary Adds `yt update`. Downloads the platform-appropriate prebuilt binary from the Generic Package registry and atomically replaces the running executable. ``` yt update # download + replace yt update --dry-run # print the URL, do nothing yt update --url <override> # one-off URL override (mostly for tests / forks) ``` ## Configuring the URL Per your question on this PR's parent message: the URL is the **package directory base**, not the repo URL. Specifically: ``` https://dev.a8n.run/api/packages/pandoras-box/generic/youtrack-cli ``` `yt update` appends `/latest/yt-<platform>` to that base. Today: `yt-linux-x86_64` or `yt-windows-x86_64.exe`. Plumbing: a Forgejo Actions variable (`vars.YOUTRACK_CLI_UPDATE_URL`) flows into the workflow env, then into a `--build-arg` on `docker buildx build`, then into `ARG` + `ENV` in `oci-build/Dockerfile{,.windows}`, then `build.rs` reads it and emits `cargo:rustc-env=YOUTRACK_CLI_UPDATE_URL=<value>`, then `commands/update.rs` reads it via `env!`. If the Forgejo variable is unset, `build.rs` falls back to the in-source default above so local `cargo` builds and forks both work without ceremony. ## How the binary swap works Atomic replacement uses the [`self-replace`](https://crates.io/crates/self-replace) crate. Windows locks the running `.exe`, so a naive `std::fs::rename` over the current binary breaks on that platform; `self-replace` handles both unix and windows correctly. The flow: download to `<current_exe>.new`, chmod 0755 on unix, call `self_replace::self_replace`, clean up the staging file. Platform detection uses `cfg(target_os) + cfg(target_arch)`. Today we only ship Linux x86_64 and Windows x86_64; other targets get a clear "no prebuilt binary for target_os=X target_arch=Y" instead of a silent broken URL. ## Verification - `cargo fmt --check` clean. - `cargo clippy --all-targets -- -D warnings` clean. - `cargo test --all-targets`: 181 tests pass (6 new). - `cargo run -- update --help` shows `--url` and `--dry-run`. - `cargo run -- update --dry-run` prints `Updating from https://dev.a8n.run/api/packages/pandoras-box/generic/youtrack-cli/latest/yt-linux-x86_64` and exits 0 without making an HTTP call. Coverage on this PR: pure URL construction (with and without trailing slashes), baked-base-URL is non-empty (regression guard on build.rs), dry-run makes no HTTP call (wiremock-free test that would fail if a request leaked out), 404 response surfaces in the error. The self-replace path is intentionally not unit-tested because mucking with the test runner's own binary is bad form; real verification is `yt update` against the live registry. ## Setting the Forgejo variable In `dev.a8n.run/pandoras-box/youtrack-cli/settings/actions/variables`, add a repo variable named `YOUTRACK_CLI_UPDATE_URL` set to: ``` https://dev.a8n.run/api/packages/pandoras-box/generic/youtrack-cli ``` (Or leave it unset; the in-source default points at the same URL, so it only matters for forks pointing at a different registry.) ## Test plan - [ ] After merge, the Linux + Windows CI workflows build with `YOUTRACK_CLI_UPDATE_URL` baked in. `yt --version` doesn't change; `yt update --dry-run` prints the URL. - [ ] On a Linux host with the published `yt`: `yt update`, then `yt --version` shows the build date moving to the newest CI run. - [ ] On a Windows host: same. - [ ] `yt update --url https://example.com/nothing/here` returns a clear 404 error. - [ ] On an unsupported target (e.g. macOS, which we don't publish), the command errors with the unsupported-platform message instead of trying to download a nonexistent file. ## Sequence This is part 2 of 3 for today's items. Part 1 (TODO.md + version metadata) is PR #23. Part 3 (per-instance config redesign) is next; it'll be a bigger PR since it reworks the on-disk layout that landed in PR #19.
feat: add yt update self-replacing binary updater
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 11s
78c8111858
New top-level subcommand: `yt update [--url <override>] [--dry-run]`. Downloads the platform's prebuilt binary from the Forgejo Generic Package registry and atomically replaces the running executable.

The package base URL is baked at build time from `YOUTRACK_CLI_UPDATE_URL`. The plumbing: a Forgejo Actions variable (`vars.YOUTRACK_CLI_UPDATE_URL`) flows into the workflow env, then into a `--build-arg` on `docker buildx build`, then into `ARG` + `ENV` in `oci-build/Dockerfile{,.windows}`, then `build.rs` reads it and emits `cargo:rustc-env=YOUTRACK_CLI_UPDATE_URL=<value>` for the compile, then `commands/update.rs` reads it via `env!`. If the Forgejo variable is unset, `build.rs` falls back to `https://dev.a8n.run/api/packages/pandoras-box/generic/youtrack-cli` so local cargo builds and forks both work without ceremony. The URL is the package directory base (NOT the repo URL): `yt update` appends `/latest/yt-<platform>` to it, mirroring the layout `build-linux.yml` and `build-windows.yml` publish.

Platform detection uses `cfg(target_os)` + `cfg(target_arch)`. Today we only ship `yt-linux-x86_64` and `yt-windows-x86_64.exe`; other targets get an explicit "no prebuilt binary for target_os=X target_arch=Y" instead of a silent broken URL.

Atomic replacement uses the `self-replace` crate (Armin Ronacher; the de facto answer for cross-platform self-update because Windows locks the running .exe). The flow: download to `<current_exe>.new`, chmod 0755 on unix, hand the path to `self_replace::self_replace`, mop up the staging file. Errors surface with the URL or path in context so failures are diagnosable from the message.

`--dry-run` prints the URL it would hit and returns success without making any HTTP call or filesystem write; useful in CI smoke tests and for users who want to know what's about to happen before granting write access to their `$PATH`. `--url` overrides the baked URL for the run, mostly useful for tests but also lets forks point at their own registry without rebuilding.

Tests: pure URL construction (with and without trailing slashes), baked-base-URL is non-empty (regression guard on build.rs), dry-run path makes no HTTP call, 404 response surfaces in the error. The self-replace path is intentionally not tested because mucking with the test runner's own binary is bad form; manual verification: `cargo run -- update --dry-run` prints the right URL, real `update` against the live registry replaces the binary in place.

Adds `self-replace = "1"` to dependencies. 181 tests pass (6 new); clippy/fmt clean.
Merge branch 'main' into feat/self-update
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 9s
Create release / Create release from merged PR (pull_request) Has been skipped
c1f1bfeae1
David merged commit c9c4309e1e into main 2026-05-14 17:31:46 +02:00
David deleted branch feat/self-update 2026-05-14 17:31:46 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pandoras-box/youtrack-cli!24
No description provided.