upgrade

package
v1.10.7 Latest Latest
Warning

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

Go to latest
Published: May 5, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package upgrade implements the in-binary self-upgrade subcommand pair `helix update` (read-only API check) and `helix upgrade` (download → verify → atomic swap → relaunch). Phase 52 D-06..D-13; Phase 58 D-02 swaps minisign verification for sigstore cosign keyless via sigstore-go.

The sigstore TUF trusted root consumed by `verify.go` is embedded at build time from `internal/upgrade/trusted_root.json` — a snapshot of the upstream public-good Sigstore TUF trust root, refreshed periodically (typically before each minor release) via `make update-trust-root` per CONTRIBUTING.md §"Trust root refresh".

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Canonical

func Canonical(v string) string

Canonical normalizes a version string to the SemVer form expected by `golang.org/x/mod/semver` (which requires a leading `v`). Accepts both `v1.9.0` and `1.9.0` and returns the `v`-prefixed form. Empty input is returned unchanged so the caller's downstream `semver.IsValid` check fails loudly rather than this helper producing `v`.

func IsDowngrade

func IsDowngrade(current, target string) bool

IsDowngrade reports whether installing `target` would be a downgrade or no-op relative to `current`. Returns true when target ≤ current per `semver.Compare`.

Equal versions count as a downgrade (no-op) per CONTEXT.md D-10: `helix upgrade` running against the same tag exits 0 with the "already up to date" message rather than re-installing the running binary. Build metadata (`+build.42`) is ignored by `semver.Compare` per the SemVer 2.0.0 spec.

func IsPrerelease

func IsPrerelease(tag string) bool

IsPrerelease reports whether tag has a SemVer pre-release segment such as `-rc1`, `-beta.1`, or `-alpha`. Used by the upgrade flow to filter out pre-release tags unless the user explicitly passes `--prerelease` per CONTEXT.md D-11.

Build metadata (`+build.42`) is NOT a pre-release segment and returns false (RESEARCH.md A5).

func NewStageDir

func NewStageDir(installPath string) (string, func(), error)

NewStageDir creates a stage directory as a sibling of installPath and returns its path plus a cleanup function. The cleanup function is idempotent and best-effort (multiple calls are safe; a failure to remove the dir is swallowed because cleanup runs in defer paths and must not propagate).

The stage dir is intentionally a sibling of the install path, NOT os.TempDir(): /tmp is often on tmpfs while /usr/local/bin lives on the root fs, and os.Rename across filesystems falls back to a non-atomic copy + unlink (Pitfall 3). The permission probe (D-09) already guarantees the install directory is writable, so the stage dir is automatically writable.

On signature-verification failure the orchestrator deliberately does NOT call cleanup — the stage dir stays on disk for postmortem inspection (VALIDATION.md State row).

func ProbeWritable

func ProbeWritable(installPath string) error

ProbeWritable returns nil iff the directory containing installPath is writable by the current user. Implementation creates and immediately removes a temp file via `os.CreateTemp`, which avoids the stat-time TOCTOU window (a stat-only check could lie about effective permissions when ACLs, mount options, or capability filters are in play).

Called BEFORE any network I/O per CONTEXT.md D-09 — failing fast on an unwritable install directory saves a multi-MB download and lets the upgrade flow surface the SudoHint message before the user invests time in a download that cannot land.

func RunningInDaemon

func RunningInDaemon() bool

RunningInDaemon reports whether the calling process is running inside the helix daemon's process tree. Returns true iff HELIX_RUNNING_AS_DAEMON=1 in the current process environment.

The daemon supervisor sets this env var on its own os.Setenv at the start of `(*Daemon).Run`, so any forked child (LS worker, hypothetical upgrade exec) inherits it. The upgrade subcommand short-circuits with a manual-restart hint when this returns true (CONTEXT.md D-08).

Tests scope the var with `t.Setenv` to avoid leaking state between test cases.

func SetCurrentVersion

func SetCurrentVersion(v string)

SetCurrentVersion records the binary version string used to build the User-Agent header for GitHub API requests. Called by internal/cli/upgrade.go (Task 3) before invoking Upgrade or FetchReleaseInfo.

func SudoHint

func SudoHint(installPath string, args []string) string

SudoHint formats the actionable re-invocation message printed by the upgrade subcommand when ProbeWritable fails. The message tells the user where the binary lives and how to re-run with elevated privileges.

Per CONTEXT.md D-09 the upgrade flow does NOT prompt for sudo internally — the user must explicitly retype the command. The args parameter typically receives `os.Args[2:]` so the hint preserves the flag set the user originally invoked (e.g., `--prerelease --version vX`).

func Update

func Update(ctx context.Context, opts Options) error

