update

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package update implements §15: `pay update-self`, the three-state verification outcome, managed-install detection and the detached apply.

Nothing here reads the environment or the clock directly: the release API base URL, the HTTP client, the clock and every path are fields on Updater, so the whole package is testable against an httptest.Server and a t.TempDir().

Index

Constants

View Source
const (
	// StateFileName lives in the state directory (§4.1).
	StateFileName = "update-state.json"
	// StatePerm is the state file's mode.
	StatePerm fs.FileMode = 0o600
	// CheckInterval is §15's 24 h throttle between background checks.
	CheckInterval = 24 * time.Hour
	// StateVersion is the state file's schema version.
	StateVersion = 1
)
View Source
const (
	// DefaultRepo is the release repository.
	DefaultRepo = "KLIXPERT-io/pay-cli"
	// DefaultAPIBase is the GitHub API root; PAY_UPDATE_URL overrides the
	// download root for private mirrors and air-gapped installs.
	DefaultAPIBase = "https://api.github.com"
	// CheckTimeout bounds `--check`'s HTTP call (§15.1).
	CheckTimeout = 2 * time.Second
	// ApplyDeadline is the total deadline for an explicit `--apply`.
	ApplyDeadline = 120 * time.Second
)
View Source
const DefaultIdentityRegexp = `^https://github\.com/KLIXPERT-io/pay-cli/\.github/workflows/.+$`

DefaultIdentityRegexp matches this repository's GitHub Actions workflow identity, which is what §16.1's cosign step signs with.

View Source
const DefaultIssuer = "https://token.actions.githubusercontent.com"

DefaultIssuer is GitHub's OIDC issuer.

View Source
const NoRecurseEnv = "PAY_NO_UPDATE=1"

NoRecurseEnv is the variable the spawned child must see so it does not spawn a child of its own.

Variables

View Source
var ApplyArgs = []string{"update-self", "--apply", "--quiet"}

ApplyArgs is the command line the detached child runs.

View Source
var ErrLocked = errors.New("update: another update is already in progress")

ErrLocked is returned when another `pay` process is already updating.

Functions

func ArchiveExt

func ArchiveExt(goos string) string

ArchiveExt is .zip on Windows and .tar.gz everywhere else, matching goreleaser's format_overrides.

func AssetName

func AssetName(version, goos, goarch string) string

AssetName builds the release archive's file name for a target platform.

func BinaryName

func BinaryName(goos string) string

BinaryName is the executable's name inside the archive.

func CleanupOldBinary

func CleanupOldBinary(dest string)

CleanupOldBinary is a no-op on unix; it exists so callers do not need build tags for §19.13's Windows rename-self dance.

func Digest

func Digest(data []byte) string

Digest is the lowercase hex SHA-256 of data.

func Newer

func Newer(current, candidate string) bool

Newer reports whether candidate is a strictly newer release than current. A non-semver current version (a dev build, "unknown") counts as older, so a locally built binary can still be updated deliberately.

func ParseChecksums

func ParseChecksums(b []byte) map[string]string

ParseChecksums parses a goreleaser checksums.txt ("<sha256> <filename>").

func ResolveBinary

func ResolveBinary() (string, error)

ResolveBinary returns the running binary's real path, following symlinks so the swap replaces the file rather than the link.

func SaveState

func SaveState(path string, s State) error

SaveState writes the state file atomically with mode 0600.

func ShouldAutoApply

func ShouldAutoApply(s State, autoEnabled bool, current string, disabled bool) bool

ShouldAutoApply is the §15.1 implicit-apply predicate, evaluated at the top of main(). When it is true the caller spawns the DETACHED child and returns immediately; the swapped binary takes effect on the next invocation.

func SpawnDetached

func SpawnDetached(exe string, args, env []string) error

SpawnDetached starts exe fully detached from this process: no shared stdio, its own session (unix) or process group (Windows), and the handle released immediately.

This is §15.1's implicit apply. It is bounded at the fork — no network in the foreground, no syscall.Exec, no fall-through — and the swapped binary takes effect on the NEXT invocation. An in-process swap would leave the running image executing the old command surface while reporting the new version.

Types

type ApplyResult

type ApplyResult struct {
	Updated      bool         `json:"updated"`
	FromVersion  string       `json:"from_version"`
	ToVersion    string       `json:"to_version,omitempty"`
	Asset        string       `json:"asset,omitempty"`
	Path         string       `json:"path,omitempty"`
	Verification Verification `json:"verification"`
	Managed      Managed      `json:"managed"`
	// Skipped explains a no-op ("already up to date", "another update is in
	// progress", "managed install").
	Skipped string `json:"skipped,omitempty"`
}

