selfupdate

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 16 Imported by: 0

README ΒΆ

selfupdate

Ship a CLI that updates itself β€” without shipping a supply-chain hole.

Three calls in main(): your tool checks GitHub Releases, verifies what it downloads, and atomically swaps its own binary.

No dependencies. No re-exec. No surprises on Windows.

CI Go Reference Go Dependencies License

Quick start Β· Release setup Β· Layouts Β· Channels Β· Signing Β· Hardening Β· Testing


[!NOTE] Young library. The API is settled enough to build on but may move before v1. Issues welcome. πŸ™Œ

πŸš€ Quick start

go get github.com/p-arndt/selfupdate
up, err := selfupdate.New(selfupdate.Config{Owner: "you", Repo: "mytool"})

selfupdate.CleanupLeftovers()                      // once at startup
up.NotifyIfAvailable(os.Stderr, version)           // the passive hint
up.Run(ctx, os.Stdout, version, false)             // your `update` subcommand

That's the whole integration. Owner and Repo are the only required fields.

What users see:

$ mytool status
... your tool's output ...

A newer mytool is available: 1.4.0 (you have 1.2.0). Run `mytool update` to upgrade.

$ mytool update
Current version: 1.2.0. Checking for updates…
Updated mytool 1.2.0 β†’ 1.4.0.

The hint goes to stderr, so mytool status | jq stays clean.

The three calls in detail
Call When Notes
CleanupLeftovers() Once at startup Package-level, no Updater needed. Removes the <exe>.old file a previous Windows update left. Never errors.
NotifyIfAvailable(w, current) Late in a normal run Prints the cached result, then refreshes for next time. Never blocks >1.5s. Silent on dev builds and when the opt-out env var is set.
Run(ctx, w, current, checkOnly) Your update subcommand Prints progress, installs, reports. Returns an error; you own the exit code. ctx lets Ctrl-C interrupt a download; without a deadline it gets UpdateTimeout (60s).

current takes your version with or without a leading v. "", "dev" and "(devel)" are treated as source builds and never updated over β€” a local build is usually ahead of the last tag.

A runnable version lives in examples/minimal.

✨ Features

  • πŸ”’ Verified downloads β€” SHA-256 against the release checksums, before anything touches your binary
  • ✍️ Optional signing β€” Ed25519 over the checksums, with key rotation that doesn't brick shipped binaries
  • πŸͺŸ Windows-correct β€” a running .exe can't be overwritten, so it's renamed aside and cleaned up next run
  • ⚑ Free on the hot path β€” the notice reads a cache; the network refresh is bounded at 1.5s, once a day
  • πŸ§ͺ Beta channel β€” opt users into pre-releases and back out again, with the rollback that implies
  • πŸ“¦ Two release layouts β€” archives or raw binaries, or bring your own naming
  • 🧊 Zero dependencies β€” standard library only, enforced by CI
  • πŸ§ͺ Testable β€” three Config seams point the whole flow at httptest and a temp dir
  • 🚫 Opinionated refusals β€” no downgrades, no dev-build clobbering, no plain HTTP, no truncated installs

πŸ“€ What your release must ship

The library finds nothing unless your release publishes these names. Repo mytool, tag v1.2.0:

mytool_1.2.0_linux_amd64.tar.gz      ← contains a file named "mytool"
mytool_1.2.0_darwin_arm64.tar.gz
mytool_1.2.0_windows_amd64.zip       ← contains "mytool.exe"
mytool_1.2.0_checksums.txt
mytool_1.2.0_checksums.txt.sig       ← only when signing

Checksums are plain sha256sum output, hashing the archive, not the binary inside it:

9f2c…  mytool_1.2.0_linux_amd64.tar.gz

The five things that trip people up:

  1. The tag carries v, the file names don't. v1.2.0 β†’ assets say _1.2.0_. The most common mistake by far.
  2. Stamp the version in: -ldflags "-X main.version=1.2.0". Without it your build reports dev and the updater refuses to touch it.
  3. Set AppName if the binary isn't named after the repo β€” one field, not six.
  4. Signing is all-or-nothing. With a verifier set, a release without a .sig is an error, never a fallback.
  5. Tags must match ^[0-9][0-9A-Za-z.+-]{0,63}$ once v is stripped, or the release is rejected.

Also: pre-release tags (v1.2.0-rc.1) never appear in GitHub's releases/latest, so they're never offered β€” unless the user is on the prerelease channel. A missing platform asset gives a clear error, not a silent skip.

