selfupgrade

package
v0.7.1 Latest Latest
Warning

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

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

Documentation

Overview

Package selfupgrade implements the in-place upgrade flow for users who installed truestamp via docs/install.sh or manual tarball extraction. The behavior intentionally mirrors install.sh so the security posture is identical: download archive + checksums + signature bundle, verify SHA-256 (mandatory, pure Go), verify cosign (best-effort shell-out), extract, atomic replace, clear quarantine xattr on darwin.

Homebrew and `go install` users never reach this package — the cmd layer detects the install method and prints the correct package- manager instruction instead.

Index

Constants

View Source
const ArchiveProject = "truestamp-cli"

ArchiveProject is the filename stem used by GoReleaser for the tarball. Matches docs/install.sh PROJECT.

View Source
const BinaryName = "truestamp"

BinaryName is the binary inside each release tarball.

View Source
const ReleasesRepo = "truestamp/truestamp-cli"

ReleasesRepo is the GitHub slug truestamp release artifacts come from.

Variables

View Source
var ErrAlreadyCurrent = errors.New("already on the requested version")

ErrAlreadyCurrent is returned when the requested target version is already running. Callers treat this as a no-op success.

View Source
var ErrBinaryNotInArchive = errors.New("binary not found in archive")

ErrBinaryNotInArchive is returned when the extracted tarball does not contain the expected binary entry.

View Source
var ErrChecksumMismatch = errors.New("checksum mismatch")

ErrChecksumMismatch is returned when a computed SHA-256 does not match the value parsed from checksums.txt.

View Source
var ErrChecksumMissing = errors.New("archive not listed in checksums.txt")

ErrChecksumMissing is returned when checksums.txt does not contain an entry for the expected archive name.

View Source
var ErrCosignBundleMissing = errors.New("cosign bundle not published for release")

ErrCosignBundleMissing is returned when require-cosign is set and the signature bundle could not be downloaded for the release.

View Source
var ErrCosignMissing = errors.New("cosign required but not on PATH")

ErrCosignMissing is returned when TRUESTAMP_REQUIRE_COSIGN=1 is set but the `cosign` binary is not available on PATH.

View Source
var ErrCosignVerify = errors.New("cosign verification failed")

ErrCosignVerify is returned when `cosign verify-blob` exits non-zero.

View Source
var ErrNotUpgradable = errors.New("current binary is not user-writable; re-run with sudo or reinstall via install.sh")

ErrNotUpgradable is returned when the running binary is in a directory the current user cannot write to (e.g. /usr/local/bin owned by root).

View Source
var ErrPreRelease = errors.New("latest release is a pre-release")

ErrPreRelease is returned when resolveVersion resolves `latest` to a pre-release tag and the caller did not pin --version explicitly. The cmd layer translates this to exit code 3 and a helpful message.

View Source
var ErrReleaseNotFound = errors.New("release not found")

ErrReleaseNotFound is returned when the requested tag has no release.

View Source
var ErrReplaceUnsupported = errors.New("in-place upgrade not supported on this platform")

ErrReplaceUnsupported is returned from Replace on platforms that do not support in-place binary replacement (currently Windows). Unix builds never return this.

View Source
var ErrWindowsUnsupported = errors.New("in-place upgrade is not supported on Windows; run: go install github.com/truestamp/truestamp-cli/cmd/truestamp@latest")

ErrWindowsUnsupported is returned when self-upgrade is attempted on Windows. Callers should print `go install ...@latest` instead.

View Source
var LatestReleaseURL = "https://api.github.com/repos/" + ReleasesRepo + "/releases/latest"

LatestReleaseURL is the REST endpoint returning the most recent non-prerelease, non-draft release. Unauthenticated GitHub requests are capped at 60/hr per IP; callers can set GITHUB_TOKEN to lift that to 5000/hr. Declared as var (not const) so tests can redirect to an httptest server; do not reassign in production code.

View Source
var TagReleaseURLFormat = "https://api.github.com/repos/" + ReleasesRepo + "/releases/tags/%s"

TagReleaseURLFormat is a printf format for fetching a specific tag. Same var-for-testability rationale as LatestReleaseURL.

Functions

func ChecksumFor

func ChecksumFor(checksumsBody []byte, archiveName string) (string, error)

ChecksumFor parses a GoReleaser-style checksums.txt (`<hex> <file>` per line) and returns the expected hex digest for archiveName, or ErrChecksumMissing if no such line exists.

func Display added in v0.3.3

func Display(s string) string

Display returns a user-facing version string with any leading "v" stripped so versions printed from different sources — ldflags-injected build metadata (already stripped by the Taskfile build), GitHub tag names (keep the "v"), or cached upgrade-check values — render consistently. Safe to call on any string; a value without a leading "v" is returned unchanged. Never fails, never parses.

