updater

package
v0.6.0 Latest Latest
Warning

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

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

Documentation

Overview

Package updater implements `brainjar upgrade` — in-process self-update that fetches the latest release from R2, verifies the cosign bundle against a pinned identity, and atomically swaps the running binary. Consumes the outputs of .github/workflows/release.yml; the URL contract documented below is the implicit API between that pipeline and this package.

See docs/design-m9-upgrade.md for the full spec.

Index

Constants

View Source
const (
	// DefaultBaseURL is the CDN prefix every upgrade reaches under.
	// Override via Options.BaseURL in tests.
	DefaultBaseURL = "https://get.brainjar.sh/brainjar"

	// LatestPath is the pointer file listing the current release tag.
	// Plain UTF-8, a single tag (e.g. "v0.3.2"), max 32 bytes.
	LatestPath = "latest"

	ChecksumsFile       = "checksums.txt"
	ChecksumsBundleFile = "checksums.txt.sigstore.json"
)

Source-of-truth constants. The release workflow and this package must agree on every item below; if the pipeline changes any of these paths/shapes, the matching update lands here in the same PR.

View Source
const (
	ExpectedSANRegex = `` /* 130-byte string literal not displayed */
	ExpectedIssuer   = "https://token.actions.githubusercontent.com"
)

Identity pins. The cosign bundle must verify against these; a mismatch means someone other than our release workflow signed the checksums, and we refuse to install regardless of whether the sha256 lines up.

The SAN accepts two legitimate triggers:

  • refs/heads/main — the release-please flow triggers the release workflow via `workflow_dispatch` from main, so the OIDC cert's ref is `refs/heads/main`. This is what every production release has been signed under to date.
  • refs/tags/vX.Y.Z — the same workflow also triggers on tag pushes; future releases via that path get tag refs. Allow them so the pin survives a trigger change.

Feature branches, PR refs, and any other ref are rejected. An attacker who can push to main or create a v* tag can already release; this pin doesn't narrow that threat, it just confines "signed by our release workflow" to "signed while running from main or from a release tag".

The issuer is an exact match. GitHub Actions OIDC has a stable token endpoint; anything else in that field is forged or misconfigured.

Variables

View Source
var (
	ErrUsage       = errors.New("updater: usage")
	ErrNetwork     = errors.New("updater: network failure")
	ErrVerify      = errors.New("updater: verification failure")
	ErrInstall     = errors.New("updater: install failure")
	ErrUnsupported = errors.New("updater: platform unsupported")
)

Sentinel errors — the CLI layer maps each to a specific exit code.

View Source
var ErrUnsupportedPlatform = errors.New("updater: no release artifact for this platform")

ErrUnsupportedPlatform is returned when the current runtime isn't in the release matrix. Mapped to exit 2 at the CLI layer.

View Source
var ErrVerification = errors.New("updater: verification failed")

ErrVerification is the sentinel wrapper returned for every verification failure. The CLI layer checks errors.Is(err, ErrVerification) to map to exit 4.

View Source
var PackageManagerSignals = []struct {
	Prefix string
	Name   string
	Hint   string
}{
	{"/opt/homebrew/", "Homebrew", "run `brew upgrade brainjar` instead"},
	{"/usr/local/Cellar/", "Homebrew", "run `brew upgrade brainjar` instead"},
	{"/home/linuxbrew/.linuxbrew/", "Homebrew", "run `brew upgrade brainjar` instead"},
	{"/nix/store/", "Nix", "update via your flake / profile instead"},
	{"/var/lib/flatpak/", "Flatpak", "run `flatpak update` instead"},
}

PackageManagerSignals maps a resolved-install-path prefix to the pretty name of the package manager that commonly installs there. We surface a stderr warning (not a hard error) so users who upgraded the wrong binary aren't surprised when their package manager later clobbers our write.

apt and rpm are deliberately absent: their /usr/bin placement collides with manual installs on enough distros that detection produces too many false warnings.

View Source
var SupportedPlatforms = [][2]string{
	{"linux", "amd64"},
	{"linux", "arm64"},
	{"darwin", "amd64"},
	{"darwin", "arm64"},
}

SupportedPlatforms is the (GOOS, GOARCH) matrix the release pipeline publishes for. Held here so a build-time sanity check catches drift from .github/workflows/release.yml.

Functions

func CanInstall

func CanInstall() error

CanInstall reports whether the current platform supports atomic binary swap. Unix is supported; see platform_windows.go for the refusal path.

func ExpectedSHA256

func ExpectedSHA256(checksumsTxt []byte, filename string) (string, error)

ExpectedSHA256 returns the lowercase-hex sha256 recorded in checksumsTxt for filename, or an error when the file is missing or multiply-listed. Trailing whitespace on lines is tolerated; empty lines are skipped. Any malformed line is a pipeline bug and fails the whole parse — we don't silently accept "mostly" valid checksum files.

func ExtractBrainjarBinary

func ExtractBrainjarBinary(r io.Reader, dst io.Writer) error

ExtractBrainjarBinary reads a gzipped tar from r and streams the single `brainjar` entry into dst. Enforces:

  • exactly one entry, matching binaryNameInTarball exactly
  • regular file (no symlinks, hardlinks, devices)
  • path has no directory component, no "..", not absolute
  • uncompressed size ≤ maxBinaryBytes

