Documentation
¶
Overview ¶
Package selfupdate is to GitHub Releases what internal/claudecode is to Claude Code: the one place that knows the redirect shape of {base}/latest, GoReleaser's asset naming and checksums.txt format, and the minisign signature file format. It knows nothing about the daemon's HTTP surface, its store, or tmux — internal/server owns the poller, apply serialisation and the wire messages; cmd/musterd owns the -update flag, the install classification at startup, and the in-place re-exec (kb:adr/update-release-knowledge-in-selfupdate-package).
Index ¶
- Constants
- Variables
- func AcquireLock(exeDir string) (release func(), err error)
- func Apply(ctx context.Context, opts Options) error
- func AssetName(ver, goos, goarch string) string
- func ChecksumFor(checksums []byte, asset string) ([32]byte, error)
- func DescribeApplyFailure(err error) string
- func DescribeCheckFailure(err error) string
- func DownloadURL(base, tag, asset string) string
- func LatestTag(ctx context.Context, client *http.Client, base string) (string, error)
- func ProbeVersion(ctx context.Context, exePath string) (string, error)
- func PublicKey() []byte
- func ReleaseTag(version string) string
- func SHA256Of(data []byte) [32]byte
- func VerifyChecksums(pubKey, checksums, minisig []byte) error
- func WritableDir(dir string) error
- type DownloadError
- type FetchStatusError
- type Install
- type Kind
- type MissingSignatureError
- type Options
- type Phase
- type StatusError
- type TagError
- type TransportError
- type Version
Constants ¶
const ApplyTimeout = 120 * time.Second
ApplyTimeout bounds one whole Apply call — three small files and one archive must never hang the daemon indefinitely.
const CheckTimeout = 10 * time.Second
CheckTimeout bounds one LatestTag call — a hung/slow GitHub must never stall the daemon's check loop.
const HomebrewRemedy = "installed by Homebrew — run brew upgrade musterd"
HomebrewRemedy is the one-line remedy shown for a homebrew install (kb:anchor/ws.update "remedy"), and `musterd -update`'s stderr message for the same kind. The unmanaged remedy is composed per-install by unwritableRemedy/gitTreeRemedy instead of a fixed constant, since it names the resolved path and the concrete reason (kb:adr/update-remedy-names-path-and-cause).
const ProbeVersionTimeout = 5 * time.Second
ProbeVersionTimeout bounds `<exe> -version` — a hung swapped binary must never wedge the update manager's tick loop.
const VersionLinePrefix = "musterd "
VersionLinePrefix is the literal prefix of musterd's own `-version` output, immediately followed by the version itself: cmd/musterd's -version printer and versionPattern below are the format's only two consumers, and both build off this one declaration rather than each spelling "musterd " by hand.
Variables ¶
var ( ErrBadSignature = errors.New("signature on checksums.txt did not verify — the release may be tampered with; nothing was installed") ErrChecksumMismatch = errors.New("downloaded archive's checksum does not match the signed checksums.txt; nothing was installed") ErrNoChecksumLine = errors.New("checksums.txt has no line for this platform's asset; nothing was installed") )
ErrBadSignature, ErrChecksumMismatch and ErrNoChecksumLine are VerifyChecksums'/ ChecksumFor's sentinel refusals — every message doubles as the UI status line's failure text ("Update failed: <error>"), so each names both the fact and the remedy in one sentence.
var ErrInProgress = errors.New("another musterd update is in progress")
ErrInProgress is AcquireLock's failure when another process already holds the update lock — cross-process serialisation so `musterd -update` racing the daemon's own apply can't run two applies at once (kb:adr/update-install-kinds-decide-who-may-apply).
Functions ¶
func AcquireLock ¶
AcquireLock takes a non-blocking exclusive flock on <exeDir>/.musterd-update.lock, returning a release func the caller must call exactly once when its apply is done. Fails fast with ErrInProgress — never blocks — when another process already holds it.
func Apply ¶
Apply downloads the release archive plus checksums.txt/checksums.txt.minisig for runtime.GOARCH, verifies them, extracts the musterd member, and installs it over opts.ExePath, reporting phases via opts.Progress. Any failure returns before installBinary is ever reached, so opts.ExePath stays byte-identical to before the call (kb:adr/update-trust-root-minisign-signed-checksums) — including a failure during installBinary itself, which removes its own temp file on every error path.
func AssetName ¶
AssetName is GoReleaser's archive name_template for the musterd build: ver is the bare version (Version.String(), no leading "v"); goos is always "darwin" in practice — callers pass it explicitly rather than this package hardcoding runtime.GOOS, since that constant belongs to the caller's own build context.
func ChecksumFor ¶
ChecksumFor returns the SHA-256 recorded in an already-verified checksums.txt for asset, matched by exact basename against GoReleaser's "<hex> <asset>" lines — one or two spaces both accepted, since strings.Fields splits on any run of whitespace.
func DescribeApplyFailure ¶ added in v0.18.4
DescribeApplyFailure composes the one-sentence, URL-free `apply.error` wire text from Apply's returned error. A download-shaped failure (*DownloadError/*MissingSignatureError, both introduced for this composition) is classified here into its own wire sentence, distinct from and never reusing that type's own log-only Error() text. Anything else — a verification refusal or install error (verify.go's pre-existing sentinels, or an installBinary failure) — falls through to err.Error() unchanged: those sentinels were already written as complete, static UI sentences (internal/selfupdate/CLAUDE.md's "Sentinel error text is user-facing" gotcha), so they need no classification here. A new selfupdate failure that needs to name something the caller only learns at the call site (a path, a status code, a cause word) takes the typed-error-plus-classification shape this function and DescribeCheckFailure both use; only a failure whose entire sentence is static text known in advance may stay a plain sentinel.
func DescribeCheckFailure ¶ added in v0.18.4
DescribeCheckFailure composes the one-sentence, URL-free `check_failed` wire message for a failed release check from LatestTag/CheckNewer's returned error, un-prefixed and unwrapped — checkAvailability returns that error as-is, never adding its own wrapper, so this function's fallback branch (an error that isn't one of the three typed classes) never has to strip a caller-added prefix back off before adding its own. The full error (still carrying its wrapped chain, from wherever the caller itself logs it) is what the daemon log keeps (kb:adr/update-failure-one-sentence-chain-in-log) — this function only ever produces the short wire text.
func DownloadURL ¶
DownloadURL builds a release download URL: {base}/download/{tag}/{asset} — the same base LatestTag resolved against.
func LatestTag ¶
LatestTag resolves {base}/latest's redirect to a release tag (kb:adr/release-latest-resolved-via-redirect-not-api): a HEAD request (the redirect is unmetered, unlike the rate-limited REST API scripts/install.sh also avoids), following no redirects itself — the Location header is read directly off the 3xx response. The Location may be absolute or path-relative (both measured against github.com's real redirect shape); only its last path segment is used, and it must parse as a release tag. Every failure returns one of TransportError/StatusError/TagError, or a plain error for anything else (e.g. no Location header) — DescribeCheckFailure (failure.go) classifies these into the wire text (kb:adr/update-failure-one-sentence-chain-in-log).
func ProbeVersion ¶
ProbeVersion runs `exePath -version`, never a $PATH shim, and parses the leading "musterd v?X.Y.Z" from its output. ctx should already carry ProbeVersionTimeout; a non-parseable or non-release version (e.g. "musterd dev") is an error, exactly like a probe that fails or hangs.
func PublicKey ¶
func PublicKey() []byte
PublicKey returns the compiled-in minisign public key file's raw bytes — the trust root every production verification uses (kb:adr/update-trust-root-minisign-signed-checksums). -update-public-key-file overrides this with a file's contents instead (a test seam: E2E signs with its own throwaway key; production always uses this embedded one).
func ReleaseTag ¶ added in v0.18.3
ReleaseTag is a release Version's tag form, "v" + Version.String() — the one place the tag⇄version mapping is spelled (kb:adr/update-release-knowledge-in-selfupdate-package puts every release-format fact here). Used to rebuild a tag from a Version a caller held onto past the moment it resolved one (internal/server's RequestApply, applying against the version it displayed at check time, not a re-fetched tag).
func SHA256Of ¶
SHA256Of hashes data — used to compare a downloaded archive against ChecksumFor's result before extraction.
func VerifyChecksums ¶
VerifyChecksums verifies minisig (a checksums.txt.minisig file's raw bytes) as a signature over checksums (checksums.txt's raw bytes), against pubKey (a compiled-in minisign public key file's raw bytes). Both minisign signature modes verify transparently through the one call: minisign.Verify re-hashes the message with BLAKE2b-512 internally whenever the parsed signature's algorithm says prehashed (`ED`), and compares directly for the legacy `Ed` mode — both signature modes are accepted (kb:adr/update-trust-root-minisign-signed-checksums) with no branching here.
func WritableDir ¶
WritableDir is the production `access` func for Classify/Reclassify: it reports whether dir is writable by the current user by attempting to create and immediately remove a probe file — Go's stdlib has no direct "is this writable" query.
Types ¶
type DownloadError ¶ added in v0.18.4
DownloadError marks a failed download of one release asset — classified by failure.go's DescribeApplyFailure into the "couldn't download <file> (<cause>); nothing was installed" wire sentence (kb:adr/update-failure-one-sentence-chain-in-log). Asset is the file's own name (e.g. "checksums.txt", or the archive's own name); Err is the fetch failure, a *FetchStatusError or a transport error.
func (*DownloadError) Error ¶ added in v0.18.4
func (e *DownloadError) Error() string
func (*DownloadError) Unwrap ¶ added in v0.18.4
func (e *DownloadError) Unwrap() error
type FetchStatusError ¶ added in v0.18.4
type FetchStatusError struct{ Status int }
FetchStatusError is a non-200 response to one of Apply's three small downloads — its "status <code>" cause classified by failure.go's fetchCause.
func (*FetchStatusError) Error ¶ added in v0.18.4
func (e *FetchStatusError) Error() string
type Install ¶
Install is Classify's/Reclassify's result: the kind, the resolved path it was computed from, and — for homebrew/unmanaged only — the remedy to display (nil/"" otherwise).
func Classify ¶
func Classify(version, exePath string, env func(string) string, home string, access func(dir string) error) Install
Classify determines the install kind from the resolved executable path at startup (kb:adr/update-install-kinds-decide-who-may-apply).
- version is the running binary's version string (as built) — anything that doesn't parse via ParseRelease is a dev build, checked before path is examined at all.
- exePath must already be resolved through os.Executable + filepath.EvalSymlinks.
- env reads one environment variable (production: os.Getenv; tests: a fake map).
- home is the user's home directory (production: os.UserHomeDir()).
- access reports whether dir is writable by the current user: nil means writable, a non-nil error means not (production: WritableDir). A nil access func is treated as "writable" (a defensive default, never exercised by cmd/musterd's real wiring).
dev and homebrew are decided here and never re-derived; installer/unmanaged are handed off to Reclassify, the same function a release check calls again later (kb:adr/update-install-rechecked-on-every-check).
func Reclassify ¶ added in v0.18.4
Reclassify re-derives only the installer/unmanaged half of an install classification: the write probe and the git-tree walk Classify itself uses at startup for every non-dev, non-homebrew install. Called again at the start of every release check (kb:adr/update-install-rechecked-on-every-check) — dev and homebrew are decided once, at startup, and never reach this function a second time.
type Kind ¶
type Kind string
Kind is the daemon's install classification (kb:adr/update-install-kinds-decide-who-may-apply). Dev and homebrew are decided once at startup and fixed for the daemon's life; installer and unmanaged are re-derived at the start of every release check (kb:adr/update-install-rechecked-on-every-check, kb:anchor/ws.update "install").
type MissingSignatureError ¶ added in v0.18.4
type MissingSignatureError struct{ Status int }
MissingSignatureError is a 404 on checksums.txt.minisig specifically, checked ahead of the generic DownloadError so failure.go's DescribeApplyFailure can give a missing signature its own distinct wire sentence rather than the generic download-failure one. Error() is the log-only text; failure.go owns the wire sentence (§ Design "One owner per concept" — this package has one home per user-facing failure sentence, this type's own Error() is deliberately not it).
func (*MissingSignatureError) Error ¶ added in v0.18.4
func (e *MissingSignatureError) Error() string
type Options ¶
type Options struct {
// Client makes the three downloads. Nil defaults to http.DefaultClient.
Client *http.Client
// Base is the update base URL (e.g. https://github.com/Zalaras/muster/releases).
Base string
// Tag is the release tag to install, e.g. "v0.11.0".
Tag string
// ExePath is the resolved (os.Executable + filepath.EvalSymlinks) real path of the
// running binary — the temp file is written beside it and renamed over it.
ExePath string
// PubKey is the compiled-in (or -update-public-key-file-overridden) minisign public
// key file's raw bytes.
PubKey []byte
// Progress, if non-nil, is called synchronously as each phase begins.
Progress func(Phase)
}
Options configures one Apply call.
type Phase ¶
type Phase string
Phase is one step of an Apply call, mirrored on the wire as `update.apply.phase` (kb:anchor/ws.update).
const ( PhaseIdle Phase = "idle" PhaseDownloading Phase = "downloading" PhaseVerifying Phase = "verifying" PhaseInstalling Phase = "installing" PhaseRestarting Phase = "restarting" PhaseFailed Phase = "failed" PhaseDone Phase = "done" )
The seven phases Apply/the update manager walk through, in order (kb:anchor/ws.update): Idle is never reported by Apply itself (it's the pre-apply/at-rest phase the manager starts at).
type StatusError ¶ added in v0.18.4
type StatusError struct{ Status int }
StatusError is a non-redirect response to the latest-release HEAD — classified into the "answered <status>, not a redirect" wire class.
func (*StatusError) Error ¶ added in v0.18.4
func (e *StatusError) Error() string
type TagError ¶ added in v0.18.4
type TagError struct{ Tag string }
TagError is a redirect tag that doesn't parse as a release version — classified into the "is not a release version" wire class.
type TransportError ¶ added in v0.18.4
type TransportError struct{ Err error }
TransportError marks a failed network round trip to the release host — classified by failure.go's DescribeCheckFailure into the "couldn't reach the release host" wire class, as opposed to a redirect that came back but failed to parse (StatusError/TagError) or a request that failed to build at all.
func (*TransportError) Error ¶ added in v0.18.4
func (e *TransportError) Error() string
func (*TransportError) Unwrap ¶ added in v0.18.4
func (e *TransportError) Unwrap() error
type Version ¶
type Version struct {
Major, Minor, Patch int
}
Version is a parsed strict release version — MAJOR.MINOR.PATCH, nothing else.
func CheckNewer ¶ added in v0.18.3
func CheckNewer(ctx context.Context, client *http.Client, base, running string) (latest Version, tag string, newer bool, err error)
CheckNewer answers "is a newer release available than running": resolve the latest release tag, parse it, and compare against running (kb:adr/update-release-knowledge-in-selfupdate-package). tag is LatestTag's own resolved string, returned alongside latest so a caller that applies immediately (cmd/musterd's `-update`) can pass GitHub's own tag straight to Apply instead of reconstructing one with ReleaseTag, which is only exact for a tag already shaped "v"+Version.String()`. err is non-nil only when the latest tag could not be resolved or parsed. running failing to parse (a dev build) reports newer=false with no error — every caller only reaches this after establishing its own version is a release build (Install.Kind != KindDev), so that branch is a defensive default, not a distinct error condition either caller needs to surface.
func ParseRelease ¶
ParseRelease parses s as a strict release version: optional leading "v", then exactly three dot-separated non-negative integers. Reports false for anything else, including "dev" and any pre-release/build suffix.