func ExtractBinary

func ExtractBinary(archivePath, binaryName, destDir string) (string, error)

ExtractBinary opens archivePath (a .tar.gz produced by GoReleaser) and writes the first entry named binaryName into destDir, returning the full path of the written file. The file is written with 0755 perms.

Rejects:

  • non-regular tar entries (symlinks, hardlinks, devices) — GoReleaser tarballs ship only regular files, so these are always suspicious.
  • tar entries with `..` path components (defense against path traversal — shouldn't happen with GoReleaser output, defensive).
  • entries larger than extractMaxBytes.

func IsGitDescribeDev added in v0.6.0

func IsGitDescribeDev(s string) bool

IsGitDescribeDev reports whether s looks like a locally-built development binary whose version string came from `git describe --tags --always --dirty` — i.e. a `<base-version>-<N>-g<SHA>[-dirty]` shape where N is the count of commits past the nearest release tag.

This matters because strict SemVer §11 ranks `M.m.p-<any-pre>` BELOW `M.m.p` — but git-describe's `-<N>-g<SHA>` suffix means the build is AHEAD of the tag, the opposite semantic. Without special-casing this shape, the upgrade flow would happily "upgrade" a dev build back to the base tag (an actual downgrade), and the passive notice would nag on every command run.

func Replace

func Replace(destPath, newBinPath string) (backupPath string, err error)

Replace atomically swaps destPath with newBinPath, leaving a timestamped backup of the previous binary alongside it. On same-filesystem moves this is a single rename; on cross-filesystem it falls back to copy+ rename (same behavior as install.sh:286-290).

On darwin, the macOS quarantine xattr is cleared via the `xattr` CLI, matching install.sh:296-298. Missing `xattr` is NOT a fatal error.

After a successful replace, backups older than 7 days in the binary's directory are pruned.

The returned string is the path to the backup of the previous binary, or "" if no prior binary existed.

func SHA256File

func SHA256File(path string) (string, error)

SHA256File computes the lowercase hex SHA-256 digest of a file.

func Upgrade

func Upgrade(ctx context.Context, opts Options) (installedVersion string, backupPath string, err error)

Upgrade performs the full in-place upgrade flow. Returns the installed version on success.

func UpgradeAvailable added in v0.6.0

func UpgradeAvailable(current, latest Semver) bool

UpgradeAvailable reports whether `latest` represents a real upgrade from `current`, taking git-describe dev builds into account.

For non-git-describe versions this is plain `latest.Compare(current) > 0`.

For git-describe dev builds (see IsGitDescribeDev), we compare the MAJOR.MINOR.PATCH cores only: a dev build `0.5.0-4-g356ee75-dirty` is considered at-or-above `v0.5.0` (no spurious upgrade offer), but `v0.5.1` or `v0.6.0` still register as upgrades.

func VerifyCosign

func VerifyCosign(checksumsPath, bundlePath string, opts CosignOptions) (bool, error)

VerifyCosign runs `cosign verify-blob --bundle=<bundlePath> ... <checksumsPath>`. If cosign is not on PATH or bundlePath is empty, the behavior depends on opts.Required: required=true returns an error, required=false returns (false, nil) to indicate "skipped, non-fatal". Returns (true, nil) when verification succeeds.

Resolution order for the cosign binary:

  1. $TRUESTAMP_COSIGN_PATH (absolute path; lets hardened environments pin cosign to a known location and avoid $PATH hijacking).
  2. exec.LookPath("cosign") (the usual $PATH search).

The SHA-256 verification in VerifySHA256 is mandatory regardless of what this function does, so a cosign-layer miss is defense-in-depth, not a single point of failure.

func VerifySHA256

func VerifySHA256(archivePath, expectedHex string) error

VerifySHA256 reads archivePath and asserts its SHA-256 matches expectedHex (case-insensitive). Returns ErrChecksumMismatch otherwise.

Types

type CheckResult

type CheckResult struct {
	CurrentVersion string
	LatestVersion  string
	UpgradeAvail   bool
	PreRelease     bool // latest is a pre-release (GitHub flag OR semver -suffix)
}

CheckResult describes the outcome of a dry-run check.

func Check

func Check(ctx context.Context, opts Options) (*CheckResult, error)

Check resolves the latest release without downloading any binaries. It honors the same pre-release filtering as Upgrade().

type CosignOptions

type CosignOptions struct {
	// Required mirrors TRUESTAMP_REQUIRE_COSIGN=1 from install.sh. When
	// true, missing cosign binary or bundle is a hard error.
	Required bool

	// PinnedPath is an absolute path to the cosign binary. When empty,
	// $PATH is searched. Populated from config.Config.CosignPath which
	// in turn sources from config.toml or $TRUESTAMP_COSIGN_PATH. The
	// config layer validates the path is absolute; resolveCosignBinary
	// re-checks existence and the executable bit at use time.
	PinnedPath string

	// CertIdentityRegexp is passed to cosign as
	// --certificate-identity-regexp. Must match the release workflow.
	CertIdentityRegexp string

	// OIDCIssuer is passed as --certificate-oidc-issuer.
	OIDCIssuer string
}

CosignOptions configures a cosign signature verification pass.

func DefaultCosignOptions

func DefaultCosignOptions(required bool, pinnedPath string) CosignOptions

DefaultCosignOptions returns the identity values wired into install.sh:256-261 — the release.yml workflow in truestamp/truestamp-cli signed by GitHub Actions' OIDC token endpoint. pinnedPath is optional (empty = $PATH lookup) and is forwarded verbatim to CosignOptions.

type Options

type Options struct {
	// TargetVersion overrides the default of "latest" (i.e. the user
	// passed --version). Leading "v" is auto-added when missing so
	// callers can pass "0.4.0" or "v0.4.0".
	TargetVersion string

	// CurrentVersion is the running binary's version string, typically
	// version.Version. Used for comparison and backup naming.
	CurrentVersion string

	// RequireCosign corresponds to TRUESTAMP_REQUIRE_COSIGN=1 in
	// install.sh. When true, missing cosign binary or missing bundle
	// becomes a hard error.
	RequireCosign bool

	// SkipCosign disables cosign verification entirely (SHA-256 still
	// mandatory). Corresponds to `truestamp upgrade --no-verify` and
	// TRUESTAMP_SKIP_CHECKSUM=0 parity is intentional only for SHA-256.
	SkipCosign bool

	// CosignPath pins the cosign binary used for signature verification.
	// Empty = $PATH lookup. Must be an absolute path when set; the
	// config layer validates this up front.
	CosignPath string

	// Logger receives progress lines. May be nil for silent operation.
	Logger func(msg string)
}

Options configures an Upgrade() or Check() call.

type Release

type Release struct {
	TagName    string `json:"tag_name"`
	Name       string `json:"name"`
	Prerelease bool   `json:"prerelease"`
	Draft      bool   `json:"draft"`
	HTMLURL    string `json:"html_url"`
}

Release is the subset of the GitHub Releases API response we consume.

func FetchByTag

func FetchByTag(ctx context.Context, tag string) (*Release, error)

FetchByTag returns the release for a specific tag (e.g. "v0.3.0"). Returns ErrReleaseNotFound if GitHub returns 404.

func FetchLatest

func FetchLatest(ctx context.Context) (*Release, error)

FetchLatest returns the current latest non-prerelease, non-draft release. Respects ctx cancellation.

type Semver

type Semver struct {
	Major, Minor, Patch int
	PreRelease          string // content after `-`, empty for stable releases
	BuildMetadata       string // content after `+`
	Raw                 string // original input, preserved for display
	// contains filtered or unexported fields
}

Semver is the CLI's release-tag view over `golang.org/x/mod/semver`. Parsing, validation, and ordering are delegated to the Go team's canonical implementation; this struct keeps typed Major/Minor/Patch ints + a Raw copy of the original input around because it reads more clearly at call sites than passing bare strings, and because a few places (samePatch, UpgradeAvailable's core-only comparison) need structural access.

Release tags in this project are strict `v<MAJOR>.<MINOR>.<PATCH>[-<PRE>][+<BUILD>]`. Any leading "v" is normalized internally (x/mod/semver requires it) but preserved in Raw for display.

func ParseSemver

func ParseSemver(s string) (Semver, error)

ParseSemver accepts tags like "v0.3.0", "0.3.0", "v1.0.0-rc.1", "v1.0.0-beta.2+build.7", and git-describe shapes like "0.5.0-4-g356ee75-dirty". Validation and the canonical form come from x/mod/semver; the typed MAJOR.MINOR.PATCH fields are derived from the canonical string.

func (Semver) Compare

func (v Semver) Compare(other Semver) int

Compare returns -1, 0, or 1 for v < other, v == other, v > other. Delegates to golang.org/x/mod/semver.Compare, which implements the strict SemVer §11 ordering (pre-releases rank below the same core tagged release; pre-release identifiers compare numerically when both are numeric, lexically otherwise; build metadata is ignored).

For the git-describe-vs-tag asymmetry — SemVer ranks `v0.5.0-4-g...-dirty` BELOW `v0.5.0` even though the dev build is conceptually ahead — callers should use UpgradeAvailable instead, which special-cases that shape.

func (Semver) IsPreRelease

func (v Semver) IsPreRelease() bool

IsPreRelease reports whether the version carries a pre-release identifier (anything after `-`). Build metadata alone doesn't count.

Jump to

Keyboard shortcuts

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