Update runs the read-only check: fetches the latest (or prerelease, per Options) release, prints `current/latest/status` lines plus the release-notes body, and returns. Mutates nothing on disk and never makes network calls beyond the API check.

func Upgrade

func Upgrade(ctx context.Context, opts Options) error

Upgrade runs the install flow per the system-architecture diagram in RESEARCH.md lines 162-250: daemon-detect → permission probe → API fetch → semver compare → download → minisign verify → extract → (DryRun bail) → swap → relaunch.

The function returns nil on `up to date` short-circuit, nil after a successful relaunch (which actually never returns on Unix because syscall.Exec replaces the process image), and a typed error on any failure. The relaunch path on Windows calls os.Exit(0) which also does not return.

func VerifyArchive

func VerifyArchive(archivePath, bundlePath string) error

VerifyArchive verifies that bundlePath is a valid sigstore cosign-keyless bundle for archivePath under the embedded trusted root (or the test override during tests). Returns nil iff:

  1. The trust root parses;
  2. The bundle parses;
  3. The bundle's certificate chain anchors to the trust root's Fulcio CA chain;
  4. The DSSE/messageSignature verifies over the archive bytes;
  5. The Rekor transparency-log entry verifies (signed entry timestamp SET checks against the trust root's Rekor public key);
  6. The signed timestamp from the TSA verifies;
  7. The certificate's SAN matches pinnedSANRegex AND its OIDC issuer extension equals pinnedOIDCIssuer.

On any failure VerifyArchive returns the SAME canonical error message ("signature verification FAILED"). This is intentional per RESEARCH.md Pitfall 4: an attacker who can probe the failure mode (by sending tampered vs. wrong-identity bundles) must not be able to distinguish them by error text. The single-message rule means the upgrade subcommand always prints the same string; downstream logging never branches on the verify outcome.

The Rekor-unreachable branch is the SOLE exception: it wraps the canonical literal with the additional user-friendly wording from D-04. See the package-level Pitfall-4 invariant comment block for the rationale (Rekor unreachability is operationally distinct from signature failure — different recovery action — and the threat model allows the disclosure).

Types

type Asset

type Asset struct {
	Name               string `json:"name"`
	BrowserDownloadURL string `json:"browser_download_url"`
}

Asset describes a single archive attached to a Release.

type Options

type Options struct {
	// Prerelease widens the release search to include `-rc*`, `-beta*`,
	// and `-alpha*` tags. Default false (D-11 stable-only).
	Prerelease bool

	// Version pins the upgrade target to a specific tag (e.g. "v1.10.0").
	// Empty means "use Prerelease to pick latest". --version always
	// wins over --prerelease (D-12 precedence rule).
	Version string

	// DryRun runs the full flow up to (but not including) the swap +
	// relaunch step. The stage dir is cleaned up; nothing on disk
	// outside the stage dir is modified.
	DryRun bool

	// Current is the running binary's version, used by the downgrade-
	// refuse check (D-10). Caller passes cli.CurrentVersion(); empty
	// is treated as "dev" and any latest tag wins.
	Current string

	// Stdout receives user-facing status messages. nil means os.Stdout.
	Stdout io.Writer
	// contains filtered or unexported fields
}

Options drives the Upgrade and Update entrypoints. All fields are optional except Current, which the cobra wiring fills with cli.CurrentVersion() so the version-compare branch can run.

type Release

type Release struct {
	TagName    string  `json:"tag_name"`
	Name       string  `json:"name"`
	Body       string  `json:"body"`
	Prerelease bool    `json:"prerelease"`
	Draft      bool    `json:"draft"`
	Assets     []Asset `json:"assets"`
}

Release matches the GitHub Releases JSON shape we care about. Fields not enumerated here are ignored. The struct mirrors the testdata fixture at internal/upgrade/testdata/release_latest.json.

func FetchByTag

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

FetchByTag exposes fetchByTag publicly for the upgrade orchestrator (`--version vX.Y.Z` pinning).

func FetchReleaseInfo

func FetchReleaseInfo(ctx context.Context, prerelease bool) (*Release, error)

FetchReleaseInfo is the public entry point for `helix update` and `helix upgrade` (without --version). When prerelease=false it calls /releases/latest (which already excludes prereleases server-side). When prerelease=true it lists /releases and returns the highest semver-comparing tag including prereleases.

The optional baseURL overrides the GitHub API host — tests pass an httptest server URL; production callers pass "" to use defaultAPIBase.

func (*Release) FindAsset

func (r *Release) FindAsset(name string) *Asset

FindAsset returns the first asset matching the given name, or nil if no match is found. Used by Upgrade() to locate the per-platform archive and signature pair from a Release's asset list.

Jump to

Keyboard shortcuts

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