selfupdate

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 20 Imported by: 0

README

selfupdate

github.com/strongo/selfupdate lets a Go CLI update its own binary in place — safely. It decides how the running binary was installed before it touches anything: a package-manager-owned install (Homebrew, Scoop, WinGet) is never overwritten directly. It is redirected to that manager's own upgrade command by default, or can explicitly delegate to structured manager argv; a manual install (a release archive someone unpacked, or a go install target) is downloaded, sha256-verified against the release's own checksums, and swapped in atomically. Everything specific to one CLI — its identity, its managers, its naming conventions, its exit codes — is supplied by the caller. Nothing here is hard-coded to any one consumer.

See spec/features/self-update/README.md for the full behavioral contract this package implements, and cmd/selfupdate/ for a complete, runnable consumer (this module's own reference CLI, which updates itself from this repository's GitHub releases using nothing but the public API below).

Safety guarantees

  • A managed install is never overwritten directly. Classify resolves symlinks first (a Homebrew cask shim usually is one) and checks the result against each configured Manager's path markers. A match routes to ActionRedirected unless the consumer explicitly configured an executable and argv. Executable mode confirms and invokes the manager without a shell; it still never downloads or writes the managed binary itself, and it refuses release pins the manager cannot guarantee.
  • An unrecognized install is never treated as safe to overwrite. A path that matches neither a manager nor a plausible manual location (go/bin, or directly inside a bin directory) is Ambiguous, not Manual. Ambiguity fails closed.
  • The checksum is verified before a single byte is extracted. The downloaded archive's sha256 is compared against that release's own checksums file first; extraction only happens on a match. A mismatch, or a missing checksum entry, aborts with nothing written.
  • The replace is atomic. The verified binary is staged to a temp file in the same directory as the target (same filesystem) and moved into place with a single rename. On POSIX that's one atomic rename(2); on Windows, where a running .exe can't be overwritten, the current target is renamed aside first and restored if the final move fails.
  • Every failure leaves a working binary. Release lookup, download, checksum, staging, and permission failures all return before any write to the install location. There is no failure mode that ends with a partial or missing executable where the old one used to be.
  • A pin fetches that release's own assets, never "latest." The download URL is built from the release's own tag (.../releases/download/<tag>/<asset>), not the /releases/latest/ alias — an older pinned release can't accidentally resolve to whatever is currently newest.

Install

go get github.com/strongo/selfupdate

Wiring example

A minimal CLI wires one Config and builds a Cobra command from it:

package cli

import (
	"github.com/spf13/cobra"
	"github.com/strongo/selfupdate"
	"github.com/strongo/selfupdate/cobracmd"
)

// version is stamped at link time, e.g. -ldflags "-X your/module.version=v1.2.3".
var version = "dev"

func newSelfUpdateCommand() *cobra.Command {
	cfg := selfupdate.Config{
		BinaryName:     "wb",
		Repository:     "sneat-dev/wb",
		CurrentVersion: version,
		// "dev" is the default undetermined placeholder; only set this when
		// a different one is needed, e.g. a Homebrew-formula build reports
		// "unknown" instead.
		UndeterminedVersions: []string{"unknown"},
		Managers: []selfupdate.Manager{
			selfupdate.Homebrew("brew upgrade --cask wb").
				WithExecutableUpgrade("brew", "upgrade", "--cask", "wb"),
		},
		SupportedPlatforms: []selfupdate.Platform{
			{GOOS: "darwin", GOARCH: "amd64"},
			{GOOS: "darwin", GOARCH: "arm64"},
			{GOOS: "linux", GOARCH: "amd64"},
			{GOOS: "linux", GOARCH: "arm64"},
		},
		VersionProbeArgs: []string{"version", "--json"},
		// AssetName, ChecksumsName, ReleasesAPIURL, DownloadURL, and
		// HTTPClient all default to GoReleaser-shaped conventions against
		// the real GitHub API — set them only to deviate, or (in tests) to
		// point at an httptest.Server.
	}

	return cobracmd.New(cfg, cobracmd.CommandOptions{
		Aliases:    []string{"update"},
		Errors:     wbErrors{}, // maps *selfupdate.Failure onto wb's own exit codes
		JSONFormat: true,
	})
}

