selfupdate

package
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 21 Imported by: 0

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, executable manager commands, and optional post-update integrations are caller-supplied callbacks (Options.Confirm, Options.RunManaged, and Options.AfterUpdate); 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

func CompareVersions(a, b string) int

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
	// ActionAhead means the running version is known and orders strictly
	// above the latest stable release (REQ: ahead-of-latest): nothing was
	// downloaded, written, replaced, or — for a managed install — redirected
	// or run through a manager command. Appended after the existing values
	// so no consumer's existing switch on this type changes meaning (see
	// TestAction_ExistingValuesPinned).
	ActionAhead
)

func (Action) String

func (a Action) String() string

String renders the action as a stable, lower_snake_case token suitable for machine-readable output.

type AfterUpdate

type AfterUpdate struct {
	Outcome    Outcome
	Executable ExecutableIdentity
}

AfterUpdate is supplied to Options.AfterUpdate after an update reaches a successful terminal outcome. Outcome is the completed update receipt and Executable identifies the installed binary that an integration may reexec.

type AfterUpdateFunc

type AfterUpdateFunc func(ctx context.Context, update AfterUpdate) error

AfterUpdateFunc runs after a successful self-update outcome. Its error is retained as Outcome.AfterUpdateWarning because the binary update is already complete and must not be reported as failed.

type Availability added in v0.8.0

type Availability struct {
	Result    CheckResult
	Target    string
	Pinned    bool
	Detection Detection
	Warning   error
}

Availability is the structured version information reported before an update confirmation or package-manager command. Pinned is true when Target names the requested release rather than the latest stable release. Warning means the managed-release lookup was unavailable; Result.Current remains useful, but Result.Latest is empty and must not be presented as a version.

type AvailabilityReporter added in v0.8.0

type AvailabilityReporter func(Availability)

AvailabilityReporter receives version information before any confirmation or mutation. It is a callback so the core remains framework- and I/O-free.

type CheckResult

type CheckResult struct {
	Current string
	Latest  string
	Verdict Verdict
}

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

func (c Config) DetectSelf() (Detection, error)

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) InstallNew added in v0.14.0

func (c Config) InstallNew(ctx context.Context, destPath string, plan InstallPlan) (InstallResult, error)

InstallNew downloads, verifies and places plan's release at destPath, using the identical download, checksum, and extraction code self-replace uses (REQ: direct-release-install, REQ: download-matching-asset, REQ: checksum-before-extract), and places the verified binary at destPath with a no-replace operation (REQ: install-never-overwrites): a file already at destPath — including one that appears after a caller decided on destPath and before this call finishes placing it — is never overwritten, and a failed install leaves neither a partial destPath nor a staging file behind.

plan MUST come from a prior call to c.PlanInstall — InstallNew performs no release lookup of its own and installs exactly plan.Tag, never "whatever is latest now" (REQ: install-destination-follows-policy's planning is a separate, one-time step from execution). A zero plan (an empty Tag) is a caller error, reported as KindUnexpected rather than silently resolving a tag InstallNew was never asked to resolve.

Unlike Update, InstallNew never accepts a version pin (REQ: direct-release-install: "pinning a target version is not offered") and never resolves or replaces the running binary's own path: destPath is entirely the caller's choice. Choosing, creating, and validating that destination — the per-user bin directory, the destination denylist, the host's own executable directory — is the cliinstall package's job, a different, higher-level consumer of this same Config; InstallNew itself applies no policy to destPath and never creates its parent directory, so a missing directory surfaces as an ordinary staging failure rather than being silently created.

A permission failure is reported as KindPermission and, like the rest of this package, carries Path (REQ: permission-failure-identifiable); a file already at destPath is reported as KindDestinationExists.

func (Config) LatestRelease added in v0.18.0

func (c Config) LatestRelease(ctx context.Context) (string, error)

LatestRelease returns the tag of the newest stable release this Config's unpinned path would resolve to (REQ: latest-release-source). It is the lookup the unpinned Update/UpdateAt path uses internally, exposed so a caller — the cliinstall upgrade command is the motivating case — can resolve a target once, show and confirm it, and then pass it back through Options.ResolvedTag rather than have UpdateAt silently pick a possibly different release on its own (REQ: update-at-classified-copy).

func (Config) PlanInstall added in v0.19.0

func (c Config) PlanInstall(ctx context.Context) (InstallPlan, error)

PlanInstall resolves this Config's latest stable release exactly as InstallNew's own lookup used to (REQ: latest-release-source, REQ: multi-product-repository, REQ: unsupported-platform), without downloading, verifying or writing anything. The returned InstallPlan is the exact release InstallNew(ctx, destPath, plan) will place when given this same plan back — resolving nothing itself.

func (Config) Update

func (c Config) Update(ctx context.Context, opts Options) (Outcome, error)

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. Update is exactly DetectSelf followed by UpdateAt (REQ: update-at-classified-copy).

Every return, error or not, carries the Detection so a caller can build its own message without a second DetectSelf call.

func (Config) UpdateAt added in v0.18.0

func (c Config) UpdateAt(ctx context.Context, detection Detection, opts Options) (Outcome, error)

UpdateAt performs the update path for a copy the caller has already classified and — for the unpinned path, via Options.ResolvedTag — whose latest release it has already resolved (REQ: update-at-classified-copy). detection.Path is the symlink-resolved file to replace; c.CurrentVersion is that copy's version, exactly as for any other Config. Update is exactly DetectSelf followed by UpdateAt, so calling UpdateAt directly on a classified non-running copy (a different installed target, or a symlinked one that is not the process currently executing) replaces that resolved path, never the calling process's own binary.

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

func Classify(path string, managers []Manager) Detection

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 ExecutableIdentity

type ExecutableIdentity struct {
	Path         string
	ResolvedPath string
}

ExecutableIdentity identifies the executable a post-update integration can invoke after a successful update. Path is the absolute invocation path; ResolvedPath is that path after symlinks are followed. Manager-owned updates resolve both only after the package-manager command completes, so they track cask or version-directory changes instead of retaining the old path.

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.

func (*Failure) Error

func (f *Failure) Error() string

Error satisfies the error interface. The Kind is deliberately not part of the message — String() exists for callers that want it, and baking it into Error() would pressure every caller into parsing a "kind: message" convention instead of using KindOf.

func (*Failure) Unwrap

func (f *Failure) Unwrap() error

Unwrap exposes the underlying error to errors.Is/errors.As, e.g. so a caller can check errors.Is(err, fs.ErrPermission) in addition to (or instead of) checking Kind.

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 cannot be proven to match the
	// latest release an executable package-manager update would install, or
	// was requested from a redirect-only manager.
	KindManagedVersion
	// KindManagedCommand means the executable manager runner or its required
	// configuration failed. The underlying process error remains unwrap-able.
	KindManagedCommand

	// KindUnknownTarget means a named install target is not a catalog id.
	// Produced by cliinstall before any confirmation, network request, or
	// write (cli-install#req-unknown-target-refused).
	KindUnknownTarget
	// KindNoInstallDir means no destination directory could be used for a
	// direct install: the per-user bin directory is not on PATH, or the only
	// available directory is refused by the destination denylist. Produced by
	// cliinstall's destination policy (cli-install#req-per-user-bin-dir,
	// cli-install#req-destination-denylist).
	KindNoInstallDir
	// KindDestinationExists means a direct install's chosen destination path
	// is already occupied by a file this package will not overwrite,
	// including one that appears after status was probed and before
	// placement. Path is always set. Produced directly by this package's own
	// InstallNew (cli-install#req-install-never-overwrites) as well as by
	// cliinstall when an unrecognized copy already occupies the destination
	// (cli-install#req-unrecognized-copy-not-trusted).
	KindDestinationExists
)

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

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 InstallPlan added in v0.19.0

type InstallPlan struct {
	// Tag is the exact published release tag InstallNew will install.
	Tag string
	// Version is Tag's normalized (no leading "v", no TagPrefix) version.
	Version string
	// AssetURL is the exact release-asset URL InstallNew will download —
	// the value details-before-install shows before any confirmation.
	AssetURL string
}

InstallPlan is the exact release a direct install will place, resolved once by PlanInstall so a caller can show the version, tag and asset URL details-before-install requires and get it confirmed, then pass that SAME plan to InstallNew — never resolving "latest" a second time.

This mirrors, for a fresh install, the "resolve once, pass the tag on" contract self-update#req:update-at-classified-copy gives an existing install's own upgrade path (Config's exposed latest-release lookup plus Options' resolved-tag field): a caller that already asked the user to confirm one version must never let a second, independent lookup install a different one.

type InstallResult added in v0.14.0

type InstallResult struct {
	// Path is the destination path the verified binary was placed at —
	// always exactly the destPath InstallNew was called with.
	Path string
	// Version is the normalized (no leading "v", no Config.TagPrefix)
	// version of the release that was installed.
	Version string
	// Tag is that release's exact published tag, which may differ from
	// Version by a TagPrefix and/or a leading "v"
	// (REQ: multi-product-repository).
	Tag string
}

InstallResult is what InstallNew placed.

type ManagedBinaryVerifier

type ManagedBinaryVerifier func(ctx context.Context, detection Detection, binary string, args []string, expectedVersion string) (ExecutableIdentity, error)

