Skip to content

feat(cli): add machineid update self-update command - #17

Merged
christiangda merged 2 commits into
mainfrom
feat/update-command
Sep 19, 2026
Merged

christiangda merged 2 commits into
mainfrom
feat/update-command

Conversation

@christiangda

Copy link
Copy Markdown
Contributor

Summary

Adds machineid update, a self-update verb for the CLI only. The importable library gains no code, no network access and no dependencies; everything lives in internal/selfupdate (standard library only) and cmd/machineid/update.go is a thin adapter.

Builds on #15 (Go 1.27 bump), now merged. Design and rationale: .plan/update-command.md (local, not committed), modelled on rrcli update in ds-utils with the private-repo machinery removed.

How it works

  • No API, no token. The newest tag comes from the redirect that github.com/slashdevops/machineid/releases/latest returns, a tag's existence from its release page, and assets from releases/download/<tag>/<name>. None of these carry the REST API's 60 req/h unauthenticated quota. Downloads only follow redirects to github.com and *.githubusercontent.com.
  • Hourly budget. At most 5 live lookups per rolling hour per user, the answer cached for an hour in the user cache directory (0600). -check serves the cache and, over budget, still answers from the stale cache with the next allowed time; a real update refuses with exit 1. MACHINEID_UPDATE_BUDGET overrides, 0 disables. A GitHub 429/403 Retry-After is persisted as a backoff. A corrupt or unwritable state file never blocks an update.
  • Nothing runs unasked. Normal machineid runs never touch the network; there is no background check and no nag. Root is never requested; the remedy prints the sudo command.
  • Install paths. Linux zip and the new macOS universal zip replace the binary in place via atomic rename. The macOS .pkg is used only when the running binary is in /usr/local/bin. go install is the fallback and the only method on Windows (no Windows assets are published). Both fixed-destination methods are guarded against installing somewhere other than the running binary; -force overrides.
  • Verification. SHA-256 always. Sigstore bundle via cosign when installed, Apple signature via pkgutil / codesign; -require-signature makes a skipped check fatal.
  • Exit codes. 0 ok / current / -check / declined, 1 prerequisite not met (nothing attempted), 2 bad arguments (as elsewhere in the CLI), 3 attempted and failed.

Release pipeline

release.yml now also publishes machineid-darwin-universal.zip + .zip.sha256 from the signed universal binary. Asset names are pinned by internal/selfupdate/asset_test.go and documented in CONTRIBUTING as a contract.

Decisions taken (say if you want any changed)

Chosen
Exit codes 0/1/2/3, keeping 2 = invalid arguments CLI-wide
Budget 5 per hour, env override
Signatures opportunistic by default, strict with -require-signature
Auto-check never
macOS zip published in this PR
Location internal/selfupdate, unimportable from outside the module

Verification

  • gofmt, vet, golangci-lint (0 issues), go test -race ./... on darwin; vet + test binaries compile for linux and windows; go fix / betteralign / modernize clean.
  • Tests use an in-memory fake GitHub (httptest.NewTestServer), a fake exec and an injected clock; nothing reaches the real network.
  • Smoke test with a dev build against the real GitHub: live lookup resolved v0.1.3 (4 of 5 checks left this hour), a second -check was served from cache, and the install-target guard refused a binary outside /usr/local/bin with the -force remedy (exit 1). A normal machineid -debug run logged no network activity.

The first release the updater can move from is the one that ships it; it can move to any tag that carries assets, including older ones via -version.

🤖 Generated with Claude Code

CLI only: the importable library gains no code, no network access and no
dependencies. All logic lives in internal/selfupdate (standard library
only) and cmd/machineid/update.go is a thin adapter.

Release lookup for a public repository
- The newest tag is read from the redirect that
  github.com/<repo>/releases/latest returns; a tag's existence from its
  release page; assets from releases/download/<tag>/<name>. No REST API,
  no gh, no token, so the unauthenticated 60/h quota never applies.
- Redirects are followed only to github.com and *.githubusercontent.com.

Hourly budget
- At most 5 live lookups per rolling hour per user (MACHINEID_UPDATE_BUDGET
  overrides, 0 disables), the last answer cached for an hour in the user
  cache directory (0600). -check serves the cache; over budget it still
  answers from the stale cache while a real update refuses with the next
  allowed time. 429/403 Retry-After is persisted as a backoff. A corrupt
  or unwritable state file never blocks an update.

Install
- Linux zip and the new macOS universal zip replace the binary in place
  with an atomic rename staged in the target's directory. The macOS .pkg
  is used only when the running binary is in /usr/local/bin and needs
  root, which is never requested: the remedy prints the sudo command.
  `go install` is the fallback and the only method on Windows.
- Both fixed-destination methods are guarded against installing somewhere
  other than the running binary; -force overrides.
- SHA-256 is mandatory. Sigstore bundles are verified with cosign when
  present, Apple signatures with pkgutil/codesign; -require-signature
  makes a skipped check fatal.
- Exit codes: 0 ok/current/-check/declined, 1 prerequisite (nothing
  attempted), 2 bad arguments, 3 attempted and failed.

Release pipeline
- Publish machineid-darwin-universal.zip + .zip.sha256 from the signed
  universal binary, so go-installed macOS copies can update in place.

Docs
- README "Updating the CLI" section and security bullets, CONTRIBUTING
  asset-name contract, package docs, copilot-instructions layout.

Tests use an in-memory fake GitHub (httptest.NewTestServer), a fake exec
and an injected clock; nothing reaches the real network.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@christiangda christiangda self-assigned this Sep 19, 2026
…or machineid update

- Skip the POSIX file-mode and executable-bit assertions on Windows.
- The fake go env returns GOBIN verbatim so filepath.Join cannot rewrite
  separators on Windows; a new test covers the GOPATH/bin fallback.
- docs/updating.md: quick start, walkthrough, flags, exit codes,
  per-platform table, install-target guard, cache and hourly budget with
  state-file paths, verification, script/cron/CI recipes, privacy,
  troubleshooting keyed by error text, FAQ. README links to it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@christiangda
christiangda merged commit 1ce1d8b into main Sep 19, 2026
8 checks passed
@christiangda
christiangda deleted the feat/update-command branch September 19, 2026 18:11
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.

1 participant