ApplyResult is what `pay update-self --apply` reports.

type CheckResult

type CheckResult struct {
	CurrentVersion  string       `json:"current_version"`
	LatestVersion   string       `json:"latest_version,omitempty"`
	UpdateAvailable bool         `json:"update_available"`
	Asset           string       `json:"asset,omitempty"`
	Verification    Verification `json:"verification,omitempty"`
	Managed         Managed      `json:"managed"`
	CheckedAt       time.Time    `json:"checked_at"`
}

CheckResult is what `pay update-self --check` reports.

type Env

type Env struct {
	GOPATH string
	GOBIN  string
	Home   string
	GOOS   string
}

Env is the small slice of the environment managed-install detection needs. It is passed in rather than read, so the detection is a pure function.

type Managed

type Managed struct {
	// Managed is true when a manager owns this binary.
	Managed bool `json:"managed"`
	// Manager names it ("go install", "homebrew", "nix", …).
	Manager string `json:"manager,omitempty"`
	// Hint is the command the user should run instead.
	Hint string `json:"hint,omitempty"`
}

Managed says whether the running binary was installed by a package manager, in which case PayCLI must not swap it (§15.5).

func DetectManaged

func DetectManaged(binPath string, env Env) Managed

DetectManaged classifies the install path. The path should already be symlink-resolved (ResolveBinary does that).

type Outcome

type Outcome string

Outcome is §15.3's three-state verification result. There are exactly three: collapsing "no evidence" and "evidence that disagrees" into one boolean is what makes a compromised release installable.

const (
	// OutcomeVerified — a signature or attestation is present and matches.
	OutcomeVerified Outcome = "verified"
	// OutcomeUnverifiable — no evidence either way: cosign is absent and the
	// attestation API returned non-200 or timed out. Proceed with a loud
	// warning unless PAY_UPDATE_STRICT=1.
	OutcomeUnverifiable Outcome = "unverifiable"
	// OutcomeFailed — a checksum, signature or attestation IS present and does
	// not match. Always abort; PAY_UPDATE_STRICT cannot relax this.
	OutcomeFailed Outcome = "verification_failed"
)

type Signer

type Signer struct {
	// LookPath finds cosign. nil uses exec.LookPath.
	LookPath func(string) (string, error)
	// Run executes cosign. nil uses os/exec.
	Run func(ctx context.Context, name string, args ...string) error
	// HTTP is the client used for the attestation API.
	HTTP *http.Client
	// APIBase is the GitHub API base URL.
	APIBase string
	// Repo is "owner/name".
	Repo string
	// Token is GH_TOKEN, if any.
	Token string
	// Identity and Issuer pin the expected signing identity for cosign.
	Identity string
	Issuer   string
}

Signer verifies checksums.txt itself: cosign when it is on PATH, otherwise the GitHub attestation API. Both hooks are injectable so tests never shell out or hit the network.

func (Signer) VerifySignature

func (s Signer) VerifySignature(ctx context.Context, checksums, sig, pem []byte) Verification

VerifySignature checks checksums.txt against its cosign signature, falling back to the GitHub attestation API.

sig and pem may be empty: that is the "release was not signed" case, which is unverifiable rather than failed. A signature that IS present and does not verify is failed.

type State

type State struct {
	Version int `json:"version"`
	// LastCheck is when the release API was last consulted; it drives the 24 h
	// throttle.
	LastCheck time.Time `json:"last_check,omitempty"`
	// CurrentVersion is the binary that performed the check.
	CurrentVersion string `json:"current_version,omitempty"`
	// PendingVersion is a newer release that has been seen but not applied.
	// The implicit apply path consumes it (§15.1).
	PendingVersion string `json:"pending_version,omitempty"`
	// Verification is the last known outcome for PendingVersion.
	Verification Outcome `json:"verification,omitempty"`
	// Notified records that the user has already been told about
	// PendingVersion, so the notice is printed once per release.
	Notified bool `json:"notified,omitempty"`
	// LastApply is when an apply last completed.
	LastApply time.Time `json:"last_apply,omitempty"`
	// LastError is the last apply or check failure, for `pay doctor`.
	LastError string `json:"last_error,omitempty"`
}

State is update-state.json.

func LoadState

func LoadState(path string) (State, error)

LoadState reads the state file. A missing file is the zero state. A corrupt one is ALSO the zero state and not an error: update bookkeeping must never be the reason a command fails, and the next write repairs it.

func (State) CheckDue

