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 ¶
- func Canonical(v string) string
- func IsDowngrade(current, target string) bool
- func IsPrerelease(tag string) bool
- func NewStageDir(installPath string) (string, func(), error)
- func ProbeWritable(installPath string) error
- func RunningInDaemon() bool
- func SetCurrentVersion(v string)
- func SudoHint(installPath string, args []string) string
- func Update(ctx context.Context, opts Options) error
- func Upgrade(ctx context.Context, opts Options) error
- func VerifyArchive(archivePath, bundlePath string) error
- type Asset
- type Options
- type Release
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Canonical ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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:
- The trust root parses;
- The bundle parses;
- The bundle's certificate chain anchors to the trust root's Fulcio CA chain;
- The DSSE/messageSignature verifies over the archive bytes;
- The Rekor transparency-log entry verifies (signed entry timestamp SET checks against the trust root's Rekor public key);
- The signed timestamp from the TSA verifies;
- 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 ¶
FetchByTag exposes fetchByTag publicly for the upgrade orchestrator (`--version vX.Y.Z` pinning).
func FetchReleaseInfo ¶
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.