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
- Variables
- func AssetName(version, goos, goarch string) string
- func BinaryEntryName(goos string) string
- func CleanupStale(target string)
- func Command(m Method) []string
- func InstallManual(ctx context.Context, o ManualOptions) error
- func ParseChecksums(data []byte) map[string]string
- func PreflightWritable(target string) error
- func ResolveExe() (string, error)
- func Run(ctx context.Context, argv []string, out, errOut io.Writer) error
- func SamePath(a, b, goos string) bool
- type Asset
- type Client
- type Env
- type ManualOptions
- type Method
- type Release
- type Version
Constants ¶
const ChecksumsFile = "checksums.txt"
ChecksumsFile is the name goreleaser gives the checksum manifest attached to every release (.goreleaser.yaml → checksum.name_template).
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.
const ModulePath = "github.com/CommitBrief/commitbrief/cmd/commitbrief"
ModulePath is the `go install` target for CommitBrief.
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 ¶
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") )
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") )
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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).
func Detect ¶
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.
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 ¶
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.