goselfupdate

package module
v0.8.2 Latest Latest
Warning

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

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

README

goselfupdate

Go Reference CI Bespoke CI Go Report Card

Self-updating Go binaries, for CLIs released with goreleaser and GitHub.

No dependencies. The core package imports only the standard library.

result, err := goselfupdate.Update(ctx, goselfupdate.Config{
    Owner:   "datapointchris",
    Repo:    "todoui",
    Binary:  "todoui",
    Version: version, // your ldflags-injected build version
})

Install

go get github.com/datapointchris/goselfupdate

A ready-made cobra command

import "github.com/datapointchris/goselfupdate/cobracmd"

root.AddCommand(cobracmd.New(goselfupdate.Config{
    Owner: "datapointchris", Repo: "todoui", Binary: "todoui", Version: version,
}))
$ todoui update
✓ todoui updated: v1.6.0 → v1.6.1

Changes:
  • fix(sync): refresh from the API before CLI commands
  • docs: reference the nested icb projects items command

$ todoui update --check
✓ todoui update available: v1.6.0 → v1.6.1

The command carries no aliases. update is the fleet's one self-update verb, and an alias is what let upgrade coexist with it across every CLI without anyone choosing it. This subpackage is the only thing that imports cobra — projects using flag, urfave/cli or anything else pull in nothing by importing the core.

What it does

  1. Reads the latest release from the GitHub API.
  2. Compares it to the running version by semantic version precedence.
  3. Picks the asset matching the running GOOS/GOARCH.
  4. Verifies its SHA-256 against the release's checksums file.
  5. Extracts the binary and replaces the running executable atomically.

Design decisions

Assets are discovered, not reconstructed. Asset names come from the API and are matched on delimited OS and architecture words, so projects naming archives tool_1.0.0_darwin_arm64.tar.gz or tool-1.0.0-macos-aarch64.zip both work, and a project that changes its naming does not silently break. An ambiguous match is an error listing the candidates, never a guess — picking wrong means overwriting a working binary with one built for another architecture.

Verification precedes extraction. Nothing unverified is passed to an archive reader or written to disk.

Replacement is atomic. The new binary is staged in the target's own directory, flushed to disk, and renamed into place, so an interrupted update cannot leave a half-written executable. On Unix a running process holds its executable by inode, so this is safe mid-execution. Windows locks the running file, so the old binary is displaced first and cleaned up on the next start — call CleanupOldBinary early in main to remove it.

A build with no version refuses to update. Comparing against a development build is meaningless, and updating one would silently discard whatever local build was in place.

Supported release layouts

Aspect Supported
Archives .tar.gz, .tgz, .zip, and bare uncompressed binaries
Platforms Linux, macOS, Windows, and other Unix targets
Naming _ or - separated; x86_64/amd64, aarch64/arm64, macos/darwin
macOS Universal (darwin_all) binaries, used only when no native asset exists
Nesting Binary at the archive root or in a subdirectory; .exe tolerated
Checksums sha256sum and goreleaser formats, with or without the binary-mode * or a leading path
Tags v1.2.3, or cli/v1.2.3 in a repository publishing several components

goreleaser's defaults satisfy all of this with no configuration.

Repositories publishing more than one component

A repository that releases several things gives each its own tag prefix — cli/v1.2.3, api/v2.0.0. Go requires it of a module in a subdirectory, and goreleaser calls it a monorepo tag prefix. Set TagPrefix to pick a stream:

goselfupdate.Config{
    Owner:     "datapointchris",
    Repo:      "meso",
    Binary:    "meso",
    Version:   version,
    TagPrefix: "cli/",
}

This is not only a parsing convenience. GitHub's "latest release" endpoint is repository-wide, so without a prefix it returns whichever component released most recently — a CLI would eventually try to install its own application's release. A prefix switches to the release list, filters it, and picks the highest version rather than the most recently created, so a patch to an older line published after a newer minor is not offered as an update.

Tags come back with the prefix removed, so Release.Tag, Result.From and Result.To are versions and the repository's tag layout stays internal.

Configuration

Everything beyond the four required fields has a working default.

