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 ¶
- Constants
- Variables
- func Canonical(version string) string
- func Changelog(ctx context.Context, cfg Config, fromTag, toTag string) ([]string, error)
- func CleanupOldBinary() error
- func IsReleaseVersion(text string) bool
- func IsValidVersion(text string) bool
- type Asset
- type Changeloger
- type ChecksumVerifier
- type Config
- type GitHubSource
- type NoVerification
- type Release
- type Result
- type Source
- type Verifier
Examples ¶
Constants ¶
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.
const DefaultTimeout = 60 * time.Second
DefaultTimeout bounds an update when Config.HTTPClient is not supplied.
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.
const TokenCommandEnv = "GITHUB_TOKEN_COMMAND"
TokenCommandEnv names the command that produces a GitHub credential.
Variables ¶
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
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 ¶
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
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
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.
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)
}
}
Output:
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)
}
}
Output:
func (*GitHubSource) Changelog ¶
Changelog implements Changeloger, returning the subject line of every commit between two tags.
func (*GitHubSource) Download ¶
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.
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 ¶
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)
}
}
Output:
func Update ¶
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)
}
}
Output:
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)
}
}
Output:
func UpdateTo ¶
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 ¶
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.
Source Files
¶
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. |