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
- Variables
- func ArchiveExt(goos string) string
- func AssetName(version, goos, goarch string) string
- func BinaryName(goos string) string
- func CleanupOldBinary(dest string)
- func Digest(data []byte) string
- func Newer(current, candidate string) bool
- func ParseChecksums(b []byte) map[string]string
- func ResolveBinary() (string, error)
- func SaveState(path string, s State) error
- func ShouldAutoApply(s State, autoEnabled bool, current string, disabled bool) bool
- func SpawnDetached(exe string, args, env []string) error
- type ApplyResult
- type CheckResult
- type Env
- type Managed
- type Outcome
- type Signer
- type State
- type Updater
- type Verification
Constants ¶
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 )
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 )
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.
const DefaultIssuer = "https://token.actions.githubusercontent.com"
DefaultIssuer is GitHub's OIDC issuer.
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 ¶
var ApplyArgs = []string{"update-self", "--apply", "--quiet"}
ApplyArgs is the command line the detached child runs.
var ErrLocked = errors.New("update: another update is already in progress")
ErrLocked is returned when another `pay` process is already updating.
Functions ¶
func ArchiveExt ¶
ArchiveExt is .zip on Windows and .tar.gz everywhere else, matching goreleaser's format_overrides.
func BinaryName ¶
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 Newer ¶
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 ¶
ParseChecksums parses a goreleaser checksums.txt ("<sha256> <filename>").
func ResolveBinary ¶
ResolveBinary returns the running binary's real path, following symlinks so the swap replaces the file rather than the link.
func ShouldAutoApply ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) HasPending ¶
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 ¶
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.
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.