ManagedBinaryVerifier resolves and probes the CLI after a successful package-manager command. The returned identity is the exact executable that passed the probe and is reused by AfterUpdate; callers must not perform a second PATH lookup. A failure becomes Outcome.PostSwapWarning because the manager command has already completed.

type ManagedCommand added in v0.9.1

type ManagedCommand struct {
	Executable string
	Args       []string
}

ManagedCommand is one argv-safe package-manager process. Executable and Args are passed directly to ManagedCommandRunner; neither is shell parsed.

type ManagedCommandRunner

type ManagedCommandRunner func(ctx context.Context, executable string, args []string) error

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
	// UpgradeSteps are executed in order when non-empty. They allow a manager
	// whose local metadata must be refreshed before upgrade to express both
	// operations as structured argv without invoking a shell. A failed step
	// stops the sequence.
	UpgradeSteps []ManagedCommand
	// 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

func Homebrew(upgradeCommand string) Manager

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 HomebrewCask added in v0.10.0

func HomebrewCask(name string) Manager

HomebrewCask describes an executable Homebrew cask update. It refreshes Homebrew metadata before upgrading the named cask, using structured argv rather than parsing or executing the display command through a shell. Consumers should prefer this constructor when their cask is safe for self-update to execute directly.

func HomebrewFormula added in v0.10.0

func HomebrewFormula(name string) Manager

HomebrewFormula describes an executable Homebrew formula update. It uses the same ordered, argv-safe update contract as HomebrewCask.

func Scoop

func Scoop(upgradeCommand string) Manager

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

func WinGet(upgradeCommand string) Manager

WinGet describes a WinGet-managed install (Windows Package Manager), under the user's local Microsoft\WinGet packages or links directory.

func (Manager) CanExecuteUpgrade

func (m Manager) CanExecuteUpgrade() bool

CanExecuteUpgrade reports whether the consumer explicitly opted this manager into executable updates. A display-only UpgradeCommand is never sufficient.

func (Manager) WithExecutableUpgrade

func (m Manager) WithExecutableUpgrade(executable string, args ...string) Manager

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.

func (Manager) WithExecutableUpgradeSteps added in v0.9.1

func (m Manager) WithExecutableUpgradeSteps(steps ...ManagedCommand) Manager

WithExecutableUpgradeSteps opts this manager into an ordered executable update. It is intended for managers such as Homebrew where refreshing local metadata and upgrading the package are separate commands. Every argv slice is copied and empty executables are rejected by CanExecuteUpgrade.

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
	// ReportAvailability is called once after version information is resolved
	// and before confirmation or mutation. Its callback is advisory output only;
	// it cannot alter update control flow.
	ReportAvailability AvailabilityReporter
	// AfterUpdate runs only after an actual successful update, an already-current
	// result, or a successful executable package-manager update. It receives the
	// resolved installed executable identity so integrations can reexec the new
	// binary. An error becomes Outcome.AfterUpdateWarning; it never changes a
	// completed binary update into a failure.
	AfterUpdate AfterUpdateFunc
	// ResolvedTag, when set, names the release tag a caller already resolved
	// via Config.LatestRelease and had confirmed (REQ: update-at-classified-
	// copy). Neither the manual nor the managed path performs its own
	// independent latest-release search when this is set: the target is
	// this tag, not whatever the package would otherwise pick. The one
	// lookup the unpinned path always makes is still performed, but only to
	// confirm this tag is STILL the latest stable release; if a newer one
	// was published in between, the update fails with KindReleaseLookup and
	// changes nothing, so a caller that confirmed one version never installs
	// another. Ignored when PinnedVersion is set — a pin already names an
	// exact target by construction.
	ResolvedTag string
}

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
	// ReleaseCheckWarning records an advisory failure while looking up the
	// latest published release for a managed install. The package manager
	// remains the update authority, so this warning never prevents a redirect
	// or configured manager command from proceeding.
	ReleaseCheckWarning error
	// 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
	// AfterUpdateWarning is set when Options.AfterUpdate could not resolve the
	// installed executable or returned an error. The binary update has already
	// completed, so this is a warning to surface separately, never an Update
	// failure.
	AfterUpdateWarning 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

type Platform struct {
	GOOS   string
	GOARCH string
}

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
	// Ahead means the running version is known and orders strictly above the
	// latest stable release — a Go pseudo-version after the newest tag, or a
	// build from a newer version line than the releases (REQ: ahead-of-
	// latest). Appended after the existing values so no consumer's existing
	// switch on this type changes meaning (see
	// TestVerdict_ExistingValuesPinned).
	Ahead
)

func (Verdict) String

func (v Verdict) String() string

String renders v the way a consumer's machine-readable output (e.g. cobracmd's --format json) is expected to spell it: a stable, snake_case token rather than Go's default numeric %v.

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.
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.

Jump to

Keyboard shortcuts

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