Field Default Purpose
Token see Authentication below A credential you already hold in code; you almost certainly want the default
TokenFunc none A source of your own, tried after the environment and before the command
HTTPClient 60-second timeout Proxies, retries, custom timeouts
Source GitHub Another forge, or a private mirror
Verifier ChecksumVerifier Signature verification, or NoVerification
AllowPrerelease false Include prereleases when selecting the newest version
TagPrefix "" Select one release stream in a repository publishing several

GitHub Enterprise works by setting GitHubSource.APIBase to its /api/v3 root.

Authentication

Authenticated by default. Configure nothing. GitHubSource runs gh auth token when a request is about to be made, and sends what it prints.

The alternative is not "no credential". It is 60 requests an hour, charged per IP address and shared with every other anonymous caller behind the same egress. A default that has to be opted into is a default nobody sets.

Four sources, first non-empty wins:

Source Set by Default
Config.Token you, in code unset
$GITHUB_TOKEN, then $GH_TOKEN whoever runs your CLI unset
TokenFunc() you, in code unset
$GITHUB_TOKEN_COMMAND whoever runs your CLI gh auth token

$GITHUB_TOKEN_COMMAND both redirects and disables, which is what a switch has to do to be worth having:

GITHUB_TOKEN_COMMAND='pass show github/token'   # use this instead
GITHUB_TOKEN_COMMAND=''                         # run nothing, stay anonymous

Single and double quotes are honored; no shell runs the result, so pipelines and expansion are not available. It never fails — a command that is not installed, exits non-zero, or takes longer than ten seconds degrades to an unauthenticated request, which still works against a public repository.

This lives on GitHubSource, not on Config. A credential is the host's business — a Source for another forge brings its own variable and its own command, and nothing above the Source interface learns either name.

Errors

Failures are sentinels, so callers branch without matching message text:

switch {
case errors.Is(err, goselfupdate.ErrDevBuild):
    // built from source
case errors.Is(err, goselfupdate.ErrNoAsset):
    // nothing published for this platform
case errors.Is(err, goselfupdate.ErrChecksumMismatch):
    // download did not match its published checksum
}

Full set: ErrDevBuild, ErrNoRelease, ErrNoAsset, ErrAmbiguousAsset, ErrNoChecksums, ErrChecksumMismatch, ErrBinaryNotFound, ErrInvalidConfig.

Security model

Integrity comes from the release's checksum file, fetched over TLS from the same release. This defends against a corrupted, truncated or intercepted download. It does not defend against a compromised publishing account, which can rewrite the checksum file alongside the asset — that requires a signature verified against a key distributed out of band. Implement Verifier to add one.

Deliberately absent: this package does not link golang.org/x/crypto/openpgp, which is unmaintained and carries a permanent advisory (GO-2026-5932) with no fixed version. A project depending on it cannot run govulncheck clean.

Telling the user without installing

autoupdate is the other half: it checks once a day and prints one line. It never installs, so the update command above stays the only thing that writes a binary.

import "github.com/datapointchris/goselfupdate/autoupdate"

func main() {
    config := autoupdate.Config{Update: goselfupdate.Config{
        Owner: "you", Repo: "tool", Binary: "tool", Version: version,
    }}

    if err := cobracmd.Execute(context.Background(), rootCmd, config); err != nil {
        if errors.Is(err, cobracmd.ErrUsage) {
            os.Exit(2)
        }
        os.Exit(1)
    }
}

ErrUsage marks a failure caused by how the command was typed — an unknown or malformed flag, or an unknown subcommand — rather than by the command running and failing. Cobra returns both as ordinary errors, so without this every failure flattens to exit 1 and a caller cannot tell "you typed it wrong" from "it ran and failed". Only the former is worth retrying with different arguments. Exit 2 is the shell convention, and what Python's argparse uses.

Once per 24 hours, if a newer release exists, one line goes to stderr after your command's own output:

tool v1.4.0 available (running v1.3.2) — run `tool update`

The check runs concurrently with your command and is abandoned if the command finishes first, so a fast command pays nothing. It never returns an error and never prints one: a failed check is recorded in the state file and swallowed, because an update notice must not be able to break the command the user typed.

Nothing is printed when any of these hold:

