upgrade

package
v1.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: GPL-3.0 Imports: 20 Imported by: 0

Documentation

Overview

Package upgrade implements `commitbrief upgrade`: detecting how the running binary was installed, asking GitHub Releases whether a newer version exists, and either delegating to the owning package manager or replacing a manually installed binary in place. See ADR-0034.

Index

Constants

View Source
const ChecksumsFile = "checksums.txt"

ChecksumsFile is the name goreleaser gives the checksum manifest attached to every release (.goreleaser.yaml → checksum.name_template).

View Source
const DefaultAPIURL = "https://api.github.com/repos/CommitBrief/commitbrief/releases/latest"

DefaultAPIURL is the only endpoint CommitBrief ever contacts on its own behalf, and only when the user runs `commitbrief upgrade` (ADR-0034 §D3 — there is no automatic update check). "latest" excludes prereleases, so -rc tags are never offered.

View Source
const ModulePath = "github.com/CommitBrief/commitbrief/cmd/commitbrief"

ModulePath is the `go install` target for CommitBrief.

View Source
const ReleasesPage = "https://github.com/CommitBrief/commitbrief/releases"

ReleasesPage is shown to the user when an automated path is not available (no asset for their platform, unwritable target).

Variables

View Source
var (
	// ErrNotWritable means the directory holding the binary cannot be
	// written by this user. Reported before any download happens.
	ErrNotWritable = errors.New("target directory is not writable")
	// ErrAssetMissing means the release has no archive for this platform.
	ErrAssetMissing = errors.New("no release asset for this platform")
	// ErrChecksumMismatch means the downloaded bytes did not match
	// checksums.txt. Nothing is installed.
	ErrChecksumMismatch = errors.New("checksum mismatch")
)
View Source
var (
	// ErrRateLimited is the unauthenticated GitHub API hourly cap.
	ErrRateLimited = errors.New("github api rate limit exceeded")
	// ErrNoRelease means the repository has no published release.
	ErrNoRelease = errors.New("no published release found")
	// ErrBadResponse means the GitHub API answered — the server was
	// reached, and returned a 200 — but the body did not decode as the
	// expected JSON shape. Distinct from a network failure: the request
	// itself succeeded, only the payload was unusable.
	ErrBadResponse = errors.New("could not parse the github release response")
)
View Source
var ErrToolMissing = errors.New("package manager not found on PATH")

ErrToolMissing means the package manager that owns this install is not on PATH, so the upgrade cannot be delegated.

Functions

func AssetName

func AssetName(version, goos, goarch string) string

AssetName mirrors the archive name_template in .goreleaser.yaml:

{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ arch }}

where .Version carries no leading "v", amd64 renders as x86_64, 386 as i386, and Windows archives are .zip while everything else is .tar.gz. If that template ever changes, this function changes with it.

func BinaryEntryName

func BinaryEntryName(goos string) string

BinaryEntryName is the archive entry holding the executable itself. goreleaser places it at the archive root next to LICENSE/README.

func CleanupStale

func CleanupStale(target string)

CleanupStale removes scratch files a previous, interrupted upgrade could not clean up itself. A normal InstallManual run's defers remove its own ".commitbrief-dl-*" (downloaded archive) and ".commitbrief-bin-*" (extracted binary) temp files, but Ctrl-C kills the process before any defer runs, so an interrupted upgrade leaves one of them — up to ~12 MiB — sitting next to the target binary forever, until the next `upgrade` sweeps it here.

A concurrent upgrade's in-flight temp file could in principle be swept out from under it by this same glob. That upgrade simply fails on its next read or write of the now-missing file; it never gets far enough to touch the installed binary, so the loser of that race fails safely rather than corrupting anything.

Also runs cleanupOld, the platform-specific half of this sweep: the moved-aside ".old" binary a previous Windows upgrade could not delete while it was still executing (a no-op on Unix, where the rename leaves nothing behind).

func Command

func Command(m Method) []string

Command returns the argv that upgrades a package-managed install. It returns nil for MethodManual, which is handled by InstallManual.

Delegating rather than overwriting the file is the core decision of ADR-0034 §D1: replacing a brew- or scoop-owned binary desynchronizes the manager's metadata, and its next upgrade either conflicts or silently reverts the change.

func InstallManual

func InstallManual(ctx context.Context, o ManualOptions) error

InstallManual downloads the release archive for this platform, verifies its SHA-256 against checksums.txt, extracts just the binary, and swaps it into place.

Trust model: the anchor is TLS to github.com. checksums.txt is not signed, so this detects a truncated or corrupted download, not a compromised release (ADR-0034 §D7).

func ParseChecksums

func ParseChecksums(data []byte) map[string]string

ParseChecksums reads a "<sha256> <filename>" manifest into a filename → hex-sum map. A leading '*' on the filename marks binary mode in the sha256sum format and is not part of the name.

func PreflightWritable

func PreflightWritable(target string) error

PreflightWritable reports whether the binary can be swapped. It probes the *directory*, not the file: replacement is a rename, and rename permission comes from the parent directory. This is what stops the manual path from touching a root-owned /usr/bin or a read-only /nix/store — a distro-packaged install that Detect could only classify as "manual".

func ResolveExe

func ResolveExe() (string, error)

ResolveExe returns the running binary's path with symlinks resolved. Resolution is mandatory, not cosmetic: Homebrew installs the binary into the Cellar and links it from <prefix>/bin, so an unresolved path hides the one marker that identifies a brew install.

func Run

func Run(ctx context.Context, argv []string, out, errOut io.Writer) error

Run executes argv with its output streamed straight through to the user. The delegated tool's output is never parsed, so an upstream format change cannot break CommitBrief.

argv is passed to exec directly — no shell is involved, per the argv-not-shell rule in the engineering standards.

