Skip to content

feat: add zemo upgrade command#12

Merged
hidetzu merged 1 commit into
mainfrom
feat/zemo-upgrade
May 6, 2026
Merged

feat: add zemo upgrade command#12
hidetzu merged 1 commit into
mainfrom
feat/zemo-upgrade

Conversation

@hidetzu
Copy link
Copy Markdown
Owner

@hidetzu hidetzu commented May 6, 2026

Summary

Add a self-updater that downloads the latest release from GitHub and replaces the running binary in place. Closes the user-experience gap with similar single-binary CLIs (deno upgrade, bun upgrade, rustup self update) and removes the need to re-run the README curl one-liner.

This PR also bumps the version to 0.3.0, intended to be tagged immediately after merge.

Supported targets

  • Linux x86_64 (glibc only) — released asset is built with -Dtarget=x86_64-linux-gnu, so musl-built binaries refuse to upgrade rather than risk landing a glibc binary on Alpine/musl-only systems.
  • macOS x86_64 / aarch64
  • Windows is deferred to feat: add Windows support to zemo upgrade #11 — needs zip extraction and the running-.exe self-replacement workaround. zemo upgrade on Windows fails fast with UnsupportedTarget instead of confusing gzip errors.

Related issue

Closes #10. Windows support tracked in #11.

Changes

  • New Command.upgrade: upgrade.UpgradeMode variant in cli.zig; routed from parseArgs and dispatched in run.
  • Two modes: zemo upgrade (download + replace) and zemo upgrade --check (version compare only, no filesystem changes).
  • New src/upgrade.zig module with helpers split by responsibility:
    • Pure: assetName, binaryBasename, compareVersions, findAssetUrl, parseReleaseJson.
    • I/O: fetchLatestReleaseJson, downloadToMemory, extractZemoBinary, installPath, replaceBinary.
  • assetName is intentionally strict: only emits an asset name for targets whose released artifact matches the local ABI. Linux is gated on builtin.abi == .gnu; Windows always returns UnsupportedTarget for now.
  • HTTP client wiring (std.http.Client) with proper User-Agent (zemo/0.3.0 (+https://github.com/hidetzu/zemo)) and Accept: */* headers — tighter UA strings get rejected by GitHub Releases CDN.
  • Archive extraction via std.compress.flate.Decompress (gzip) + std.tar.Iterator.
  • Atomic self-replacement on Unix: write to sibling <install_path>.new with mode 0o755, then renameAbsolute over the running binary.
  • HELP_TEXT and README updated.
  • Version bump: VERSION constant updated from 0.2.0 to 0.3.0.

Testing

  • zig build test passes locally (host is Linux musl; affected assetName test correctly skips and the new assetName: rejects musl Linux test exercises the negative path)
  • Cross-build matrix: x86_64-linux-gnu, x86_64-linux-musl, x86_64-macos, aarch64-macos, x86_64-windows all build green
  • Manually verified the affected command(s):
    • zemo upgrade --check against the live GitHub API correctly reports Already up to date (v0.3.0) after self-bump (and Newer version available: ... when VERSION is artificially lowered)
    • zemo upgrade end-to-end: temporarily lowered VERSION to 0.0.1, ran /tmp/zemo-test upgrade, observed Downloading zemo-x86_64-linux-gnu.tar.gz... and Upgraded v0.0.1 -> v0.2.0. /tmp/zemo-test --version then reported zemo 0.2.0, confirming the binary was replaced.

New tests added (in upgrade.zig):

  • assetName: returns asset for current target — runs only on linux + x86_64 + gnu, macos + (x86_64|aarch64); skips otherwise so cross-target test runs stay green.
  • assetName: rejects musl Linux to avoid replacing musl with glibc binary — explicit negative test.
  • assetName: rejects Windows until #11 lands zip extraction — explicit negative test.
  • compareVersions: equal / older / newer / handles \v` prefix / invalid input` — five cases covering the comparison enum.
  • parseReleaseJson: extracts tag_name and assets (with deliberately-included unknown fields to validate ignore_unknown_fields) and parseReleaseJson: invalid JSON returns error.
  • findAssetUrl: returns URL when asset name matches and findAssetUrl: returns null when not found.
  • installPath: returns absolute non-empty path.
  • 4 parseArgs tests covering both upgrade modes plus invalid-arg paths.

HTTP / archive / replacement paths are intentionally untested at the unit level (mirrors the project convention from fetchLatestReleaseJson / git.isGitRepo style); validated via the manual smoke test above.

Notes for reviewers

  • The User-Agent saga is worth flagging: the initial zemo-upgrade UA worked for api.github.com but produced HTTP 400 from releases/download/... (which redirects to objects.githubusercontent.com). Switching to the proper <product>/<version> (+<url>) form fixed it. fetchLatestReleaseJson still uses zemo-upgrade for now — could be unified in a follow-up but isn't strictly necessary.
  • replaceBinary uses @enumFromInt(0o755) to construct an executable std.Io.File.Permissions. For Windows the Permissions shape is different, which is why Windows support is deferred (feat: add Windows support to zemo upgrade #11).
  • Archive extraction matches by std.fs.path.basename, which lets us ignore the platform-specific top-level directory inside the tarball (zemo-x86_64-linux-gnu/zemo etc.).
  • Memory usage during upgrade is bounded by archive size (~1 MB for the current zemo release) since downloadToMemory and extractZemoBinary both keep buffers in memory rather than streaming to disk. Acceptable for the current binary size; can revisit if releases grow.
  • After merge: git checkout main && git pull && git tag v0.3.0 && git push origin v0.3.0.

@hidetzu hidetzu force-pushed the feat/zemo-upgrade branch from f0c1358 to 547019a Compare May 6, 2026 03:05
@hidetzu hidetzu merged commit 7da74d6 into main May 6, 2026
6 checks passed
@hidetzu hidetzu deleted the feat/zemo-upgrade branch May 6, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add zemo upgrade self-updater

1 participant