Documentation
ΒΆ
Overview ΒΆ
Package selfupdate self-updates a Go CLI from its GitHub Releases, and prints the passive "a newer version is available" notice that goes with it.
The flow: ask GitHub for the latest release, compare it against the running version, download the asset for this platform, verify its SHA-256 against the release's checksums file (and, with a verify.Verifier, verify that checksums file's signature), unpack the executable, and atomically swap it in place of the running binary.
A minimal integration is three calls:
up, err := selfupdate.New(selfupdate.Config{Owner: "you", Repo: "mytool"})
if err != nil {
return err
}
selfupdate.CleanupLeftovers() // once, at startup
up.NotifyIfAvailable(os.Stderr, buildinfo.Version) // passive hint
err = up.Run(ctx, os.Stdout, buildinfo.Version, false) // the `update` subcommand
Only Owner and Repo are required; everything else is derived from them. If the binary is not named after the repository, set Config.AppName β every other default follows it. See Config for the full list, and the layout package for matching a release workflow that names its assets differently.
What it refuses to do ΒΆ
The defaults are chosen so that a hostile release, a hostile network, or a tampered cache cannot turn an update check into an install of something the project did not publish:
- Downloads are https-only to GitHub-controlled hosts, redirects included β a checksum only vouches for a download if the whole chain is authenticated.
- Release tags are validated at ingress, before they reach a terminal, a file name, or the on-disk cache.
- Size caps are errors, never truncation: a truncated binary bricks an install as thoroughly as a malicious one.
- Only a strictly newer version installs, so an attacker who controls the network can stall a user on their current version but cannot serve them an older, known-vulnerable release as an "update". The one exception is an explicit Updater.SwitchChannel, where moving back is what the user asked for.
- Dev and source builds are never updated over.
- Error messages never echo server-supplied URLs or HTTP status text.
Release channels ΒΆ
By default an updater follows Stable, which is what GitHub's /releases/latest endpoint returns: full releases, never pre-releases or drafts. Prerelease widens that to every published release and takes the highest version, so a user tracking betas still lands on the final release once it ships.
ch, err := selfupdate.ParseChannel(*channelFlag) // "", "stable", "beta", β¦
if err != nil {
return err
}
err = up.RunSwitch(ctx, os.Stdout, buildinfo.Version, ch) // `mytool update --beta`
The choice is remembered in the state file, so plain updates and the passive notice follow it afterwards. Leaving a channel may install an *older* version β see Updater.SwitchChannel for why that is confined to this one path.
Checksums alone do not protect against someone who can edit release assets; see the verify package for closing that gap with a signing key.
Index ΒΆ
- Constants
- func CleanupLeftovers()
- type Asset
- type Channel
- type Config
- type Release
- type Result
- type Updater
- func (u *Updater) Channel() Channel
- func (u *Updater) DisableEnvName() string
- func (u *Updater) Disabled(current string) bool
- func (u *Updater) LatestRelease(ctx context.Context) (*Release, error)
- func (u *Updater) NotifyIfAvailable(w io.Writer, current string)
- func (u *Updater) Refresh(current string) string
- func (u *Updater) Run(ctx context.Context, w io.Writer, current string, checkOnly bool) error
- func (u *Updater) RunSwitch(ctx context.Context, w io.Writer, current string, ch Channel) error
- func (u *Updater) SelfUpdate(ctx context.Context, current string, checkOnly bool) (*Result, error)
- func (u *Updater) SetChannel(ch Channel) error
- func (u *Updater) SwitchChannel(ctx context.Context, current string, ch Channel) (*Result, error)
Constants ΒΆ
const ( // DefaultMaxAsset caps how many bytes are accepted from a release asset β the // download and, for archive layouts, the binary inside it β so a hostile or // corrupt response can't exhaust memory. Release binaries are a few MB. DefaultMaxAsset = int64(64 << 20) // 64 MiB // DefaultMaxChecksums is far tighter: a genuine checksums file is a few // hundred bytes, so there is no reason to buffer megabytes of "checksums" a // hostile release asset serves up. DefaultMaxChecksums = int64(1 << 20) // 1 MiB // DefaultCheckInterval is how often the passive notice refreshes its cached // "latest version", so normal commands never repeatedly hit the network. DefaultCheckInterval = 24 * time.Hour // DefaultNoticeTimeout bounds the background refresh, so a slow or // unreachable GitHub can't delay the user's command by more than this. DefaultNoticeTimeout = 1500 * time.Millisecond // DefaultUpdateTimeout bounds the whole check-download-verify-install cycle. // Generous compared to the notice, since the user explicitly asked to update // and is waiting on the result rather than being interrupted by it. DefaultUpdateTimeout = 60 * time.Second )
Default sizes and timings. All are overridable through Config.
Variables ΒΆ
This section is empty.
Functions ΒΆ
func CleanupLeftovers ΒΆ
func CleanupLeftovers()
CleanupLeftovers removes the "<exe>.old" file left behind by a prior Windows self-update. Call it once at startup; it is best effort and never errors, so calling it is always safe.
Types ΒΆ
type Channel ΒΆ added in v0.2.0
type Channel string
Channel selects which published releases an updater considers.
A channel is a filter over the repository's releases, not a separate stream of them: both channels install assets from the same GitHub releases, verified the same way. The only difference is which of those releases are eligible.
const ( // Stable considers only full releases β what GitHub's /releases/latest // endpoint returns, which excludes pre-releases and drafts. This is the // default, and the only channel a tool needs unless it ships betas. Stable Channel = "stable" // Prerelease considers every published release, pre-releases included, and // takes the highest-versioned one. It is a superset of Stable rather than an // alternative to it: once 1.5.0 ships, a user on the prerelease channel moves // from 1.5.0-rc.2 to 1.5.0 like everyone else, instead of being stranded on // release candidates. Prerelease Channel = "prerelease" )
func ParseChannel ΒΆ added in v0.2.0
ParseChannel turns a user-supplied string β a `--channel` flag, a config file entry β into a Channel. An empty string is Stable, so a flag that defaults to "" needs no special case, and "beta" is accepted as the name users actually type for the prerelease channel.
The error deliberately does not echo the input; it names the valid choices instead, which is the more useful half anyway.
type Config ΒΆ
type Config struct {
// Owner is the GitHub user or organisation that owns the releases. Required.
Owner string
// Repo is the repository name β where the releases live. Required.
//
// It seeds AppName, and through it every other default, so a project whose
// binary and repository share a name needs nothing else.
Repo string
// AppName is how the tool calls itself: the binary name, the name in
// user-facing text, and the basis for every other derived default. Defaults
// to Repo.
//
// Set it whenever the binary is not named after the repository β for a repo
// "compose-check-updates" shipping a binary "ccu", AppName: "ccu" is what
// makes the asset names, checksums file, opt-out variable, state directory
// and user agent all say "ccu" instead of the repository name.
AppName string
// UpdateCmd is the command shown in the "run X to upgrade" hint. Defaults to
// "<AppName> update".
UpdateCmd string
// DisableEnv is the environment variable that switches update checking off
// when set to anything non-empty. Defaults to "<APPNAME>_NO_UPDATE_CHECK",
// with non-alphanumerics replaced by underscores.
DisableEnv string
// Channel is the release channel to follow when the user has not chosen one.
// Defaults to Stable, which is what a tool that publishes no pre-releases
// wants; set it to Prerelease only for a build that should track betas out of
// the box, such as a nightly distribution of the same tool.
//
// A choice the user makes through [Updater.SwitchChannel] or
// [Updater.SetChannel] is remembered in the state file and takes precedence
// over this.
Channel Channel
// Layout describes the release asset naming and packaging. Defaults to
// &layout.Archive{}.
Layout layout.Layout
// ChecksumsName names the checksums asset for a version. Defaults to
// "<AppName>_<version>_checksums.txt".
ChecksumsName func(version string) string
// Verifier authenticates the checksums file. Nil means checksums-only β see
// the verify package for what that does and does not protect against.
Verifier verify.Verifier
// HTTP is the client used for all requests. Nil gets one with a 30s timeout
// and an https-only redirect policy. A supplied client without a CheckRedirect
// has that policy installed on it.
HTTP *http.Client
// APIBase is the GitHub API root. Defaults to https://api.github.com; tests
// point it at a local server.
APIBase string
// UserAgent identifies this client to GitHub, which rejects requests without
// one outright. Defaults to "<AppName>-updater".
UserAgent string
// StatePath returns where the update-check cache lives. Defaults to
// "<os.UserConfigDir>/<AppName>/update-check.json" β %AppData% on Windows,
// ~/.config elsewhere.
StatePath func() (string, error)
// ExecutablePath returns the binary an update replaces. Defaults to the
// running executable with symlinks resolved, which is what you want in
// production.
//
// Override it in tests to point the install at a throwaway file, so a test
// that exercises the real install path does not overwrite the test binary.
ExecutablePath func() (string, error)
// MaxAsset, MaxChecksums, CheckInterval, NoticeTimeout and UpdateTimeout
// override the Default* constants when non-zero.
MaxAsset int64
MaxChecksums int64
CheckInterval time.Duration
NoticeTimeout time.Duration
UpdateTimeout time.Duration
}
Config describes one project's release setup.
Owner and Repo are required. Every other field has a working default derived from Repo, so the common case is a two-field literal.
type Result ΒΆ
type Result struct {
Current string // version before the attempt
Latest string // latest available version in Channel
Updated bool // whether the binary was replaced
ExePath string // the binary that was (or would be) replaced
Channel Channel // the channel Latest was taken from
}
Result reports the outcome of an update attempt.
type Updater ΒΆ
type Updater struct {
// contains filtered or unexported fields
}
Updater is a configured self-updater. Create one with New and keep it for the process's lifetime; it holds no per-call state and is safe for concurrent use.
func New ΒΆ
New returns an Updater for cfg, filling in every default. It fails only when Owner or Repo is missing β the two things it cannot invent.
func (*Updater) Channel ΒΆ added in v0.2.0
Channel reports the channel this updater follows: the one the user last switched to with Updater.SetChannel or Updater.SwitchChannel, or Config.Channel when they never did.
func (*Updater) DisableEnvName ΒΆ
DisableEnvName returns the environment variable that switches update checking off for this Updater β the configured DisableEnv, or the name derived from Repo.
Useful for a help text or a `--version` footer that tells users how to silence the notice, without restating the derivation rule and letting it drift.
func (*Updater) Disabled ΒΆ
Disabled reports whether update checking is off for this run: the opt-out environment variable is set, or this is a source build with no release to compare against.
func (*Updater) LatestRelease ΒΆ
LatestRelease fetches the latest published release in the channel this updater follows, for callers that want the tag or asset list rather than an update.
func (*Updater) NotifyIfAvailable ΒΆ
NotifyIfAvailable prints a one-line "newer version available" hint to w when the cached latest version is newer than current, then refreshes a stale cache. It never blocks longer than Config.NoticeTimeout.
Write to stderr, not stdout, so the tool's actual output stays pipeable.
The notice reflects the *previously* cached check; the refresh here is for the next run. That is what keeps it cheap on the hot path β the first run after a release ships stays silent, and the one after it tells the user.
func (*Updater) Refresh ΒΆ
Refresh returns the latest version when it is newer than current, and "" otherwise. It refreshes a stale cache first, so unlike Updater.NotifyIfAvailable it reports what is true now β which a TUI can afford, calling it off the UI thread. It blocks for at most Config.NoticeTimeout.
func (*Updater) Run ΒΆ
Run performs (or checks for) a self-update, writing human-readable progress to w. It is the whole body of a `mytool update` subcommand.
ctx bounds the whole check-download-verify-install cycle, so a caller that wires up signal handling can let Ctrl-C interrupt an in-flight download. If ctx has no deadline of its own, Config.UpdateTimeout is applied as one; pass context.Background() to just get that default.
Errors travel back unwrapped: the caller owns the exit code, this only decides what the user reads.
func (*Updater) RunSwitch ΒΆ added in v0.2.0
RunSwitch switches to ch and installs that channel's latest release, writing human-readable progress to w. It is the body of a `mytool update --beta` / `mytool update --stable`, and the counterpart to Updater.Run for the one command that is allowed to move a user backwards; see Updater.SwitchChannel for what that does and does not permit.
Deadlines and error handling work exactly as in Updater.Run.
func (*Updater) SelfUpdate ΒΆ
SelfUpdate checks for a newer release and, unless checkOnly, downloads, verifies and installs it in place of the running binary. current is the running version, with or without a leading "v".
It follows the channel from Updater.Channel β Stable unless the user or the config says otherwise β and only ever moves forward within it; use Updater.SwitchChannel to change channels.
checkOnly makes exactly one network request β the release lookup β and returns as soon as it knows the latest version. It downloads nothing and writes nothing, cache included. The Result still reports Latest, so a caller can report an available update without installing it.
The updated binary is NOT re-executed: the calling process keeps running the old code it already loaded, which is both simpler and safer than re-exec'ing mid-command with arguments that may no longer mean the same thing.
A dev or source build is refused outright β it has no release to compare against, and silently overwriting it with a published binary would discard the user's own build.
func (*Updater) SetChannel ΒΆ added in v0.2.0
SetChannel records ch as the channel this updater follows, without touching the network or the binary. It is the "remember my choice" half of Updater.SwitchChannel, for a tool that exposes the setting on its own (a `mytool config channel beta`) rather than as part of an update.
The cached "latest version" is dropped along with it: it describes the channel being left, and leaving it in place would let the passive notice advertise a version from the old channel until the next check falls due.
It errors only on an unknown channel. Persisting is best effort, like every other cache write here β a cache that cannot be written means the next run checks again, not that the command failed.
func (*Updater) SwitchChannel ΒΆ added in v0.2.0
SwitchChannel moves this updater to ch and installs that channel's latest release, returning what it found and did. It is what a `mytool update --beta` or `mytool update --stable` runs.
Unlike Updater.SelfUpdate it may install an *older* version: leaving the prerelease channel while running 1.5.0-beta.2 means going back to stable 1.4.0, and refusing that would strand the user on a pre-release until the final ships. The downgrade is deliberately confined to this path β an ordinary update stays strictly forward-only, so an attacker who controls the network still cannot serve an old, known-vulnerable release as an "update". What is installed here is whatever the requested channel's newest release contains, verified by checksum and signature exactly like any other update.
The channel is remembered only once the switch has actually landed, so a failed download leaves the updater following the channel the binary is really on.
Directories
ΒΆ
| Path | Synopsis |
|---|---|
|
examples
|
|
|
minimal
command
Command minimal is the smallest useful integration of selfupdate: a passive notice on every run, and an `update` subcommand.
|
Command minimal is the smallest useful integration of selfupdate: a passive notice on every run, and an `update` subcommand. |
|
signed
command
Command signed shows the stronger posture: the release's checksums file must carry a valid Ed25519 signature before anything is installed.
|
Command signed shows the stronger posture: the release's checksums file must carry a valid Ed25519 signature before anything is installed. |
|
testing
Package testing shows how to test a selfupdate integration end to end, offline.
|
Package testing shows how to test a selfupdate integration end to end, offline. |
|
internal
|
|
|
checksum
Package checksum verifies a downloaded asset against a release's sha256sum -format checksums file.
|
Package checksum verifies a downloaded asset against a release's sha256sum -format checksums file. |
|
ghrelease
Package ghrelease is the GitHub Releases client the updater downloads through.
|
Package ghrelease is the GitHub Releases client the updater downloads through. |
|
install
Package install swaps the running executable for a new one.
|
Package install swaps the running executable for a new one. |
|
statecache
Package statecache stores the result of the last update check, so a passive notice costs nothing on the hot path.
|
Package statecache stores the result of the last update check, so a passive notice costs nothing on the hot path. |
|
Package layout describes how a project publishes its release binaries: what the asset for a given platform is called, and how to get an executable out of it.
|
Package layout describes how a project publishes its release binaries: what the asset for a given platform is called, and how to get an executable out of it. |
|
Package verify authenticates a release's checksums file, closing the gap that TLS and checksums leave open.
|
Package verify authenticates a release's checksums file, closing the gap that TLS and checksums leave open. |
|
Package version implements the version ordering the updater relies on: semver precedence, an ingress guard for version strings arriving off the network, and the "is this a source build?" question.
|
Package version implements the version ordering the updater relies on: semver precedence, an ingress guard for version strings arriving off the network, and the "is this a source build?" question. |