feat(update): update check at startup with prompt to update #29

Merged
deadstyle merged 5 commits from feature/12-update-check into develop 2026-10-02 12:41:30 -05:00
Owner

Summary

The startup update check for #12. When a newer release exists, lazyforge asks Update now? [Y/n] before the UI opens. Yes downloads, verifies and swaps the binary, then restarts into the new version. No starts the current one, and the next launch asks again (PM decision, #13).

Changes

  • internal/update (ADR 0009):
    • Fetches the latest release with a 2s limit, so startup never hangs when offline.
    • Only plain release versions take part, compared with golang.org/x/mod/semver, so dev and prerelease builds are never offered.
    • Detects installs managed by a package manager or in a location you can't write, and only tells you an update exists instead of replacing the binary.
    • Downloads beside the binary, verifies SHA-256 against checksums.txt, and swaps by rename. The old binary stays as lazyforge.old, and a failed restart rolls back to it.
  • cmd/lazyforge:
    • Skips the check for dev builds, --no-update-check, LAZYFORGE_NO_UPDATE_CHECK=1, config update.check = false, or when stdin isn't a terminal.
    • The restarted process gets the skip variable, so it can't loop.
    • lazyforge.old is cleaned up on every start.
  • macOS: replacing by rename gives the binary a new file, which avoids the code-signature cache killing the process.

Test plan

  • Tests cover:
    • the latest-release lookup, including a slow server hitting the deadline
    • version comparison
    • managed-install detection
    • the happy-path swap
    • five failure cases that must leave the install untouched: checksum mismatch, missing checksum line, missing archive, archive without the binary, and a failed swap
    • rollback, cleanup, and the prompt answers (an empty line means yes, end of input means no)
  • Mutation check: 34 mutations. Three weak tests were rewritten. The survivors are listed under Rulings.
  • make check exit 0 after syncing develop.

Rulings

  • The engineering lead wrote the scope and the self-update design, without a planner.
  • End of input at the prompt counts as "no", because no input isn't consent.
  • No size cap on downloads, because the archive and the checksum come from the same host and the checksum is verified before extraction.
  • Accepted mutation survivors: package-manager path prefixes (the not-writable check also covers them), the missing-checksum-line branch (still fails as a mismatch), and rollback after a failed second rename (no fault seam).

Closes #12

🤖 Generated with Claude Code

## Summary The startup update check for #12. When a newer release exists, lazyforge asks `Update now? [Y/n]` before the UI opens. Yes downloads, verifies and swaps the binary, then restarts into the new version. No starts the current one, and the next launch asks again (PM decision, #13). ## Changes - `internal/update` (ADR 0009): - Fetches the latest release with a 2s limit, so startup never hangs when offline. - Only plain release versions take part, compared with `golang.org/x/mod/semver`, so dev and prerelease builds are never offered. - Detects installs managed by a package manager or in a location you can't write, and only tells you an update exists instead of replacing the binary. - Downloads beside the binary, verifies SHA-256 against `checksums.txt`, and swaps by rename. The old binary stays as `lazyforge.old`, and a failed restart rolls back to it. - `cmd/lazyforge`: - Skips the check for dev builds, `--no-update-check`, `LAZYFORGE_NO_UPDATE_CHECK=1`, config `update.check = false`, or when stdin isn't a terminal. - The restarted process gets the skip variable, so it can't loop. - `lazyforge.old` is cleaned up on every start. - macOS: replacing by rename gives the binary a new file, which avoids the code-signature cache killing the process. ## Test plan - Tests cover: - the latest-release lookup, including a slow server hitting the deadline - version comparison - managed-install detection - the happy-path swap - five failure cases that must leave the install untouched: checksum mismatch, missing checksum line, missing archive, archive without the binary, and a failed swap - rollback, cleanup, and the prompt answers (an empty line means yes, end of input means no) - Mutation check: 34 mutations. Three weak tests were rewritten. The survivors are listed under Rulings. - `make check` exit 0 after syncing develop. ## Rulings - The engineering lead wrote the scope and the self-update design, without a planner. - End of input at the prompt counts as "no", because no input isn't consent. - No size cap on downloads, because the archive and the checksum come from the same host and the checksum is verified before extraction. - Accepted mutation survivors: package-manager path prefixes (the not-writable check also covers them), the missing-checksum-line branch (still fails as a mismatch), and rollback after a failed second rename (no fault seam). Closes #12 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Checks the latest release on interactive launches, offers to install it,
and swaps the binary by rename before re-exec. Records ADR 0009.

#12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red-first tests for #12 against the scope's seams and acceptance criteria 1-7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(update): prove status-code and swap-failure cleanup behaviors
All checks were successful
ci / check (pull_request) Successful in 1m21s
ca674133eb
Mutation check for #12: the 500 test passed on a decode error alone, and no test
covered temp-file cleanup when the swap fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deadstyle merged commit 72e3e561a6 into develop 2026-10-02 12:41:30 -05:00
deadstyle deleted branch feature/12-update-check 2026-10-02 12:41:30 -05:00
Sign in to join this conversation.
No description provided.