πŸŽ›οΈ What you get for free

For Config{Owner: "you", Repo: "mytool"} β€” all derived from AppName, which defaults to Repo:

Default
Asset name mytool_<version>_<goos>_<goarch>.tar.gz (.zip on Windows)
Binary inside mytool (mytool.exe on Windows)
Checksums file mytool_<version>_checksums.txt
Upgrade hint mytool update
Opt-out variable MYTOOL_NO_UPDATE_CHECK
State cache <os.UserConfigDir>/mytool/update-check.json (channel choice lives here too)
Release channel stable β€” full releases only
User agent mytool-updater
Check interval / timeouts 24h Β· 1.5s notice Β· 60s update

[!TIP] Binary not named after the repo? Set AppName and stop β€” asset names, checksums file, opt-out variable, state dir, user agent and hint text all follow it. Repo compose-check-updates shipping ccu needs AppName: "ccu", not six separate overrides.

Every field is overridable on Config.

πŸ—‚οΈ Release layouts

Layout Produces Use when
&layout.Archive{} (default) mytool_1.2.0_linux_amd64.tar.gz goreleaser-style, binary inside an archive
&layout.RawBinary{} mytool-linux-amd64 (.exe on Windows) your README's install one-liner curls that exact name

Neither fits? Override the names instead of forking:

Layout: &layout.Archive{
    Name:       func(version, goos, goarch string) string { ... },
    BinaryName: func(goos string) string { ... },
},
ChecksumsName: func(version string) string { ... },

Or implement layout.Layout β€” three methods.

πŸ§ͺ Release channels

Two channels, both installing from the same releases and verified the same way β€” they differ only in which releases are eligible:

Channel Sees Endpoint
Stable (default) full releases only /releases/latest β€” GitHub excludes pre-releases and drafts for you
Prerelease every published release, pre-releases included, highest version wins /releases

Prerelease is a superset, not a parallel track: when 1.5.0 finally ships it outranks 1.5.0-rc.2, so beta users move to the final release like everyone else instead of getting stranded on release candidates.

// `mytool update --beta` / `mytool update --stable`
ch, err := selfupdate.ParseChannel(*channelFlag) // "", "stable", "beta", "prerelease"
if err != nil {
	return err
}
err = up.RunSwitch(ctx, os.Stdout, version, ch)
$ mytool update --beta
Switching mytool to the prerelease channel (currently 1.4.0)…
Updated mytool 1.4.0 β†’ 1.5.0-beta.1 on the prerelease channel.

$ mytool update --stable
Switching mytool to the stable channel (currently 1.5.0-beta.1)…
Rolled mytool back 1.5.0-beta.1 β†’ 1.4.0, the latest stable release.

The choice is remembered in the state file, so plain mytool update and the passive notice follow it from then on β€” and only once the switch actually landed, so a failed download never leaves the updater tracking a channel the binary isn't on.

Call Does
up.Channel() the channel in effect: the user's choice, else Config.Channel
up.SetChannel(ch) remember a choice, no network, no install
up.SwitchChannel(ctx, current, ch) switch + install, returns a *Result
up.RunSwitch(ctx, w, current, ch) the same, with human-readable output

Ship betas by default (a nightly build of the same tool) with Config{Channel: selfupdate.Prerelease} β€” a user's own choice still outranks it.

[!IMPORTANT] Switching channels is the only path allowed to install an older version; leaving beta while running 1.5.0-beta.2 has to mean going back to 1.4.0, or the user is stuck until the final ships. Plain Run/SelfUpdate stays strictly forward-only, so the network can never talk you into a downgrade.

πŸ” Signing releases

Checksums only prove the download wasn't corrupted in transit. They travel over the same channel as the binary, so anyone who can edit release assets regenerates both and verification still "passes". Signing anchors trust in a key that never lives in the repo or the release.

Verifier: &verify.Ed25519{
    Keys:   []string{"sQrabBts6F9SlNhvnwFw5HRHS8xHHM92frEJKpctvd4"},
    Domain: "mytool release checksums v1",
},
  • Unsigned release β†’ refused outright. Never downgraded to a warning.
  • Replay-proof. The signed message includes the checksums file's name, which carries the version, so an old signed release can't reappear under a newer tag.
  • Cheap to reject. Signature presence is checked against metadata already in hand, then the small files are verified β€” a bad release never costs you a 64 MiB download.
  • Fails closed. An empty or all-garbage key list refuses everything; it never becomes "no key, so skip".

