Documentation
¶
Overview ¶
Package selfupdate lets a Go CLI update its own binary in place.
The problem it solves is not the download — that part is easy — but the decision of whether a swap is safe at all. A binary installed by Homebrew, Scoop, or WinGet is owned by that manager's bookkeeping; overwriting it out from under the manager leaves the manager's records pointing at a version that no longer matches the file on disk, and the next "brew upgrade" fights the CLI's own write. So the package classifies how the running binary got where it is before it does anything else: a managed install is redirected to the manager's own upgrade command by default, or can explicitly delegate structured argv to that manager without directly touching its binary; a manual install (a release archive someone unpacked, or a `go install` target) is eligible for replacement, and anything the package cannot confidently place in either bucket is treated as manual-adjacent risk and refused — ambiguity never resolves to "safe to overwrite".
The second problem is that the update code path is the one place a bug leaves the user with no working tool: a partially written executable cannot re-run itself to recover. Every write the package performs is therefore staged to a temporary file on the same filesystem as the target and moved into place with a single atomic rename, and every step that can fail — release lookup, download, checksum mismatch, staging, permission — fails before that rename, leaving the previous binary exactly as it was.
Identity stays with the caller ¶
Everything specific to one CLI — its binary name, GitHub repository, current version, which strings mean "this build cannot say its version", the managers that might own its install and their upgrade commands, the asset/checksum naming convention, the version-probe arguments, and which platforms it publishes — is supplied through Config. The package hard-codes none of it, which is what lets two CLIs with incompatible exit-code conventions both build a working self-update command from the same Config shape (see the cobracmd subpackage).
What the core does not do ¶
Config.Update and Config.Check themselves never print to a terminal, read from stdin, or decide a process exit code. Confirmation and executable manager commands are caller-supplied callbacks (Options.Confirm and Options.RunManaged); process I/O, output formatting, and exit-code mapping belong to the caller or to the optional cobracmd adapter. This makes the package usable by a CLI with any output convention, and what makes its own test suite able to exercise every path without a network connection or a real installed binary.
Typical use ¶
A CLI builds one Config describing itself, then either calls Config.Check for a read-only availability report, or Config.Update to perform (or plan, via Options.DryRun) the replacement. The cobracmd subpackage wraps both behind a ready-made Cobra command for CLIs that use that framework; the root package has no dependency on it, so a CLI built on any other command framework — or none — can call Config.Update directly.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CompareVersions ¶
CompareVersions orders two semver-ish version strings, returning -1 if a < b, 0 if they are equal, and +1 if a > b. A leading "v" is ignored. Comparison is by numeric major/minor/patch; a suffix after the first "-" (a prerelease, or a Go pseudo-version's "0.yyyymmddhhmmss-abcdef123456") sorts below the same core version without one, per semver — which is what makes a Go pseudo-version compare as "below its eventual release" rather than as an undetermined or unrelated value (REQ: undetermined-version). This is a minimal comparison sufficient for the self-update downgrade guard and stable-release ordering, not a full semver implementation (it does not, for instance, order multi-field prerelease identifiers numerically per the full semver spec).
Types ¶
type Action ¶
type Action int
Action is what Update actually did (or, for a dry run, would do).
const ( // ActionRedirected means a managed install was detected; nothing was // downloaded, written, or replaced. ActionRedirected Action = iota // ActionAlreadyCurrent means the running version already equals the // latest stable release; nothing was downloaded or replaced. ActionAlreadyCurrent // ActionUpdated means the binary was downloaded, verified, and swapped. ActionUpdated // ActionAborted means Options.Confirm was called and declined (returned // false, nil) — the caller chose not to proceed, as opposed to Update // refusing on its own account. Nothing was downloaded or replaced. ActionAborted // ActionPlanned means Options.DryRun was set and Update stopped before // any manager process, download, or write. PlannedCommand names a // managed operation; PlannedURL names a manual-install asset // (REQ: dry-run). ActionPlanned // ActionManagerExecuted means the configured package-manager command // exited successfully. The manager remains the install authority; the // core did not download or replace the executable itself. ActionManagerExecuted )
type CheckResult ¶
CheckResult captures the comparison between the running build and the latest stable release. Current and Latest are both normalized (no leading "v") except when Verdict is Undetermined, in which case Current is reported exactly as configured (e.g. "dev") since it is not a version at all.
type Config ¶
type Config struct {
// BinaryName is the executable name inside the release archive and the
// first component of the default asset/checksums naming.
BinaryName string
// Repository is "owner/repo" on GitHub, e.g. "sneat-dev/wb".
Repository string
// CurrentVersion is the running build's own version string, typically
// stamped at link time. See UndeterminedVersions for builds that can't
// know this (e.g. a local `go build`).
CurrentVersion string
// UndeterminedVersions lists the CurrentVersion values that mean "this
// build cannot say its version" (e.g. {"dev"} or {"unknown"}). Such a
// version is reported Undetermined rather than UpToDate or
// UpdateAvailable, and disables the pinned-downgrade guard, because
// direction can't be established without a real version to compare from
// (REQ: undetermined-version). Defaults to {"dev"} when empty — a Go
// pseudo-version is never in this set implicitly; it orders below its
// release like any other known version.
UndeterminedVersions []string
// Managers are the package managers that might own this binary's
// install, checked in order by Classify. Empty means the CLI is never
// distributed through a package manager the caller wants recognized —
// every install classifies as Manual or Ambiguous.
Managers []Manager
// SupportedPlatforms restricts self-replace to the listed GOOS/GOARCH
// pairs (REQ: unsupported-platform). Empty means all platforms the host
// Go toolchain runs on are assumed supported.
SupportedPlatforms []Platform
// TagPrefix selects which releases belong to this binary when one
// repository publishes several products, e.g. "cli-" for tags like
// "cli-v1.2.3". Empty means every release belongs to this binary (the
// single-product default).
TagPrefix string
// VersionProbeArgs are the arguments run against the newly installed
// binary to confirm it reports the expected version after a swap
// (REQ: post-swap-version-check). Defaults to {"--version"}.
VersionProbeArgs []string
// AssetName names the release archive for one binary/version/platform
// combination. Defaults to GoReleaser's own convention:
// "<binary>_<version>_<os>_<arch>.tar.gz" (".zip" on windows), with a
// leading "v" in version stripped.
AssetName func(binary, version, goos, goarch string) string
// ChecksumsName names the release's checksums file for one version.
// Defaults to GoReleaser's "<binary>_<version>_checksums.txt", with a
// leading "v" in version stripped.
ChecksumsName func(binary, version string) string
// ReleasesAPIURL is the GitHub REST endpoint listing this repository's
// releases, newest first. Defaults to
// "https://api.github.com/repos/<Repository>/releases". Overriding it is
// how this package's own tests point at an httptest.Server instead of
// the real GitHub API.
ReleasesAPIURL string
// DownloadURL builds the download URL for one asset (or the checksums
// file, which is downloaded the same way) within a specific release.
// Defaults to that release's OWN GitHub Releases URL —
// "https://github.com/<repository>/releases/download/<tag>/<asset>" —
// never the "/releases/latest/download/" alias, which would silently
// fetch whatever is currently latest instead of the pinned release
// (REQ: pinned-exact-tag).
DownloadURL func(repository, tag, asset string) string
// HTTPClient is used for every GitHub request. Defaults to
// http.DefaultClient.
HTTPClient *http.Client
}
Config carries everything one CLI has already decided about itself: its identity, the managers that might own its install, and the naming/network rules for its own releases (REQ: consumer-configured-identity). Nothing in this package hard-codes any one CLI's values — that's what lets two consumers with incompatible exit-code conventions, manager sets, and version placeholders both build a working self-update command from the same Config shape (see the cobracmd subpackage and AC: two-cli-contracts- coexist).
Every method on Config takes it by value and never mutates the caller's copy; the optional func/string fields left zero fall back to the GoReleaser-shaped defaults documented on each field.
func (Config) Check ¶
func (c Config) Check(ctx context.Context) (CheckResult, error)
Check reports whether a newer stable release is available without downloading or writing anything — the read-only counterpart to Update, usable on its own (a "--check" flag) or before deciding whether to call Update at all.
func (Config) DetectSelf ¶
DetectSelf resolves the running executable's path, following symlinks first — a Homebrew cask shim is typically a symlink into the Caskroom, and classifying the symlink itself instead of its target would miss the managed install entirely (REQ: detect-managed) — and classifies the result against c.Managers. When symlink resolution fails (the target doesn't exist, a permission error, or similar), classification falls back to the unresolved path rather than failing the whole call: a path that can't be resolved is still worth classifying as-is.
func (Config) Update ¶
Update resolves the target release (the latest stable release, or an exact pin), and — unless the install is managed, the platform is unsupported, the downgrade guard refuses, Options.DryRun stops it first, or Options.Confirm declines — downloads, verifies, and atomically swaps the running binary.
Every return, error or not, carries the Detection so a caller can build its own message without a second DetectSelf call.
type Detection ¶
type Detection struct {
// Method is how the binary was installed.
Method InstallMethod
// Manager identifies the owning package manager when Method is Managed;
// nil for Manual and Ambiguous.
Manager *Manager
// Path is the resolved path that was classified — after following
// symlinks when DetectSelf performed the resolution, or exactly the
// input path when Classify was called directly (as the reference CLI's
// --explain-path does).
Path string
}
Detection is the result of classifying an executable path.
func Classify ¶
Classify decides the install method purely from path, checking it against each manager's PathMarkers in order and returning the first match. It is case-insensitive and treats both '/' and '\' as path separators, so a Windows path (e.g. a Scoop or WinGet layout) can be classified on any host — which is what lets this package's own tests, and a consumer's --explain-path-style tooling, exercise every manager without running on that manager's platform.
When no manager matches, a path ending in a `bin` directory, or containing a `go/bin` segment (a `go install` target under GOBIN or GOPATH/bin), is classified Manual. Anything else is Ambiguous: per REQ: ambiguous-safe- default, an unrecognized location never resolves to Manual, because that would make self-replace eligible for a binary the package cannot actually place.
type Failure ¶
type Failure struct {
Kind FailureKind
// Path is the executable path involved, when applicable (set for
// KindPermission and KindAmbiguous). Empty otherwise.
Path string
// Err is the underlying error, always non-nil.
Err error
}
Failure is the error type every failure path from Config.Update and Config.Check returns. It carries a typed Kind a caller can switch on without string-matching, plus the executable Path when the failure is path-specific (REQ: permission-failure-identifiable) and the underlying error for logging or wrapping.
type FailureKind ¶
type FailureKind int
FailureKind is a machine-checkable classification of why Update or Check failed. REQ: host-owned-exit-codes exists precisely so each consumer can switch on this and map it onto its own exit codes — including two consumers that disagree about what a given situation should cost, which this type does not adjudicate.
const ( // KindAmbiguous means the install method could not be classified. KindAmbiguous FailureKind = iota // KindReleaseLookup means the GitHub releases listing could not be // fetched or decoded (network error, rate limit, malformed response). KindReleaseLookup // KindDownload means fetching a release asset or its checksums file // failed for a reason other than the asset simply not existing (that // case is KindUnknownTag). KindDownload // KindChecksum means the downloaded asset's sha256 did not match the // release's checksums file, or no checksum entry could be found for it. // This always occurs before extraction (REQ: checksum-before-extract). KindChecksum // KindPermission means the replacement failed because the process // lacks permission to write the install location. Path is always set. KindPermission // KindNonInteractive means a self-replace needed confirmation, the // caller did not skip it, and no interactive terminal was available to // ask (REQ: non-interactive-refusal). The core package never produces // this itself — it is intended for an Options.Confirm implementation // (typically the cobracmd adapter) to return, so the typed kind still // reaches the caller through the normal Update error path. KindNonInteractive // KindDowngrade means a pinned target was strictly older than the // running version and AllowDowngrade was not set. KindDowngrade // KindUnknownTag means a pinned version matched no published release, or // the matched release has no asset for the host platform. KindUnknownTag // KindUnsupportedPlatform means the host GOOS/GOARCH is not in // Config.SupportedPlatforms. KindUnsupportedPlatform // KindUnexpected is anything else: a staging/rename failure that isn't a // permission error, a failure resolving the running executable's own // path, or an error returned from an Options.Confirm callback that // wasn't already a *Failure. KindUnexpected // KindManagedVersion means a version pin was requested for an // executable package-manager update, which cannot promise an arbitrary // historical release. KindManagedVersion // KindManagedCommand means the executable manager runner or its required // configuration failed. The underlying process error remains unwrap-able. KindManagedCommand )
func KindOf ¶
func KindOf(err error) FailureKind
KindOf returns err's FailureKind when err is (or wraps) a *Failure, and KindUnexpected otherwise — including when err is nil, so a caller does not need a separate nil check before branching on the kind of a definitely- non-nil error.
func (FailureKind) String ¶
func (k FailureKind) String() string
String renders the kind as a stable, lower_snake_case token suitable for machine-readable output.
type InstallMethod ¶
type InstallMethod int
InstallMethod classifies how the running binary reached its current location, which is the single fact that decides whether self-replace is ever attempted.
const ( // Managed means a package manager owns the binary; the package redirects // to that manager's upgrade command and never writes to the file. This // is the zero value, so a Detection nobody explicitly classified reads // as the most restrictive, never-self-replace case rather than silently // looking like an eligible Manual install. Managed InstallMethod = iota // Manual means the binary was placed by the user or by `go install` — a // release archive extracted by hand, or a GOBIN/GOPATH/bin target. // Self-replace is eligible. Manual // Ambiguous means the path matched neither a configured manager's layout // nor a plausible manual location. Per REQ: ambiguous-safe-default, // Ambiguous is a distinct outcome from Manual, not a fallback that // resolves to it — an unrecognized path is never treated as eligible for // self-replace. Ambiguous )
func (InstallMethod) String ¶ added in v0.2.0
func (m InstallMethod) String() string
String renders the install method as a stable, lower_snake_case token suitable for machine-readable output, matching the convention Action and Verdict already follow.
type ManagedBinaryVerifier ¶ added in v0.6.0
ManagedBinaryVerifier probes the CLI after a successful package-manager command. A failure becomes Outcome.PostSwapWarning because the manager command has already completed.
type ManagedCommandRunner ¶ added in v0.6.0
ManagedCommandRunner executes a configured package-manager program and argv. The core deliberately owns no process I/O; command adapters provide a runner that wires stdin/stdout/stderr according to their own output contract.
type Manager ¶
type Manager struct {
// Name is shown to the user, e.g. "Homebrew".
Name string
// UpgradeCommand is the exact command printed for the user to run,
// e.g. "brew upgrade --cask wb". It is display-only and is never parsed
// or passed to a shell.
UpgradeCommand string
// UpgradeExecutable is the program invoked for an executable managed
// update. Empty keeps this manager redirect-only for backward
// compatibility. Configure it through WithExecutableUpgrade so its argv
// is copied rather than aliased.
UpgradeExecutable string
// UpgradeArgs are passed directly to UpgradeExecutable without shell
// parsing or interpolation.
UpgradeArgs []string
// PathMarkers are lowercased, '/'-separated substrings; a resolved
// executable path containing any one of them classifies as this
// manager's install.
PathMarkers []string
}
Manager describes one package manager that might own the running binary's install. PathMarkers are lowercased, '/'-separated substrings of a resolved executable path that identify that manager's install layout — Classify normalizes both the candidate path and these markers the same way (lowercase, backslashes folded to forward slashes) so a Windows path can be classified on any host, including in tests.
Consumers are not limited to Homebrew, Scoop, and WinGet: any manager can be described by constructing a Manager literal directly with its own Name, UpgradeCommand, and PathMarkers. A manager remains redirect-only unless WithExecutableUpgrade explicitly configures structured argv; the display command is never parsed or passed to a shell. The three constructors below exist because those three account for effectively every managed Go CLI install in the wild, and getting their marker sets right (see Homebrew's doc comment for the Intel-cask gotcha) is exactly the kind of detail this package exists to get right once instead of per consumer.
func Homebrew ¶
Homebrew describes a Homebrew-managed install (macOS, Linux, or Linuxbrew), covering both Formula and Cask installs.
The marker set has one non-obvious entry: a GoReleaser homebrew_casks install resolves, through the symlink Homebrew creates, into a Caskroom path. On Apple Silicon that path already contains "/homebrew/" (it lives under /opt/homebrew/Caskroom/...) so the Cellar/Homebrew markers alone would catch it, but on Intel it is /usr/local/Caskroom/..., which matches none of the other markers — "/caskroom/" is required specifically so an Intel cask install classifies as managed instead of falling through to ambiguous.
func Scoop ¶
Scoop describes a Scoop-managed install (Windows). Both the versioned "apps" directory and the "shims" directory Scoop puts on PATH are markers, because either one may be the resolved, symlink-followed path depending on how the binary was invoked.
func WinGet ¶
WinGet describes a WinGet-managed install (Windows Package Manager), under the user's local Microsoft\WinGet packages or links directory.
func (Manager) CanExecuteUpgrade ¶ added in v0.6.0
CanExecuteUpgrade reports whether the consumer explicitly opted this manager into executable updates. A display-only UpgradeCommand is never sufficient.
func (Manager) WithExecutableUpgrade ¶ added in v0.6.0
WithExecutableUpgrade opts this manager into executable updates. executable and args are passed directly to the consumer-supplied ManagedCommandRunner; UpgradeCommand remains the independently configured human-readable form. The argument slice is copied so later caller mutations cannot change the command that will run.
type Options ¶
type Options struct {
// PinnedVersion, when non-empty, installs exactly that release instead
// of the latest stable one (REQ: version-pin). A leading "v" is
// optional.
PinnedVersion string
// AllowDowngrade permits a PinnedVersion that orders below the running
// version (REQ: pinned-downgrade-guard). Ignored when PinnedVersion is
// empty, and ignored when the running version is undetermined (there is
// no direction to guard).
AllowDowngrade bool
// DryRun walks the full decision path and stops before any download or
// write (REQ: dry-run). See ActionPlanned.
DryRun bool
// Confirm, when non-nil, is called with a human-readable description of
// the version transition (e.g. "1.0.0 → 1.1.0", or "downgrade: 1.1.0 →
// 1.0.0") before any download begins, and must return whether to
// proceed. This is the ONLY place Update touches anything resembling
// user interaction, and it does none of the interaction itself
// (REQ: no-io-side-effects-in-core) — prompting, or deciding to skip the
// prompt because a --yes flag was given, or refusing because no
// terminal is attached (REQ: non-interactive-refusal), all belong to
// Confirm's implementation. A refusal like the non-interactive one is
// reported by returning a *Failure (e.g. {Kind: KindNonInteractive})
// as the error, which Update passes straight through; returning
// (false, nil) instead means "the user was asked and said no", which
// Update reports as ActionAborted with a nil error, not a failure.
// Nil means no confirmation gate at all — Update proceeds immediately.
Confirm func(transition string) (bool, error)
// RunManaged is required when the detected Manager opted into executable
// upgrades. It receives structured argv, never a shell command string.
RunManaged ManagedCommandRunner
// VerifyManaged is required alongside RunManaged and probes the CLI found
// after the manager command using Config.VersionProbeArgs.
VerifyManaged ManagedBinaryVerifier
}
Options controls one Update call. For a manual install, the zero value (no pin, no downgrade allowance, DryRun false, Confirm nil) updates unconditionally to the latest stable release with no confirmation gate. An executable managed install additionally requires RunManaged and VerifyManaged; see Confirm's doc for the interactive case.
type Outcome ¶
type Outcome struct {
// Action is what happened.
Action Action
// Detection is how the running binary's install was classified.
Detection Detection
// Result is the version comparison that led to Action, when one was
// performed.
Result CheckResult
// Target is the normalized version that was (or would be, or was
// declined to be) installed.
Target string
// Downgrade is true when Target orders below the running version, i.e.
// this was a downgrade (only possible via a pinned Options.
// PinnedVersion with AllowDowngrade set).
Downgrade bool
// PlannedURL is the exact asset URL a non-dry-run call would have
// fetched for a manual install. Set only when Action is ActionPlanned.
PlannedURL string
// PlannedCommand is the exact display command an executable manager
// would run. Set for a managed ActionPlanned outcome.
PlannedCommand string
// PostSwapWarning is set when Action is ActionUpdated and the post-swap
// version probe did not confirm the expected version, or when
// ActionManagerExecuted and the installed CLI could not be probed after
// the manager command completed. The mutation already succeeded — this
// is a warning to surface, not a failed Update.
PostSwapWarning error
}
Outcome describes what Update did. Result and Target are only meaningful for the actions that actually compared or resolved a version (ActionAlreadyCurrent, ActionUpdated, and manual ActionAborted/ ActionPlanned); for managed outcomes and every error return, they are left at their zero value and the caller should look at Detection.Manager instead.
type Platform ¶
Platform identifies one OS/architecture pair a consumer publishes release assets for. An empty Config.SupportedPlatforms means "every platform the host Go toolchain runs on" — most CLIs that don't cross-compile narrowly don't need to populate this at all.
type Verdict ¶
type Verdict int
Verdict is the outcome of comparing the running build against the latest stable release.
const ( // UpToDate means the current version equals the latest stable release. UpToDate Verdict = iota // UpdateAvailable means a newer stable release exists. UpdateAvailable // Undetermined means the current version is one of Config's // UndeterminedVersions (e.g. an unstamped local build) and so cannot be // meaningfully compared at all — it is reported as neither up to date // nor available, per REQ: undetermined-version. Undetermined )
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cliui holds the framework-neutral parts of a self-update CLI's user interaction: the confirmation prompt (REQ: non-interactive-refusal), the terminal check it relies on, and the text/JSON writers for selfupdate.Outcome and selfupdate.CheckResult.
|
Package cliui holds the framework-neutral parts of a self-update CLI's user interaction: the confirmation prompt (REQ: non-interactive-refusal), the terminal check it relies on, and the text/JSON writers for selfupdate.Outcome and selfupdate.CheckResult. |
|
cmd
|
|
|
selfupdate
command
Command selfupdate is the reference consumer of github.com/strongo/ selfupdate: it exists so the package's own release path is genuinely exercised — something has to actually download, verify, and swap a real executable, and it should be this repository's own binary rather than a downstream CLI's users finding a bug first (REQ: reference-cli-single- command).
|
Command selfupdate is the reference consumer of github.com/strongo/ selfupdate: it exists so the package's own release path is genuinely exercised — something has to actually download, verify, and swap a real executable, and it should be this repository's own binary rather than a downstream CLI's users finding a bug first (REQ: reference-cli-single- command). |
|
Package cobracmd builds a ready-made self-update Cobra command from a selfupdate.Config.
|
Package cobracmd builds a ready-made self-update Cobra command from a selfupdate.Config. |