Condition Why
NO_AUTO_UPDATE or TOOL_NO_AUTO_UPDATE is set Opted out
CI, BUILD_NUMBER, RUN_ID, GITHUB_ACTIONS, CODESPACES Not a human
stdout or stderr is not a terminal tool list > out 2>&1 must stay clean
The version is not a plain vX.Y.Z A development build
The command is update, version, completion or a shell-completion callback Would be pointless or constant
Checked within the interval One request per day, not per invocation

Presence-only, any value: NO_AUTO_UPDATE=0 disables it, the same way NO_COLOR works. Set the interval separately with AUTO_UPDATE_INTERVAL=6h or TOOL_AUTO_UPDATE_INTERVAL=30m.

Config.Interactive overrides terminal detection — pass false from a program that already knows it is writing into a pager or a structured-output mode.

State lives in ${XDG_STATE_HOME:-~/.local/state}/<tool>/autoupdate.json, written atomically, and the timestamp is written before the network call — gh stamps only on success, so a rate-limited or offline user re-hits the API on every invocation until the window resets.

autoupdate links nothing outside the standard library, so adding a notice to a CLI adds no dependencies. Only cobracmd imports cobra.

Scope

Provided: GitHub releases and checksum verification. GitLab, Gitea and signature verification are not implemented — Source and Verifier are the interfaces to implement for them, and neither requires changes here.

Not supported: in-place binary patching, rollback to an arbitrary version, and update channels beyond the prerelease toggle.

Alternatives

Package Notes
creativeprojects/go-selfupdate Broadest source support (GitHub, GitLab, Gitea, HTTP). Links x/crypto/openpgp for signature verification, so it carries GO-2026-5932; pulls in ~14 modules
minio/selfupdate Applies an update with minisign verification. Does not locate releases — that half is yours to write
sanbornm/go-selfupdate The original. Binary-diff patching against your own update server

Use this one if you publish goreleaser archives to GitHub releases and want no dependencies. Use creativeprojects/go-selfupdate if you need GitLab or Gitea.

Requirements

  • The Go version in go.mod, or newer.
  • Releases publishing per-platform archives and a checksums file.
  • A version injected at build time, e.g. -ldflags "-X main.version={{.Version}}".

License

MIT

Documentation

Overview

Package goselfupdate replaces a running binary with a newer release.

It targets the shape of release that goreleaser produces by default: per-platform archives published to a GitHub release alongside a checksums.txt. Integrity is established from that checksum file, so no signing key or PGP implementation is involved.

Usage

The zero-configuration case needs four fields:

result, err := goselfupdate.Update(ctx, goselfupdate.Config{
	Owner:   "datapointchris",
	Repo:    "todoui",
	Binary:  "todoui",
	Version: version, // injected at build time via ldflags
})
if err != nil {
	return err
}
if result.Applied {
	fmt.Printf("updated %s → %s\n", result.From, result.To)
}

Check performs the same lookup without downloading or writing anything.

For a ready-made cobra command, see the cobracmd subpackage. It is kept separate so that importing this package does not pull in a CLI framework.

Scope

GitHub releases and checksum verification are provided. Source and Verifier are the extension points for anything else — a different forge, or signature verification — and can be supplied through Config without changes here.

Archives may be tar.gz or zip; a release asset that is a bare binary is also accepted. Linux, macOS and Windows are supported.

Versions

Versions are compared by semantic version precedence, implemented here so the package keeps no third-party dependencies. A leading "v" is optional on both the running version and the release tag, since goreleaser configurations disagree about whether to inject {{.Tag}} or {{.Version}}.

A binary with no version injected reports itself as a development build and refuses to update, because there is no meaningful version to compare and an update would silently discard whatever local build was in place.

Release streams

A repository publishing one component tags it v1.2.3 and needs no configuration. A repository publishing several gives each its own prefix — cli/v1.2.3, api/v2.0.0 — which is what Go requires of a module in a subdirectory and what goreleaser calls a monorepo tag prefix.

Set Config.TagPrefix to select a stream. GitHub's "latest release" endpoint is repository-wide and would otherwise return whichever component released most recently, so a prefix switches to the release list and filters it. Reported tags have the prefix removed, leaving Release.Tag a version.

Index

Examples

Constants

View Source
const DefaultGitHubAPI = "https://api.github.com"