func SamePath

func SamePath(a, b, goos string) bool

SamePath reports whether a and b name the same filesystem location, under the platform rules normalizePath already applies for the marker comparisons above: case-insensitive and separator-normalized on Windows (whose filesystem is not case-sensitive), exact bytes everywhere else. goos is a parameter rather than read from runtime for the same reason Env.GOOS is: it lets a non-Windows host exercise the Windows comparison rules in a test.

Exported for internal/cli's shadowingPath, which compares a PATH-resolved binary against the one just upgraded. Without this, it would warn about a shadowing commitbrief on Windows purely from a letter-case or `\`-vs-`/` difference that filepath.EvalSymlinks does not normalize away — a false positive, not a real shadow.

Types

type Asset

type Asset struct {
	Name               string `json:"name"`
	BrowserDownloadURL string `json:"browser_download_url"`
}

Asset is one file attached to a release.

type Client

type Client struct {
	HTTP      *http.Client
	Assets    *http.Client // used by Download; falls back to HTTP when nil
	APIURL    string
	UserAgent string
}

Client talks to the GitHub Releases API. APIURL is a field so tests can point it at an httptest server — no test ever reaches github.com.

func NewClient

func NewClient(version string) *Client

NewClient returns a client that identifies itself with the running CommitBrief version. HTTP (the API client) gives up after 10 seconds — a release-metadata response is a few KB and fast.

Assets (used by Download) deliberately has no whole-request Timeout. http.Client.Timeout covers the entire round trip, including reading the response body, and a release archive can be several megabytes — a fixed deadline there fails a slow or throttled connection outright, permanently, no matter how much of the file already arrived. Instead it is bounded by ResponseHeaderTimeout (a stalled server still gives up after 30s) and by the context passed to Download for cancellation.

Assets' Transport is a *clone of http.DefaultTransport*, not a bare &http.Transport{} — a zero-valued Transport is a materially different, worse thing than "DefaultTransport with one field changed". A bare Transport has Proxy == nil, so it silently ignores HTTPS_PROXY/ HTTP_PROXY on a proxied network — while Latest's client (nil Transport ⇒ http.DefaultTransport) still honors it, so a user behind a corporate proxy would see `upgrade` correctly detect an update and then fail to download it. A bare Transport also has no DialContext and TLSHandshakeTimeout == 0; since ResponseHeaderTimeout only starts counting after connect + TLS finish, a blackholed endpoint would fall back to the OS TCP timeout (commonly 75s+) with an unbounded TLS handshake on top of that — nothing else in this codebase bounds it, since the context passed in has no deadline of its own.

func (*Client) Download

func (c *Client) Download(ctx context.Context, url string, w io.Writer) error

Download streams url into w. Redirects are followed (GitHub sends release downloads to objects.githubusercontent.com). Uses c.Assets — the client with no whole-request timeout — falling back to c.HTTP so a hand-constructed Client (as in tests) still works.

func (*Client) Latest

func (c *Client) Latest(ctx context.Context) (*Release, error)

Latest fetches the newest published (non-prerelease) release.

type Env

type Env struct {
	ExePath     string // resolved (symlinks evaluated) path of the running binary
	GOOS        string
	Scoop       string // $SCOOP
	UserProfile string // %USERPROFILE%
	GOBIN       string
	GOPATH      string
	Home        string
}

Env is every piece of ambient state Detect reads. It is passed in rather than read from os.Getenv/runtime inside Detect so a macOS host can exercise the Windows and Scoop branches in a unit test.

func CurrentEnv

func CurrentEnv(exePath string) Env

CurrentEnv builds an Env from the live process environment.

type ManualOptions

type ManualOptions struct {
	Client  *Client
	Release *Release
	Target  string
	GOOS    string
	GOARCH  string
}

ManualOptions carries everything InstallManual needs. Target is the resolved (symlink-free) path of the running binary.

type Method

type Method string

Method is how the running binary was installed. It decides whether `upgrade` delegates to a package manager or replaces the file itself (ADR-0034 §D1).

const (
	MethodHomebrew  Method = "homebrew"
	MethodScoop     Method = "scoop"
	MethodGoInstall Method = "go-install"
	MethodManual    Method = "manual"
)

func Detect

func Detect(env Env) Method

Detect classifies the installation, most specific marker first. Anything unrecognized is MethodManual — including distro packages we do not publish (nix, AUR, apt). That is safe by construction: those live in read-only or root-owned locations, so the write-permission gate aborts before a single byte is downloaded.

type Release

type Release struct {
	TagName string  `json:"tag_name"`
	HTMLURL string  `json:"html_url"`
	Assets  []Asset `json:"assets"`
}

Release is the subset of the GitHub release payload we use.

func (*Release) AssetByName

func (r *Release) AssetByName(name string) (Asset, bool)

AssetByName finds an attached file by its exact name.

type Version

type Version struct {
	Major int
	Minor int
	Patch int
	Pre   string // "" for a release; "rc.1" for v1.0.0-rc.1
}

Version is a parsed semantic version. Build metadata (+meta) is not modelled: CommitBrief never tags with it, and semver says it is ignored for precedence anyway.

func ParseVersion

func ParseVersion(s string) (Version, bool)

ParseVersion accepts "v1.2.3", "1.2.3" and "v1.2.3-rc.1". It returns ok=false for anything else — most importantly the "dev" placeholder a locally built binary carries, which the caller turns into a "this is a development build" message rather than a bogus comparison.

func (Version) Compare

func (v Version) Compare(o Version) int

Compare returns -1, 0 or +1 as v sorts before, equal to, or after o, following semver precedence: numeric core first, then a release outranking any prerelease of the same core.

func (Version) String

func (v Version) String() string

String renders the version back in tag form (always v-prefixed).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL