Skip to content

Upgrade command

Muhammet Şafak edited this page Jul 26, 2026 · 1 revision

Home / Commands / upgrade

commitbrief upgrade

Checks GitHub Releases for a newer CommitBrief and installs it, using the right mechanism for however the running binary got there in the first place. One command works whether you installed via Homebrew, Scoop, go install, or a raw GitHub Releases tarball.

Introduced in v1.15.0 (ADR-0034).

Synopsis

commitbrief upgrade [--check]

What it does

  1. Resolves the running binary's real path (symlinks followed — Homebrew's bin/commitbrief is a symlink into the Cellar) and classifies how it was installed.
  2. Asks the GitHub Releases API for the latest published (non-prerelease) tag.
  3. Compares it against the running version.
  4. If a newer version exists, shows what it's about to do and asks for confirmation (skipped by --check, --json, or global --yes).
  5. Acts — either delegating to a package manager or replacing the binary itself.

The four install methods

Method Detected by What upgrade does
Homebrew resolved path contains a Cellar/commitbrief/ segment runs brew upgrade commitbrief
Scoop resolved path under $SCOOP/apps/commitbrief/ (or %USERPROFILE%\scoop\apps\commitbrief\) runs scoop update commitbrief
go install resolved path is $GOBIN/commitbrief, $GOPATH/bin/commitbrief, or ~/go/bin/commitbrief runs go install github.com/CommitBrief/commitbrief/cmd/commitbrief@latest
Manual anything else — including a distro package CommitBrief doesn't publish (nix, AUR, apt) downloads, verifies, and swaps the binary in place (below)

Why delegate instead of always self-replacing? Overwriting a Homebrew- or Scoop-owned binary desynchronizes that manager's own version bookkeeping — its next upgrade either conflicts with a file it doesn't recognize as its own, or silently reverts the swap. Delegating keeps every manager's metadata correct.

Delegated commands inherit stdout/stderr directly: their output is streamed straight through, never parsed, so an upstream format change in brew/scoop/ go can't break CommitBrief. If the owning tool isn't on PATH, upgrade errors naming which tool it expected.

Misclassification is safe. A package manager CommitBrief doesn't specifically detect (nix, AUR, apt, a hand-built binary in /usr/local/bin) falls into "Manual" by default. Those locations are typically read-only (/nix/store) or root-owned (/usr/bin), so the permission gate below stops the attempt — cleanly, before any download — rather than silently corrupting a package-manager-owned file.

Manual replacement pipeline

Only a manual install reaches this path.

  1. Permission gate — before any network I/O. CommitBrief probes whether the binary's directory is writable (replacement is a rename, and rename permission comes from the parent directory, not the file). If the probe fails, nothing is downloaded — see Permission gate below.
  2. Download. The release archive for your GOOS/GOARCH (commitbrief_<version>_<os>_<arch>.tar.gz, or .zip on Windows) plus checksums.txt, over HTTPS with redirects followed.
  3. Verify. SHA-256 of the downloaded archive is compared against its line in checksums.txt. A mismatch aborts immediately — the temp file is deleted and the currently installed binary is left untouched.
  4. Extract. Only the single binary entry (commitbrief / commitbrief.exe) is pulled from the archive, into a temp file in the same directory as the target (so the final rename stays on one filesystem and is atomic). The existing binary's file mode is preserved rather than forced to a default.
  5. Replace — platform-specific (below).
  6. Verify the result with two independent checks. Both run on the manual path only — never after a delegated Homebrew/Scoop/go install upgrade, since a package manager may relocate its own binary and neither check has anything fixed to compare against. Either way it's a warning only — it never turns a completed upgrade into a failure: the file swap already succeeded.
    • Does the swapped binary itself report the right version? CommitBrief re-runs it at the resolved path with --version and checks the output names the release just installed. Because this execs the resolved path directly, it bypasses PATH entirely — a mismatch here means the file itself is wrong, not that something else is shadowing it. A failure to re-run the binary at all (a sandboxed or hardened mount, for example) is reported the same way.
    • Is commitbrief on PATH actually this binary? Separately, CommitBrief resolves commitbrief the way your shell would (a PATH lookup, with symlinks resolved) and compares that against the binary it just upgraded. If they differ, it warns and names both paths — the one that was upgraded, and the one your shell will actually run next. This is the check that catches another commitbrief shadowing the one just replaced; the first check can't, since it never goes through PATH.

