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
- Variables
- func ChecksumFor(checksumsBody []byte, archiveName string) (string, error)
- func Display(s string) string
- func ExtractBinary(archivePath, binaryName, destDir string) (string, error)
- func IsGitDescribeDev(s string) bool
- func Replace(destPath, newBinPath string) (backupPath string, err error)
- func SHA256File(path string) (string, error)
- func Upgrade(ctx context.Context, opts Options) (installedVersion string, backupPath string, err error)
- func UpgradeAvailable(current, latest Semver) bool
- func VerifyCosign(checksumsPath, bundlePath string, opts CosignOptions) (bool, error)
- func VerifySHA256(archivePath, expectedHex string) error
- type CheckResult
- type CosignOptions
- type Options
- type Release
- type Semver
Constants ¶
const ArchiveProject = "truestamp-cli"
ArchiveProject is the filename stem used by GoReleaser for the tarball. Matches docs/install.sh PROJECT.
const BinaryName = "truestamp"
BinaryName is the binary inside each release tarball.
const ReleasesRepo = "truestamp/truestamp-cli"
ReleasesRepo is the GitHub slug truestamp release artifacts come from.
Variables ¶
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.
var ErrBinaryNotInArchive = errors.New("binary not found in archive")
ErrBinaryNotInArchive is returned when the extracted tarball does not contain the expected binary entry.
var ErrChecksumMismatch = errors.New("checksum mismatch")
ErrChecksumMismatch is returned when a computed SHA-256 does not match the value parsed from checksums.txt.
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.
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.
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.
var ErrCosignVerify = errors.New("cosign verification failed")
ErrCosignVerify is returned when `cosign verify-blob` exits non-zero.
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).
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.
var ErrReleaseNotFound = errors.New("release not found")
ErrReleaseNotFound is returned when the requested tag has no release.
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.
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.
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.
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 ¶
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
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 ¶
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
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 ¶
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 ¶
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
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:
- $TRUESTAMP_COSIGN_PATH (absolute path; lets hardened environments pin cosign to a known location and avoid $PATH hijacking).
- 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 ¶
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.
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 ¶
FetchByTag returns the release for a specific tag (e.g. "v0.3.0"). Returns ErrReleaseNotFound if GitHub returns 404.
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 ¶
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 ¶
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 ¶
IsPreRelease reports whether the version carries a pre-release identifier (anything after `-`). Build metadata alone doesn't count.