DefaultGitHubAPI is the API root used when GitHubSource.APIBase is empty. Point APIBase at a GitHub Enterprise installation's /api/v3 root to update from one.

View Source
const DefaultTimeout = 60 * time.Second

DefaultTimeout bounds an update when Config.HTTPClient is not supplied.

View Source
const DefaultTokenCommand = "gh auth token"

DefaultTokenCommand runs when nothing overrides TokenCommandEnv.

Authenticating is the default because the alternative is not "no credential" but sixty requests an hour, charged per IP address and shared by every host behind one egress. Measured 2026-08-21 across one household: two machines checking on a timer held that pool at zero for whole hours, and every tool on the network that asked anonymously was refused. A default that has to be opted into is a default nobody sets.

View Source
const TokenCommandEnv = "GITHUB_TOKEN_COMMAND"

TokenCommandEnv names the command that produces a GitHub credential.

Variables

View Source
var (
	// ErrDevBuild is returned when the running binary carries no usable
	// version. Updating it would discard a local build for a release that may
	// be older, with no way to tell which is newer.
	ErrDevBuild = errors.New("cannot update a development build")

	// ErrNoRelease is returned when the source publishes no usable release.
	ErrNoRelease = errors.New("no release found")

	// ErrNoAsset is returned when a release carries nothing for the running
	// platform.
	ErrNoAsset = errors.New("no release asset for this platform")

	// ErrAmbiguousAsset is returned when more than one asset matches the
	// running platform. Choosing between them by guessing risks installing a
	// binary for the wrong architecture over a working one.
	ErrAmbiguousAsset = errors.New("multiple release assets match this platform")

	// ErrNoChecksums is returned when a release publishes no checksum file and
	// no alternative [Verifier] was configured.
	ErrNoChecksums = errors.New("release publishes no checksum file")

	// ErrChecksumMismatch is returned when a downloaded asset does not match
	// its published checksum. Nothing is extracted or installed.
	ErrChecksumMismatch = errors.New("checksum mismatch")

	// ErrBinaryNotFound is returned when the downloaded archive does not
	// contain the configured binary.
	ErrBinaryNotFound = errors.New("binary not found in release archive")

	// ErrInvalidConfig is returned when a [Config] is missing a required field.
	ErrInvalidConfig = errors.New("invalid config")
)

Functions

func Canonical added in v0.3.0

func Canonical(version string) string

Canonical returns a version with the leading "v" this package prints versions with. Tags carry one and build metadata often does not, so both forms reach a caller and have to be reported the same way.

func Changelog

func Changelog(ctx context.Context, cfg Config, fromTag, toTag string) ([]string, error)

Changelog returns the commit subjects between two versions when the configured Source implements Changeloger, and nil when it does not.

func CleanupOldBinary

func CleanupOldBinary() error

CleanupOldBinary removes the previous executable left behind by an update on platforms that cannot overwrite a running binary, currently only Windows.

It is safe to call on every platform and on every start, and reports no error when there is nothing to remove. Calling it early in main keeps a stale copy from accumulating next to the installed binary.

func IsReleaseVersion added in v0.3.0

func IsReleaseVersion(text string) bool

IsReleaseVersion reports whether text is a plain vX.Y.Z release, with no pre-release and no build metadata.

Stricter than IsValidVersion on purpose, and the distinction is the whole reason this exists. Go stamps a VCS-derived pseudo-version such as v1.6.1-0.20260724161156-2c04703+dirty onto local builds, and that string is *valid semver*: "0.20260724161156-2c04703" is a legal pre-release identifier, so it parses and sorts below v1.6.1 exactly as the specification says. A caller asking "is this a real release" therefore cannot use IsValidVersion.

Every consumer had reimplemented this regex; it belongs here.

func IsValidVersion added in v0.3.0

func IsValidVersion(text string) bool

IsValidVersion reports whether text is a semantic version.

Types

type Asset

type Asset struct {
	// Name is the file name, used to match the running platform.
	Name string

	// URL is where [Source.Download] fetches the asset from.
	URL string

	// Size is the asset's length in bytes, or zero if the source does not
	// report it.
	Size int64
}

Asset is one file attached to a Release.

type Changeloger

type Changeloger interface {
	Changelog(ctx context.Context, fromTag, toTag string) ([]string, error)
}