Unsigned checksums — the honest limit. checksums.txt itself carries no signature. The trust anchor is TLS to github.com (and the CDN it redirects downloads to). That defends against a truncated or corrupted download, or an inconsistent CDN response — not against a compromised release: an attacker who could publish a malicious release could publish a matching malicious checksums.txt alongside it. Release-artifact signing (cosign or goreleaser's signs: block) is listed as future work, not shipped today (ADR-0034).

Only the binary is replaced. Bundled man pages are not installed or refreshed by a manual-install upgrade — if you placed man pages yourself, you manage them yourself too.

Replace — Unix

A single os.Rename swaps the temp file into place. This is atomic and legal even while the process is running: the running binary keeps its own inode, so the swap is invisible to it. On any failure, nothing has changed — no rollback is needed.

Replace — Windows and the .old cleanup

Windows refuses to overwrite a running .exe, but does allow renaming it, so the live binary is moved aside first (targettarget.old), then the new one is renamed into target's place. If that second rename fails, the original is renamed back from .old.

If the rollback also fails — i.e. the swap-in failed and restoring the original from .old also failed — the target directory is left with no binary at all. The returned error names both failures and points you at <binary>.old: rename it back to <binary> yourself to recover. This is a rare failure mode, but the error message exists specifically to make it recoverable instead of silent.

.old can't be deleted while the process that renamed it away is still running. The next time you run commitbrief upgrade on Windows, it sweeps away any stale .old left over from a previous run, best-effort, before doing anything else.

Permission gate

Before any download, CommitBrief checks whether it can write to the directory holding the binary. If it can't:

  • Nothing is downloaded. The abort happens before any network request.
  • CommitBrief prints the exact sudo commitbrief upgrade command, plus the releases page URL, as copyable next steps.
  • CommitBrief never runs sudo itself. Whether to elevate is entirely your call.

This same gate is what makes an unrecognized distro-packaged install (/nix/store, root-owned /usr/bin) safe: it's classified as "Manual" (no matching marker), but the read-only or root-owned location trips this gate before any file gets touched.

--check

Reports what would happen and installs nothing:

commitbrief upgrade --check
  • Prints the current version, the latest version, the detected install method, and what action would be taken — but stops there.
  • On a manual install, it also runs the permission gate and prints the same not-writable warning and recovery hint if the target can't be written — so --check reports the install a plain upgrade would actually refuse, rather than promising one that would never happen. Not run under --json (below), which prints only the report object.
  • Always exits 0 on any non-error outcome, including "a newer version is available" and the not-writable warning above. The exit-code contract has only two codes (0/1, see Exit codes); making "an update exists" a distinct exit code would be indistinguishable from a genuine failure to any script checking $?. Use the printed report (or --json's update_available field, not the exit code) to detect an available update programmatically.

--json

--json implies --check. It prints exactly one report object to stdout and installs nothing — whether or not --check was also passed:

commitbrief upgrade --json
{
  "current": "v1.14.0",
  "latest": "v1.15.0",
  "method": "homebrew",
  "update_available": true,
  "action": "brew upgrade commitbrief"
}

action describes what would run for a package-managed method, or the asset name that would be downloaded for a manual install. When there's no update, update_available is false and action is empty. --json never triggers a confirmation prompt or an actual install — a delegated package manager writes its own human-shaped output that would corrupt a machine-readable stream, so an automated caller that actually wants to install should invoke the package manager directly.

On a development build (a locally built binary whose version string isn't a parseable semver, e.g. dev), --json still emits a report rather than silence: current carries the raw version string as-is, and update_available is false. Without --json, the same case prints (or, for a bare upgrade, errors with) a "this is a development build" message instead.

Confirmation

Without --check/--json, an available update is confirmed before anything is installed:

  • The global --yes flag skips the prompt.
  • A non-interactive run (no TTY) without --yes aborts rather than proceeding unattended — the same non-interactive-abort behavior as commitbrief commit.

Exit codes

Code Condition
0 up to date, --check/--json ran cleanly (update available or not), or the upgrade installed successfully
1 network/API error, no published release, checksum mismatch, unwritable target, missing package-manager tool, a delegated command exiting non-zero, or a development build with a bare upgrade

See Exit codes for the project-wide two-code contract this follows.

Privacy

commitbrief upgrade is the only network request CommitBrief makes on its own behalf, and it only happens when you run this command — there is no passive or background version check, no nudge printed after a review, and no config key that would turn one on. The request carries nothing about you: an unauthenticated GET to api.github.com with a commitbrief/<version> User-Agent and nothing else (ADR-0034).

See also

Clone this wiki locally