Skip to content

Add backup/rollback support and structured logging#34

Merged
fullstackjam merged 8 commits intomainfrom
claude/evaluate-project-quality-G8JET
Apr 19, 2026
Merged

Add backup/rollback support and structured logging#34
fullstackjam merged 8 commits intomainfrom
claude/evaluate-project-quality-G8JET

Conversation

@fullstackjam
Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds pre-upgrade binary backup and rollback functionality, structured logging to disk, and refactors the update command to support version pinning and rollback operations.

Why?

This enables users to safely upgrade OpenBoot with the ability to roll back to a previous version if needed. It also introduces always-on structured logging to ~/.openboot/logs/ for debugging and audit purposes, independent of the --verbose flag.

Changes

Backup & Rollback (internal/updater/backup.go, internal/updater/backup_test.go)

  • New backup system: Before each upgrade, the current binary is copied to ~/.openboot/backup/ with a timestamped filename including the version (e.g., openboot-1.2.3-20240419T103000Z)
  • Automatic pruning: Only the 5 most recent backups are retained; older ones are deleted
  • Rollback command: openboot update --rollback restores the newest backup
  • List backups: openboot update --list-backups shows available backups
  • Semver validation: New ValidateSemver() function validates version strings (X.Y.Z format, optional leading 'v')
  • URL helpers: Extracted checksumsURL() and binaryURL() for cleaner version-specific download paths

Structured Logging (internal/logging/logging.go, internal/logging/logging_test.go)

  • Daily log files: Logs written to ~/.openboot/logs/openboot-YYYY-MM-DD.log in JSON format
  • Multi-handler: Logs simultaneously to file (debug level) and stderr (warn/debug based on --verbose)
  • Retention: Automatically prunes logs older than 14 days on startup
  • Fallback: If log file cannot be opened, gracefully falls back to stderr-only logging
  • Session tracking: Each run logs a session_start record with version, PID, and arguments

Update Command Refactor (internal/cli/update.go)

  • New update subcommand with mutually-exclusive modes:
    • openboot update — upgrade to latest release (default)
    • openboot update --version X.Y.Z — pin to exact version
    • openboot update --rollback — restore most recent backup
    • openboot update --list-backups — show available backups
    • --dry-run flag for all modes
  • Homebrew detection: Rejects --version and --rollback for Homebrew-managed installs

Core Updater Changes (internal/updater/updater.go)

  • DownloadAndReplace() now accepts a version parameter (empty string uses /latest/ for back-compat)
  • Calls backupCurrentBinary() before atomic rename
  • Updated fetchChecksums() to accept version parameter
  • Integrated with new backup/rollback infrastructure

Brew Runner Abstraction (internal/brew/runner.go, internal/brew/runner_test.go)

  • New RunInteractive() method: Routes commands that need TTY access (e.g., sudo prompts) through a dedicated path
  • Test support: SetRunner() allows tests to inject fake runners without fork/exec
  • Updated Update() in internal/brew/brew.go to use RunInteractive() for brew upgrade

Test Coverage

  • 317 lines of comprehensive tests for backup/rollback functionality
  • 228 lines of tests for logging system
  • 140 lines of tests for brew runner abstraction
  • Updated existing tests to work with new interfaces

Testing

  • go vet ./... passes
  • Comprehensive unit tests added for backup, rollback, logging, and update command
  • Existing tests updated to support new Runner interface
  • Tested locally with --dry-run flag for all update modes

Notes for reviewer

  • The backup system is non-fatal: if backup creation fails, the upgrade proceeds with a warning
  • Logging falls back gracefully

https://claude.ai/code/session_01Syu9VBJBk7uwnjXrK7tHPV

claude added 5 commits April 19, 2026 12:45
Introduces `openboot update` subcommand with three modes beyond the
default latest-version upgrade:

- `--version X.Y.Z` pins a specific release (URL-resolved, SHA-256 verified)
- `--rollback` restores the newest pre-upgrade backup
- `--list-backups` prints the backup directory

Each direct-download upgrade now copies the running binary to
~/.openboot/backup/ before atomic rename; backupRetention=5. Homebrew-
managed installs reject --version / --rollback with a clear hint.