Changeloger is an optional interface a Source may implement to describe what changed between two versions. The cobracmd subpackage prints the result after a successful update; a source that does not implement it simply produces no changelog.

type ChecksumVerifier

type ChecksumVerifier struct{}

ChecksumVerifier checks an asset's SHA-256 against the checksum file published with the release. This is the default.

It defends against a corrupted, truncated or intercepted download, given that the checksum file itself is fetched over TLS from the same release. It does not defend against a compromised publishing account, which can rewrite the checksum file alongside the asset — that requires a signature and a key distributed out of band.

func (ChecksumVerifier) Verify

func (ChecksumVerifier) Verify(ctx context.Context, source Source, release Release, asset Asset, data []byte) error

Verify implements Verifier.

type Config

type Config struct {
	// Owner and Repo identify the GitHub repository publishing the releases.
	Owner string
	Repo  string

	// Binary is the executable's name inside the release archive. It is also
	// used in messages produced by the cobracmd subpackage.
	Binary string

	// Version is the running build's version, conventionally injected with
	// -ldflags "-X main.version=...". A leading "v" is optional. An empty,
	// "dev" or otherwise non-semver value makes the update fail with
	// [ErrDevBuild].
	Version string

	// Token authenticates requests to the release source. Without one, GitHub
	// allows 60 API requests per hour per IP address and rejects private
	// repositories outright. Defaults to $GITHUB_TOKEN, then $GH_TOKEN, then
	// [Config.TokenFunc].
	Token string

	// TokenFunc resolves a token when Token is empty and neither environment
	// variable is set. Returning "" leaves the request unauthenticated.
	//
	// It exists because a credential can be expensive to obtain — a keychain
	// prompt, a `gh auth token` subprocess — and it is called only when a
	// request is actually about to be made. A caller that resolves such a token
	// eagerly into Token instead pays for it on every invocation, including the
	// ones where [autoupdate] declines to check at all; that gate is otherwise
	// free, and a subprocess spawn in front of it is the entire cost.
	TokenFunc func() string

	// HTTPClient performs every request. Defaults to a client with
	// [DefaultTimeout]. Supply one to control proxies, retries or timeouts.
	HTTPClient *http.Client

	// Source locates releases. Defaults to a [GitHubSource] built from Owner,
	// Repo, Token and HTTPClient.
	Source Source

	// Verifier checks a downloaded asset before it is extracted. Defaults to
	// [ChecksumVerifier], which uses the release's own checksums file. Set
	// [NoVerification] to skip the check, which is not recommended.
	Verifier Verifier

	// AllowPrerelease considers prereleases when looking for the latest
	// version. GitHub excludes them from its "latest release" endpoint, so
	// enabling this changes which endpoint is used.
	AllowPrerelease bool

	// TagPrefix selects one release stream in a repository publishing several,
	// as in "cli/" for tags of the form cli/v1.2.3. Empty is the single-stream
	// default. See [GitHubSource.TagPrefix].
	//
	// Like Owner and Repo, this configures the default GitHub source and is
	// unused when a custom Source is supplied — such a source owns its own tag
	// layout and is expected to report versions in [Release.Tag].
	TagPrefix string
}

Config describes what to update to and how to reach it.

Owner, Repo, Binary and Version are required unless a custom Source is supplied, in which case Owner and Repo are unused. Every other field has a working default.

Example (PrivateRepository)

A token raises GitHub's unauthenticated limit of 60 requests an hour and is required for a private repository. $GITHUB_TOKEN and $GH_TOKEN are used automatically when Token is empty.

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/datapointchris/goselfupdate"
)

// version is injected at build time, conventionally with
// -ldflags "-X main.version={{.Version}}".
var version = "v1.0.0"

func main() {
	cfg := goselfupdate.Config{
		Owner:      "datapointchris",
		Repo:       "internal-tool",
		Binary:     "internal-tool",
		Version:    version,
		Token:      os.Getenv("MY_GITHUB_TOKEN"),
		HTTPClient: &http.Client{Timeout: 2 * time.Minute},
	}

	if _, err := goselfupdate.Update(context.Background(), cfg); err != nil {
		log.Fatal(err)
	}
}

type GitHubSource