func (s State) CheckDue(now time.Time) bool

CheckDue reports whether the 24 h throttle has expired.

func (State) HasPending

func (s State) HasPending(current string) bool

HasPending reports whether a newer version than current is recorded and is not known to have failed verification. A failed verification is never applied, implicitly or otherwise (§15.3).

type Updater

type Updater struct {
	// HTTP is the client for the release API and downloads.
	HTTP *http.Client
	// APIBase is the GitHub API root (a field, not a constant, so tests point
	// it at an httptest.Server).
	APIBase string
	// DownloadBase is the release asset root. Empty derives GitHub's.
	// PAY_UPDATE_URL sets it.
	DownloadBase string
	// Repo is "owner/name".
	Repo string
	// Channel is "stable" (the /releases/latest endpoint) or anything else,
	// which lists /releases and takes the newest, prereleases included.
	Channel string
	// CurrentVersion is the running binary's version.
	CurrentVersion string
	// BinaryPath is the symlink-resolved path that will be replaced.
	BinaryPath string
	// StatePath is update-state.json's location.
	StatePath string
	// LockPath is the apply lock's location. Empty derives it from StatePath.
	LockPath string
	// Token is GH_TOKEN.
	Token string
	// Strict is PAY_UPDATE_STRICT=1: refuse an unverifiable release.
	Strict bool
	// Force applies even when the version is not newer.
	Force bool
	// AllowManaged applies even on a package-managed install.
	AllowManaged bool
	// OS and Arch name the artefact; empty uses runtime's.
	OS, Arch string
	// Now is the injected clock.
	Now func() time.Time
	// Stderr receives the loud unverifiable warning. Nil discards it.
	Stderr io.Writer
	// Signer verifies checksums.txt.
	Signer Signer
	// Timeout is the apply deadline; 0 uses ApplyDeadline.
	Timeout time.Duration
	// CheckTimeout bounds the release lookup; 0 uses CheckTimeout.
	CheckTimeout time.Duration
	// VerifyOnCheck makes --check report the three-state outcome by verifying
	// the signature over checksums.txt (no artefact download).
	VerifyOnCheck bool
	// contains filtered or unexported fields
}

Updater performs §15's check, download, verify and swap.

func (*Updater) Apply

func (u *Updater) Apply(ctx context.Context) (*ApplyResult, error)

Apply downloads, verifies and swaps the binary under a total deadline.

On success the caller exits 0 having done only the update: it never re-execs the new binary, so the running image's command surface always matches the version it reports (§15.1).

func (*Updater) Check

func (u *Updater) Check(ctx context.Context) (*CheckResult, error)

Check asks the release API for the newest version, records it in the state file and returns the comparison. It is bounded by CheckTimeout so it can be called from a fast command without an unbounded stall.

func (*Updater) SpawnApply

func (u *Updater) SpawnApply(env []string) error

SpawnApply starts the detached `pay update-self --apply` child for the implicit path. env must already contain PAY_NO_UPDATE=1 so the child cannot recurse into spawning another one.

func (*Updater) WithEnv

func (u *Updater) WithEnv(env Env) *Updater

WithEnv supplies the environment slice managed-install detection needs.

type Verification

type Verification struct {
	Outcome Outcome `json:"outcome"`
	// Method is "checksum", "cosign" or "attestation".
	Method string `json:"method,omitempty"`
	// Reason explains an unverifiable or failed outcome in one line.
	Reason string `json:"reason,omitempty"`
	// Expected and Computed are the digests, printed on a mismatch.
	Expected string `json:"expected,omitempty"`
	Computed string `json:"computed,omitempty"`
}

Verification is one verification step's result.

func Combine

func Combine(results ...Verification) Verification

Combine folds several steps into the overall outcome: any failure dominates; otherwise every step must be verified for the whole to be verified.

func VerifyChecksum

func VerifyChecksum(assetName string, artifact, checksums []byte) Verification

VerifyChecksum compares the artefact against checksums.txt.

A missing entry is UNVERIFIABLE (no evidence), a mismatching one is FAILED (evidence that disagrees).

func (Verification) Enforce

func (v Verification) Enforce(strict bool) error

Enforce turns the outcome into the §15.3 behaviour. It returns nil when the update may proceed.

A failed verification always aborts with exit 1 update_verification_failed, strict or not: a mismatching signature is positive evidence of exactly the compromised release this check exists to defend against.

func (Verification) WarnLine

func (v Verification) WarnLine() string

WarnLine is the loud stderr line printed for an unverifiable outcome.

Jump to

Keyboard shortcuts

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