DownloadAndReplace now takes an explicit version (empty = /latest/, for
back-compat with AutoUpgrade). fetchChecksums signature updated
accordingly; test fixed.
…unner

The Runner interface now exposes RunInteractive for subcommands that
need /dev/tty attached (sudo prompts for `brew upgrade`). The two direct
exec.Command call sites in Update() and PreInstallChecks() are migrated
to currentRunner().RunInteractive, closing the L1 test boundary for
upgrade and index-refresh flows.

ResolveFormulaNames now also goes through Runner.Output.

The remaining exec.Command sites (brewInstallCmd and its callers) stay
exempt: they need HOMEBREW_NO_AUTO_UPDATE env + custom stdout pipe
wiring for StickyProgress streaming that Runner cannot express cleanly.
A Runner-exempt comment now flags them.

Adds runner_test.go with a recordingRunner that asserts routing and
error propagation for both Run and RunInteractive.
Adds internal/logging that wires slog.SetDefault to a multi-handler:
captures all debug-level records to a daily rotating JSON file in
~/.openboot/logs/ (0700 dir, 0600 file), and mirrors records to stderr
at LevelWarn by default (LevelDebug with --verbose).

- Retention: openboot-YYYY-MM-DD.log files older than 14 days are pruned
  in a background goroutine on startup. Only openboot-*.log is touched.
- Fallback: if the log dir or file cannot be opened, Init never errors —
  it reports once on stderr and keeps stderr-only logging.
- Session marker: one session_start record per process (version, pid, args).

Wires logging.Init in PersistentPreRunE and flushes via a deferred closer
in Execute. Adds a minimal install_started / install_completed log pair
in the installer to prove file-sink wiring end-to-end.

Tests cover dir/file permissions, session_start emission, append (not
truncate) across invocations, retention pruning (with injected clock),
fallback on unwritable dirs, and verbose/non-verbose level wiring.
@github-actions
Copy link
Copy Markdown

👋 Thanks for opening this pull request!

Before merging:

  • Code follows existing patterns in the codebase
  • go build ./... and go vet ./... pass
  • Commit message is clear and descriptive

@fullstackjam will review this soon. Thanks for contributing! 🚀

@github-actions github-actions bot added installer Package installation logic brew Homebrew related tests Tests only labels Apr 19, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 19, 2026

claude added 3 commits April 19, 2026 13:00
The log dir needs the owner-exec bit (0700) to be traversable; gosec's
G302 rule flags any Chmod mode above 0600 since it can't distinguish
files from dirs. Follows the existing nolint pattern used elsewhere.
os.Args is the audit subject of the session_start record by design.
slog handlers quote attribute values, so there is no injection path
into the surrounding log line.
… redact args

Based on self-review of PR #34:

- updater: DownloadAndReplace now takes (targetVersion, currentVersion).
  Drops the SetCurrentVersionForTesting / currentBinaryVersionFn pair that
  leaked a test-named setter into production CLI code.
- updater: exports TrimVersionPrefix; removes the duplicated trimV helper
  from internal/cli/update.go.
- cli/update: new test seams (updateIsHomebrewInstall, updateGetLatestVersion,
  updateDownloadAndReplace, updateRollbackFn, updateListBackupsFn,
  updateGetBackupDir) let L1 cover the cobra subcommand without fork/exec.
- cli/update_test.go (new): 19 cases covering mutex-flag validation,
  Homebrew-refuse branches (pin, rollback, latest) plus allowed list-backups,
  semver validation, dry-run paths (pin/latest/rollback with+without
  backups), delegation + error wrapping for download and rollback, and the
  runListBackups branches (empty / populated / list-error / dir-error).
- logging: session_start now passes os.Args through RedactArgs, replacing
  the value of "--flag=value" entries whose flag name contains any of
  {token, password, secret, key, credential} (case-insensitive) with
  "<redacted>". Space-separated form is a documented non-goal.
- logging: adds a multiHandler WithAttrs/WithGroup test covering the fan-out
  paths, plus a table-driven RedactArgs test.
@fullstackjam fullstackjam merged commit 7a34362 into main Apr 19, 2026
9 checks passed
@fullstackjam fullstackjam deleted the claude/evaluate-project-quality-G8JET branch April 19, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

brew Homebrew related installer Package installation logic tests Tests only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants