goselfupdate

package module
v0.11.2 Latest Latest
Warning

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

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

README

goselfupdate

A Go library that gives a CLI its update command and its once-a-day notice that a newer release exists. It reads releases from GitHub, verifies the download against the release's checksum file, and swaps the running executable in one rename.

It has two packages. The root package installs a release, and backs the update command a user types. autoupdate only tells: it checks in the background and prints one line, and it never installs. A program wanting only the install half links none of the notice.

Neither package knows what a command-line parser is, and both import only the standard library. So it works the same behind flag, urfave/cli, a hand-rolled parser or cobra. For cobra, goclikit is the front door. It builds the update subcommand from this library and runs the background check around every other command.

Install

go get github.com/datapointchris/goselfupdate@latest

It needs three things:

  • The Go version in go.mod, or newer.
  • Releases that publish per-platform archives and a checksums file. goreleaser's defaults produce both.
  • A version injected at build time, as in -ldflags "-X main.version={{.Version}}". goreleaser's default ldflags already set main.version.

Starting from scratch

One main carries both halves. The update branch installs, and every other invocation runs beside the background check.

package main

import (
    "context"
    "fmt"
    "os"

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

var version = "dev" // a release build overwrites this through -ldflags

func main() {
    _ = goselfupdate.CleanupOldBinary() // removes what a Windows update displaced; a no-op elsewhere

    ctx := context.Background()
    release := goselfupdate.Config{
        Owner:   "you", // the GitHub repository that publishes the releases
        Repo:    "tool",
        Binary:  "tool", // the executable's name inside each release archive
        Version: version,
    }

    if len(os.Args) > 1 && os.Args[1] == "update" {
        result, err := goselfupdate.Update(ctx, release) // replaces this executable on disk
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        fmt.Printf("%s → %s, applied: %v\n", result.From, result.To, result.Applied)
        return // update never starts the background check
    }

    session := autoupdate.Start(ctx, autoupdate.Config{Update: release}) // returns at once
    err := run(os.Args[1:])                                              // the tool's own work
    session.Finish()                                                     // prints the notice, if any
    if err != nil {
        os.Exit(1)
    }
}

Once the repository has a published release, build it and run it:

go build -o tool .                                    # version stays "dev"
./tool update                                         # refuses: cannot update a development build
go build -ldflags "-X main.version=v0.0.1" -o tool .  # stands in for an old release
./tool                                                # in a terminal: one line naming the newer release
./tool update                                         # downloads, verifies and swaps ./tool in place
./tool update                                         # proves it: From and To match, applied: false

A version that is not semver refuses. Update returns ErrDevBuild for an empty version, dev, or anything else that does not parse as semver. It returns before any request is made. There is nothing meaningful to compare, and an update would silently discard whatever local build was in place.

The background check is stricter. It also skips a pre-release, and a Go pseudo-version such as v1.6.1-0.20260724161156-2c04703+dirty, which is valid semver. goselfupdate.IsReleaseVersion is that stricter test. A caller that wants update to refuse those builds too checks it before calling Update.

Windows cannot overwrite a running executable. There Update renames the running binary to <name>.old and moves the release into its place. The old copy cannot be deleted while the process runs, so CleanupOldBinary removes it on the next start. It reports no error when there is nothing to remove, so it is safe on every platform and every start.

No secret is needed. The GitHub source authenticates with whatever gh auth token prints, and sends the request anonymously when that fails. An anonymous request works against a public repository. A private one needs a token from one of the four sources under Authentication below, and a logged-in gh is one of them.

Update — find the asset by name, verify it, then swap the binary in one rename

goselfupdate.Check(ctx, config)                             // is a newer release out? downloads and writes nothing
goselfupdate.Update(ctx, config)                            // installs it over the running executable
goselfupdate.UpdateTo(ctx, config, path)                    // installs it over the binary at path
goselfupdate.Changelog(ctx, config, result.From, result.To) // the commit subjects between two versions
goselfupdate.CleanupOldBinary()                             // removes what a Windows update displaced

Update runs five steps and stops at the first failure:

  1. Reads the latest release from the GitHub API.
  2. Compares it with the running version by semantic version precedence. A leading v is optional on both.
  3. Picks the asset that names the running GOOS and GOARCH.
  4. Verifies its SHA-256 against the release's checksums file.
  5. Extracts the binary and replaces the running executable.

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 release's binary is staged in the target's own directory, flushed to disk, and renamed into place. An interrupted update cannot leave a half-written executable. On Unix a running process holds its executable by inode, so the rename is safe mid-execution. Symbolic links are resolved first, so an update rewrites the real file rather than replacing a link with a binary.

Changelog asks GitHub's compare endpoint for the commits between two tags and returns each one's subject line. A custom Source that does not implement Changeloger returns none.

Everything beyond Owner, Repo, Binary and Version has a working default. HTTPClient defaults to a 60-second timeout. AllowPrerelease includes prereleases when selecting the newest version. Source and Verifier are the extension points, covered under Scope. GitHub Enterprise works by passing a GitHubSource whose APIBase is its /api/v3 root as Source. A hand-built source carries its own HTTPClient, TagPrefix and token settings, and the matching Config fields go unused.

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 One asset whose name contains checksum, in sha256sum format, with or without the binary-mode * or a leading path
Tags v1.2.3 or 1.2.3, or cli/v1.2.3 in a repository publishing several components

goreleaser's defaults satisfy all of this with no configuration. Its split checksum mode, one .sha256 file per artifact, is not read.

Repositories publishing more than one component

A repository that releases several things gives each its own tag prefix, such as cli/v1.2.3 and 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:     "you",
    Repo:      "project",
    Binary:    "tool",
    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. The search reads one page of the list at the API's maximum page size, so a component whose newest release sits deeper than that is reported as ErrNoRelease.

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

Authentication — on by default, resolved once by the GitHub source

Configure nothing. GitHubSource runs gh auth token when its first request is about to be made, and sends what it prints. The credential is resolved once and reused for the source's lifetime.

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

TokenFunc is called only when a request is about to be made. A credential that costs a keychain prompt or a subprocess is paid for only on the invocations that reach the network.

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

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

autoupdate — a daily check that prints one line and never installs

session := autoupdate.Start(ctx, config)    // runs the gate; if it passes, starts the check and returns
outcome := session.Finish()                 // waits for the check, bounded by Timeout, then prints
enabled, skip := autoupdate.Enabled(config) // whether this tool checks at all, and why not; ignores the interval
path := autoupdate.StatePath("tool")        // where this machine's state for the tool lives

At most once per interval, a day by default, the check asks for the latest release. If it is newer, one line goes to stderr after your command's own output:

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

Start returns at once and the check runs beside the command. A command that outlasts the check pays nothing for it. One that finishes first waits in Finish, at most Config.Timeout, which is five seconds by default. Then the check is abandoned. Within the interval the gate declines before any request, and its only I/O is reading the state file.

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.

The check is skipped, and nothing is printed, when any of these hold. The gate tests them in this order:

Condition Why
Config.Suppress is true The caller marked a command that must never check, such as an update command or a shell-completion callback
NO_AUTO_UPDATE or <TOOL>_NO_AUTO_UPDATE is set Opted out
The version is not a plain X.Y.Z, with or without the v A development build, a pre-release or a Go pseudo-version
stdout or stderr is not a terminal tool list > out 2>&1 must stay clean
CI, BUILD_NUMBER, RUN_ID, GITHUB_ACTIONS or CODESPACES is set Not a human
Checked within the interval One request per interval, not per invocation

Every variable in the table counts when it is present, whatever its value. NO_AUTO_UPDATE=0 disables the check, the same way NO_COLOR works. <TOOL> is Config.Tool, which defaults to Binary, uppercased with - turned into _. A tool named my-tool reads MY_TOOL_NO_AUTO_UPDATE.

The interval is Config.Interval, else <TOOL>_AUTO_UPDATE_INTERVAL, else AUTO_UPDATE_INTERVAL, else 24 hours. Both variables take 30s, 45m, 12h, 7d or a bare number of seconds. A value that does not parse is ignored.

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-<machine>.json, and Config.StateDir overrides the root. It is written atomically with mode 0600. 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. A check the gate declines writes nothing, so a missing state file shows the network was never touched.

<machine> is the bare lowercased hostname, with any domain dropped, or unknown when the hostname cannot be read. It is in the name because both fields the file carries describe one machine: the version installed here, and the instant this box last checked. A state directory shared between machines, by a file syncer or a network home directory, otherwise has two writers on one path. It then reports whichever wrote last as the state of all of them. autoupdate.Machine returns the name this process writes under.

The Python and bash siblings, pyselfupdate and bashselfupdate, write the same schema under the same name. One reader can glob ~/.local/state/*/autoupdate-*.json and read every tool's state.

With cobra — goclikit carries the command and the suppression list

root.AddCommand(goclikit.UpdateCommand(release))                       // adds `tool update` and `tool update --check`
err := goclikit.Execute(ctx, root, autoupdate.Config{Update: release}) // runs root beside the background check
$ tool update --check
✓ tool update available: v1.6.0 → v1.6.1

$ tool update
✓ tool updated: v1.6.0 → v1.6.1

Changes:
  • fix(download): retry a transfer the server cut short
  • docs: describe the config file's search order

Execute sets Suppress for the update command under whatever name it was given, and for version, completion and its subcommands, help, cobra's shell-completion callbacks, --help, and a command line cobra cannot resolve. The completion callback is the one that matters most: it runs on every TAB press.

The command takes no alias. A second verb such as upgrade beside update leaves its reader guessing which one is real.

Errors — one sentinel per failure mode, so callers branch with errors.Is

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
}

The full set is ErrDevBuild, ErrNoRelease, ErrNoAsset, ErrAmbiguousAsset, ErrNoChecksums, ErrChecksumMismatch, ErrBinaryNotFound and ErrInvalidConfig.

Security — checksums catch a bad download, not a compromised account

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.

Only an archive entry's base name is compared, and archive paths never build an output path. An entry naming ../../etc/passwd has nothing to escape into.

This package does not link golang.org/x/crypto/openpgp. That package is unmaintained and carries a permanent advisory, GO-2026-5932, with no fixed version. A project using this library inherits no advisory from it.

Scope — GitHub and checksums built in, everything else behind two interfaces

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 — another library for GitLab, Gitea, minisign or binary patching

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

Use this one to install goreleaser archives from GitHub releases with nothing beyond the standard library. Use creativeprojects/go-selfupdate for GitLab or Gitea.

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 `update` command wired to this package, see github.com/datapointchris/goclikit. Nothing here knows what a command-line parser is.

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 whose version does not parse as semver, such as one with no version injected, refuses to update with ErrDevBuild. There is no meaningful version to compare, and an update would silently discard whatever local build was in place. A Go pseudo-version such as v1.6.1-0.20260724161156-2c04703+dirty is valid semver, so Update accepts it. A caller that must refuse those builds too checks IsReleaseVersion first.

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. Two machines checking on a timer are enough to hold that pool at zero for whole hours, and then every anonymous caller behind the same address is refused.

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 version is empty, "dev", or
	// anything else that does not parse as semver. With nothing to compare, an
	// update would discard a local build for a release that may be older. A Go
	// pseudo-version parses as semver and is not refused here;
	// [IsReleaseVersion] is the test that rejects it.
	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. A source that does not implement it simply produces no changelog, and Changelog returns none.

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 also names
	// the tool in whatever a caller prints about the update.
	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.

Jump to

Keyboard shortcuts

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