Anything else is a verification failure; the caller treats the error as tampering, not a malformed-but-okay tarball.

func Install

func Install(src io.Reader, t *ResolvedTarget) error

Install performs the atomic swap. src is an already-opened reader positioned at the start of the new binary (typically the tarball- extracted file). The old binary is preserved at <base>.old on a best-effort basis to enable a manual rollback.

Flow:

  1. Stage: create .brainjar.new.<pid> in t.Dir with O_EXCL, copy, fsync, chmod 0o755.
  2. Preserve: best-effort rename of current binary to <base>.old.
  3. Swap: os.Rename(staging, t.Path) — the one atomic step.
  4. Durable: fsync the directory so the rename survives a crash.

Any error unwinds best-effort: staging file is removed so we don't leak hidden files into the user's install dir.

func TarballName

func TarballName() (string, error)

TarballName returns the per-platform artifact filename for the current runtime, or an error when runtime is outside the matrix.

func VerifyBundle

func VerifyBundle(bundleJSON, checksumsTxt []byte) error

VerifyBundle verifies a cosign bundle (checksums.txt.sigstore.json) against the pinned identity AND that the signed message is the sha256 of checksumsTxt. Returns nil on success; any verification failure is wrapped with context so the CLI layer can surface a specific message rather than a generic "verification failed".

The trusted root is fetched via TUF from the sigstore public-good repository. Cached on disk by sigstore-go's TUF client — the first run needs network, subsequent runs are offline.

Types

type Fetcher

type Fetcher struct {
	BaseURL string
	Client  *http.Client
}

Fetcher turns the URL layout into concrete HTTP requests. It holds an *http.Client (injectable for tests) and a base URL (injectable so tests can point at httptest.Server without touching DNS).

func NewFetcher

func NewFetcher() *Fetcher

NewFetcher returns a Fetcher with the production base URL and a generous 30-second timeout. Tests construct the struct literal with a custom BaseURL and httptest client.

func (*Fetcher) Download

func (f *Fetcher) Download(ctx context.Context, tag, name string, dst io.Writer) error

Download streams <base>/<tag>/<name> into dst. Returns an error for any non-200 response. No checksum verification here — that's the caller's responsibility after the download completes.

func (*Fetcher) FetchLatest

func (f *Fetcher) FetchLatest(ctx context.Context) (string, error)

FetchLatest returns the current release tag from <base>/latest. The response is validated against tagPattern before return — an unrecognizable value is a pipeline bug and we refuse to guess.

type Options

type Options struct {
	CurrentVersion string   // pkg/version.Version
	Check          bool     // --check: report + exit, no download
	Force          bool     // --force: re-install even when versions match
	Fetcher        *Fetcher // nil → NewFetcher()
}

Options configure a single upgrade invocation. Zero value is valid: CurrentVersion "" is treated as "dev", no flags set, a production-configured Fetcher is created automatically.

type ResolvedTarget

type ResolvedTarget struct {
	Path   string // resolved — the file we rename onto
	Linked string // the symlink the user invoked, or "" if none
	Dir    string // filepath.Dir(Path) — where the staging file goes
	Base   string // filepath.Base(Path) — usually "brainjar"
}

ResolvedTarget holds the canonical install path (after following symlinks) plus whichever symlink the user actually invoked, if different. Both are surfaced in error messages so the user can both fix the right thing (chmod the real dir) and understand why the error names a path they didn't type.

func ResolveExecutable

func ResolveExecutable() (*ResolvedTarget, error)

ResolveExecutable wraps os.Executable + filepath.EvalSymlinks so the caller gets a useful error attributed to the right layer. Returns both the invoked path and the resolved path; Linked is only non-empty when the two differ.

func (*ResolvedTarget) PackageManagerWarning

func (t *ResolvedTarget) PackageManagerWarning() string

PackageManagerWarning returns a single-line stderr-friendly warning if the resolved path lives under a package-manager prefix, otherwise empty string. No trailing newline — caller appends.

func (*ResolvedTarget) PermissionHint

func (t *ResolvedTarget) PermissionHint(probeErr error) error

PermissionHint returns a multi-line error message for an install dir we can't write to. Discloses the symlink indirection when it exists so the user can chmod the right directory rather than getting confused by a path they didn't type.

func (*ResolvedTarget) PreflightWritable

func (t *ResolvedTarget) PreflightWritable() error

PreflightWritable checks that we can create + rename inside t.Dir before the caller spends time on a 5 MiB download. Creates a tiny probe file with O_EXCL, removes it immediately. Returns a rich error via PermissionHint on failure.

type Result

type Result struct {
	Current         string `json:"current"`
	Latest          string `json:"latest"`
	UpdateAvailable bool   `json:"update_available"`
	Upgraded        bool   `json:"upgraded"`
	InstalledPath   string `json:"installed_path,omitempty"`
	PackageWarning  string `json:"package_warning,omitempty"`
}

Result is the structured summary returned to the CLI layer. Rendered as JSON with the `json:` tags or as a one-line text message, depending on the caller.

func Run

func Run(ctx context.Context, opts Options) (*Result, error)

Run orchestrates a full upgrade cycle. The CLI passes the already-validated Options; Run owns URL construction, download, verify, extract, and swap. Never writes to stdout.

Jump to

Keyboard shortcuts

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