// wbErrors implements cobracmd.ErrorMapper for wb's own three-code exit
// contract (0/1/2).
type wbErrors struct{}

func (wbErrors) Failure(err error) error {
	code := 1
	if selfupdate.KindOf(err) == selfupdate.KindPermission {
		code = 2
	}
	return exitError{code: code, err: err}
}

func (wbErrors) UpdateAvailable(res selfupdate.CheckResult) error {
	return exitError{code: 1, err: nil} // folded into wb's general findings code
}

A CLI that doesn't use Cobra calls cfg.Check(ctx) and cfg.Update(ctx, opts) directly — cobracmd is optional sugar over the same two calls; the root package has no command-framework dependency at all. It doesn't have to be hand-rolled from scratch either: github.com/strongo/selfupdate/cliui holds the same confirmation prompt, non-interactive refusal, and text/JSON writers cobracmd itself is built from, with no Cobra (or any other framework) dependency:

package cli

import (
	"context"
	"os"

	"github.com/strongo/selfupdate"
	"github.com/strongo/selfupdate/cliui"
)

func selfUpdate(ctx context.Context, cfg selfupdate.Config, yes bool) error {
	confirm := cliui.Confirm(cliui.ConfirmOptions{
		In:  os.Stdin,
		Out: os.Stdout,
		Yes: yes, // wire from your own --yes/-y flag; nil Interactive -> cliui.IsTerminal
	})

	outcome, err := cfg.Update(ctx, selfupdate.Options{Confirm: confirm})
	if err != nil {
		if selfupdate.KindOf(err) == selfupdate.KindAmbiguous {
			cliui.WriteAmbiguousGuidance(os.Stdout, cfg)
		}
		return err // map to your own exit code however you already do
	}
	cliui.WriteOutcome(os.Stdout, os.Stderr, cfg, outcome)
	return nil
}

cobracmd and cliui implement the exact same behavior — the former is just the Cobra flag/wiring layer on top of the latter — so a Cobra CLI and a hand-rolled one built from cliui directly print byte-identical output for the same Outcome/CheckResult.

Why exit codes and output belong to the host, not this package

Two real consumers of this exact package disagree about what "an update is available" should cost: one reserves a dedicated exit code for it, one folds it into a general findings code alongside everything else. Neither is wrong — it's a property of each CLI's own contract with its scripts and users, not of the update logic. So Config.Check and Config.Update never decide a process exit code and never touch a terminal; they return typed outcomes (Verdict, Action, FailureKind) a caller switches on, and cobracmd's ErrorMapper is exactly the seam where each consumer's own convention plugs in. The alternative — baking one CLI's exit-code opinions into the shared package — is what made the pre-package version of this logic unshippable as a library in the first place: it worked for exactly one CLI.

Dry runs

Options.DryRun walks the entire decision path — detection, target resolution (latest or a pin), the downgrade guard — and stops just before the download would start, returning ActionPlanned with the exact asset URL a real run would fetch (Outcome.PlannedURL). cobracmd exposes this as --dry-run. It's the way to verify a CLI's own wiring — managers, asset naming, platform list — without ever replacing a binary.

Testing your own wiring

Nothing in this package touches the network or the filesystem beyond what a real Update call requires, and every GitHub endpoint, filesystem operation, and TTY check it makes is overridable — see Config.ReleasesAPIURL/ DownloadURL/HTTPClient for pointing at an httptest.Server, and cobracmd.CommandOptions.Interactive for driving the confirmation prompt without a real terminal. The package's own test suite (this repo) exercises every FailureKind, every Manager, and both exit-code-contract shapes this way — see *_test.go for the pattern.

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

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
)

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

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

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

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

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

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
	// 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 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 added in v0.6.0

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

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.

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

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
)

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

Jump to

Keyboard shortcuts

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