Sign at release time with the same struct β€” v.Sign(privateKey, checksumsName, content) β€” and upload the result as <checksums name>.sig. See examples/signed.

[!WARNING] Keys is a list so you can rotate. A binary only trusts the keys it was compiled with, so swapping the sole key makes every shipped binary reject every new release β€” permanently, unfixably. Instead: add the successor, ship releases signed by it while both are trusted, drop the old one once users have upgraded.

πŸ–₯️ For a TUI

latest := up.Refresh(version) // "" when current, disabled, or unreachable

Unlike NotifyIfAvailable, this refreshes a stale cache and reports what's true now. Blocks up to 1.5s β€” call it off the UI thread.

πŸ›‘οΈ What it refuses to do

Deliberate, load-bearing refusals. If one is in your way, the answer is almost never to remove it.

  • https-only, GitHub hosts only β€” redirects included. A checksum only vouches for a download if the whole chain is authenticated. Loopback may use plain http, so tests can serve locally.
  • Release tags validated at ingress β€” leading digit, [0-9A-Za-z.+-], ≀64 bytes. Tags reach your terminal, file names and cache: no ANSI escapes, no ../, no homoglyphs.
  • Size caps error, never truncate β€” including what an archive inflates to. A truncated binary bricks an install as thoroughly as a malicious one.
  • Only strictly newer versions install β€” an attacker controlling the network can stall you, but can't serve an older, known-vulnerable release as an "update". The single exception is an explicit SwitchChannel, where going back is what the user asked for.
  • Dev and source builds are never touched.
  • Errors never echo server text β€” not the URL, not the HTTP status line.
  • The new binary is not re-executed β€” the process keeps the code it loaded.
  • The cache is re-validated on load β€” it's attacker-influencable at rest.
  • Nothing large is fetched before it's trusted.

πŸ§ͺ Testing your integration

Three Config seams run the whole flow offline:

Seam Point it at
APIBase an httptest server instead of api.github.com
StatePath a temp file, so tests never touch the real cache
ExecutablePath a throwaway file, so an install doesn't overwrite your test binary
cfg := selfupdate.Config{
    Owner: "you", Repo: "mytool",
    APIBase:        srv.URL,
    StatePath:      func() (string, error) { return filepath.Join(t.TempDir(), "c.json"), nil },
    ExecutablePath: func() (string, error) { return throwaway, nil },
}

Take a Config in your own constructor so production passes the zero value and tests pass their seams. examples/testing is a complete, runnable version β€” copy tool_test.go and change the names.

πŸ“š Packages

Package What's in it
. (selfupdate) Config, New, Updater β€” what you integrate against
layout How release assets are named and packed
verify Optional Ed25519 signature verification
version Semver ordering and the ingress guard
internal/… GitHub client, checksums, the atomic swap, the cache

Most integrations import only the root package. Integration notes for coding agents live in llm.txt.

πŸ› οΈ Development

just ci          # vet + gofmt + race + dependency gate
just test        # go test ./...
just cover       # per-package coverage

CI runs on Windows, macOS and Linux β€” the executable swap takes a different path on Windows. It also fails the build if a third-party dependency ever appears: a self-updater with a dependency tree is a supply-chain surface sitting directly on the code path that replaces the user's binary.

πŸ“„ License

MIT

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

View Source
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 Asset ΒΆ

type Asset = ghrelease.Asset

Asset is one downloadable file attached to a Release.

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

func ParseChannel(s string) (Channel, error)

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 Release ΒΆ

type Release = ghrelease.Release

Release is a published GitHub release: its tag and the assets attached to it.

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

func New(cfg Config) (*Updater, error)

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

func (u *Updater) Channel() Channel

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

func (u *Updater) DisableEnvName() string

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

func (u *Updater) Disabled(current string) bool

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

func (u *Updater) LatestRelease(ctx context.Context) (*Release, error)

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

func (u *Updater) NotifyIfAvailable(w io.Writer, current string)

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

func (u *Updater) Refresh(current string) string

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

func (u *Updater) Run(ctx context.Context, w io.Writer, current string, checkOnly bool) error

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

func (u *Updater) RunSwitch(ctx context.Context, w io.Writer, current string, ch Channel) error

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

func (u *Updater) SelfUpdate(ctx context.Context, current string, checkOnly bool) (*Result, error)

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

func (u *Updater) SetChannel(ch Channel) error

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

func (u *Updater) SwitchChannel(ctx context.Context, current string, ch Channel) (*Result, error)

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.

Jump to

Keyboard shortcuts

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