type GitHubSource struct {
	// Owner and Repo identify the repository.
	Owner string
	Repo  string

	// Token authenticates requests. Leave it empty and the source resolves one
	// itself: $GITHUB_TOKEN, then $GH_TOKEN, then TokenFunc, then
	// $GITHUB_TOKEN_COMMAND — which defaults to `gh auth token`.
	//
	// Without any credential GitHub permits 60 requests per hour per IP address,
	// shared with every other anonymous caller behind the same egress, and
	// denies private repositories.
	Token string

	// TokenFunc is a source of the caller's own, tried after the environment and
	// before the command. Returning "" falls through.
	//
	// Reaching for gh no longer needs one — that is the default. This is for a
	// credential neither the environment nor a command can produce.
	//
	// Called only when a request is about to be made, because a credential can
	// be expensive to obtain and [autoupdate]'s gate declines most invocations
	// without touching the network.
	TokenFunc func() string

	// HTTPClient performs requests. Defaults to http.DefaultClient.
	HTTPClient *http.Client

	// APIBase overrides [DefaultGitHubAPI].
	APIBase string

	// AllowPrerelease returns the newest release even if it is marked as a
	// prerelease. GitHub's "latest release" endpoint excludes them, so this
	// selects a different endpoint.
	AllowPrerelease bool

	// TagPrefix selects one release stream in a repository that publishes
	// several, as in "cli/" for tags of the form cli/v1.2.3. Empty means the
	// repository publishes a single stream tagged v1.2.3, which is the common
	// case and the default.
	//
	// A prefix is not cosmetic: Go requires a module in a subdirectory to be
	// tagged with that subdirectory, and GitHub's "latest release" endpoint is
	// repository-wide, so it will happily return another component's release.
	// Setting this switches to the release list and filters it, which is the
	// only way to ask for the newest release of one stream.
	//
	// Reported tags have the prefix removed, so [Release.Tag] is a version as
	// documented and callers never see the repository's tag layout.
	TagPrefix string
	// contains filtered or unexported fields
}

GitHubSource reads releases from GitHub's REST API.

Example (Enterprise)

Point APIBase at a GitHub Enterprise installation's API root to update from one.

package main

import (
	"context"
	"log"
	"os"

	"github.com/datapointchris/goselfupdate"
)

// version is injected at build time, conventionally with
// -ldflags "-X main.version={{.Version}}".
var version = "v1.0.0"

func main() {
	cfg := goselfupdate.Config{
		Binary:  "internal-tool",
		Version: version,
		Source: &goselfupdate.GitHubSource{
			Owner:   "platform",
			Repo:    "internal-tool",
			APIBase: "https://github.example.com/api/v3",
			Token:   os.Getenv("GHE_TOKEN"),
		},
	}

	if _, err := goselfupdate.Update(context.Background(), cfg); err != nil {
		log.Fatal(err)
	}
}

func (*GitHubSource) Changelog

func (s *GitHubSource) Changelog(ctx context.Context, fromTag, toTag string) ([]string, error)

Changelog implements Changeloger, returning the subject line of every commit between two tags.

func (*GitHubSource) Download

func (s *GitHubSource) Download(ctx context.Context, asset Asset) ([]byte, error)

Download implements Source. The asset endpoint serves metadata by default and the file itself only when asked for bytes, so this Accept is what makes a download a download.

func (*GitHubSource) LatestRelease

func (s *GitHubSource) LatestRelease(ctx context.Context) (Release, error)

LatestRelease implements Source.

type NoVerification

type NoVerification struct{}

NoVerification disables integrity checking. Supplying it means trusting the transport alone; prefer a Verifier that checks something.

func (NoVerification) Verify

Verify implements Verifier by accepting anything.

type Release

type Release struct {
	// Tag is the release's version, with or without a leading "v".
	Tag string

	// Assets are the files published with the release.
	Assets []Asset

	// Prerelease reports whether the source marked this release as a
	// prerelease.
	Prerelease bool
}

Release is one published version and the files attached to it.

type Result

type Result struct {
	// From is the running version, canonicalised with a leading "v".
	From string

	// To is the version now installed, or the version that would be installed
	// by [Update] after a [Check]. It equals From when nothing is newer.
	To string

	// Applied reports whether a new binary was written.
	Applied bool

	// Release is the release To was taken from. Its zero value means no
	// release was newer.
	Release Release
}

Result describes what an update found, whether or not it installed anything.

func Check

func Check(ctx context.Context, cfg Config) (Result, error)

Check reports whether a newer release exists, without downloading an asset or touching the filesystem.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/datapointchris/goselfupdate"
)

// version is injected at build time, conventionally with
// -ldflags "-X main.version={{.Version}}".
var version = "v1.0.0"

func main() {
	result, err := goselfupdate.Check(context.Background(), goselfupdate.Config{
		Owner:   "datapointchris",
		Repo:    "todoui",
		Binary:  "todoui",
		Version: version,
	})
	if err != nil {
		log.Fatal(err)
	}

	if result.UpdateAvailable() {
		fmt.Printf("%s is available, running %s\n", result.To, result.From)
	}
}

func Update

func Update(ctx context.Context, cfg Config) (Result, error)

Update replaces the running executable with the latest release.

It is a no-op returning a Result with Applied false when the running version is already current. Symbolic links are resolved, so an update rewrites the real file rather than replacing a link with a binary.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/datapointchris/goselfupdate"
)

// version is injected at build time, conventionally with
// -ldflags "-X main.version={{.Version}}".
var version = "v1.0.0"

func main() {
	result, err := goselfupdate.Update(context.Background(), goselfupdate.Config{
		Owner:   "datapointchris",
		Repo:    "todoui",
		Binary:  "todoui",
		Version: version,
	})
	if err != nil {
		log.Fatal(err)
	}

	if result.Applied {
		fmt.Printf("updated %s → %s\n", result.From, result.To)
	} else {
		fmt.Printf("already at %s\n", result.To)
	}
}
Example (ErrorHandling)

Errors are sentinels, so a caller can tell a development build or a rate limit apart from a genuine failure without matching on message text.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/datapointchris/goselfupdate"
)

// version is injected at build time, conventionally with
// -ldflags "-X main.version={{.Version}}".
var version = "v1.0.0"

func main() {
	_, err := goselfupdate.Update(context.Background(), goselfupdate.Config{
		Owner:   "datapointchris",
		Repo:    "todoui",
		Binary:  "todoui",
		Version: version,
	})

	switch {
	case err == nil:
	case errors.Is(err, goselfupdate.ErrDevBuild):
		fmt.Println("built from source; update it the way you built it")
	case errors.Is(err, goselfupdate.ErrNoAsset):
		fmt.Println("no build published for this platform")
	case errors.Is(err, goselfupdate.ErrChecksumMismatch):
		fmt.Println("download did not match its published checksum")
	default:
		fmt.Println("update failed:", err)
	}
}

func UpdateTo

func UpdateTo(ctx context.Context, cfg Config, target string) (Result, error)

UpdateTo is Update against an explicit path rather than the running executable. It is what to call to update a binary other than this one, and what tests use to avoid replacing the test binary.

func (Result) UpdateAvailable

func (r Result) UpdateAvailable() bool

UpdateAvailable reports whether a newer version was found.

type Source

type Source interface {
	// LatestRelease returns the newest release the source knows about. It
	// returns [ErrNoRelease] when there is none.
	LatestRelease(ctx context.Context) (Release, error)

	// Download fetches an asset's bytes.
	Download(ctx context.Context, asset Asset) ([]byte, error)
}

Source locates and fetches releases. GitHubSource is the provided implementation; supply another through Config to update from a different forge or from a private mirror.

type Verifier

type Verifier interface {
	// Verify returns nil if data is authentic. release and asset describe
	// where the data came from, so an implementation can fetch a checksum
	// file, signature or manifest from the same release.
	Verify(ctx context.Context, source Source, release Release, asset Asset, data []byte) error
}

Verifier establishes that a downloaded asset is the one the release published. It runs before anything is extracted, so a failure means no untrusted bytes are ever parsed or written.

Directories

Path Synopsis
Package autoupdate tells a user that a newer release exists, and nothing else.
Package autoupdate tells a user that a newer release exists, and nothing else.
Package cobracmd provides a ready-made update command for CLIs built with cobra.
Package cobracmd provides a ready-made update command for CLIs built with cobra.

Jump to

Keyboard shortcuts

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