updater

package
v0.4.3 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package updater — self-update support for nightme.

The package owns three jobs, in order:

  • Lookup: fetch the GitHub release metadata for a given tag, including the list of release assets and SHA256SUMS.

  • Match: pick the single asset that matches the running binary's GOOS/GOARCH. Assets follow the "nightme_<version>_<os>_<arch>.<ext>" naming convention used by the project's release workflow (see the SHA256SUMS listing for v0.3.7 as the canonical example).

  • Download: fetch the matched asset to a staging path under DataDir/updates/<version>/, with cancellable context, stdlib-only progress reporting, and SHA256 verification against the SHA256SUMS file from the same release.

The package is stdlib-only. We could pull in rhysd/go-github-selfupdate but it bundles its own GitHub client, semaphore, and progress bar; for a single-binary release like nightme the stdlib path is ~200 lines and keeps the supply-chain surface small.

Layering:

  • cmd/nightme/update.go (CLI shell)
  • internal/updater (this package: Lookup / Match / Download)
  • cmd/nightme/update.go (Install — next commit: selfupdate binary swap + daemon restart).

Index

Constants

View Source
const DefaultTimeout = 5 * time.Minute

DefaultTimeout caps the entire download path (lookup + checksum + asset). Production callers pass a derived context so Ctrl-C cancels cleanly.

Variables

View Source
var LookupURL = "https://api.github.com"

LookupURL is the base URL GitHub's releases API lives at. Held as a var so tests can swap it for an httptest server without having to plumb a base URL through every caller.

Production callers should leave this untouched; the default (api.github.com) is what we ship.

Functions

func ExtractArchive

func ExtractArchive(archivePath, stagingDir string) (string, error)

ExtractArchive pulls the nightme binary out of the .tar.gz / .zip downloaded by Download. Exposed here so install.go (next commit) can reuse it without depending on archive-specific code paths in two places. Returns the absolute path to the extracted binary inside stagingDir.

func FormatBytes

func FormatBytes(n int64) string

FormatBytes is exposed for progress reporter reuse.

func FormatSpeed

func FormatSpeed(bytes int64, elapsed time.Duration) string

FormatSpeed returns a human-readable bytes/sec string for the progress reporter (e.g. "1.2 MB/s"). Exposed so tests can pin the formatter output without re-implementing the math.

func QuietProgress

func QuietProgress(int64, int64, time.Duration)

QuietProgress is a no-op ProgressFunc for callers that want to silence the progress reporter (CI, scripted runs, tests).

func StagingDir

func StagingDir(dataDir, version string) (string, error)

StagingDir returns the canonical staging path for a given version: <DataDir>/updates/<version>/. Callers should pass the value from config.Config.Paths.DataDir; an empty DataDir disables staging (returns "" + error).

Types

type Asset

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

Asset is one downloadable file in a release. The fields we need are name (for matching + SHA256SUMS lookup) and browser_download_url (the actual asset URL). Size is useful for the progress bar's total field.

func MatchAsset

func MatchAsset(release *Release, version string) *Asset

MatchAsset picks the asset matching the running binary's GOOS/GOARCH using the "nightme_<version>_<os>_<arch>.<ext>" convention. When version is empty the matcher accepts any version segment so callers can use it across releases.

Returns nil (no error) when no asset matches — the caller surfaces this as "no binary for darwin/amd64 in this release", which is the correct diagnostic for an unsupported OS/arch.

type CheckResult

type CheckResult struct {
	Latest   string
	Outdated bool
	Release  *Release
}

CheckResult is the unified stage-1 output. It bundles the raw *Release (so downstream stages can pick the asset + SHA256SUMS without a second API call) with the user- facing Latest string and the Outdated bool.

Latest  — tag of the release we're targeting (e.g. "v0.3.7")
Outdated — true when current < latest under semver rules
Release — full *Release for stages 2 + 3

func Check

func Check(ctx context.Context, tag string) (*CheckResult, error)

Check is stage 1: resolve the latest (or pinned) GitHub release and decide whether the running build is out of date. It does NOT touch the filesystem or any binaries.

Tag is the optional `--tag vX.Y.Z` override; empty means "latest". The current version is read from the package- level version.Version via version.Compare.

Errors are surfaced verbatim — the CLI translates them into the "1/3 check failed" line.

type DownloadResult

type DownloadResult struct {
	Asset       Asset
	StagingPath string // absolute path under stagingDir
	SHA256Hex   string // hex-encoded hash of the downloaded bytes
	Bytes       int64  // total bytes written (== Asset.Size on success)
	Cached      bool   // true when a local archive already matched SHA256SUMS
}

DownloadResult is what Download returns on success. The caller (CLI / install command) reads StagingPath to swap the binary in place.

func Download

func Download(
	ctx context.Context,
	release *Release,
	asset *Asset,
	stagingDir string,
	progress ProgressFunc,
) (*DownloadResult, error)

Download fetches the asset to stagingDir/<asset.Name> with cancellable context, periodic progress reporting, and a SHA256SUMS-driven integrity check.

The downloaded archive (.tar.gz on unix, .zip on windows) is kept as-is in the staging dir; Install (next commit) is responsible for extracting and replacing the binary.

stagingDir is typically <DataDir>/updates/<version>/. The function creates it (parents included) if it does not exist.

progress may be nil for silent downloads.

The SHA256SUMS file is fetched separately from the same release; if it cannot be downloaded the function fails closed (errors.New("checksums unreachable")) so callers never silently install an unverified binary.

type InstallResult

type InstallResult struct {
	NewBinaryPath string // path to the binary now on disk (== target)
	OldBinaryPath string // path to the backup of the previous binary
	ExtractedFrom string // archive we extracted
}

InstallResult is what Install returns on success. The caller (CLI) reads NewBinaryPath to print "the new binary is at X" and OldBinaryPath to mention the rollback path.

func Install

func Install(stagedBinaryPath, targetPath string) (*InstallResult, error)

Install replaces the running binary with a previously downloaded + extracted one.

stagedBinaryPath  -- the nightme / nightme.exe produced by
                      ExtractArchive, sitting in
                      <DataDir>/updates/<version>/
targetPath        -- the on-disk binary the user is currently
                      invoking (os.Executable())

Steps:

  1. Refuse to install when source == target (copying onto itself on Windows is a permissions nightmare; on unix it'd succeed but is almost certainly a caller bug).
  2. Verify stagedBinaryPath is a regular file, readable, and executable-sized.
  3. Move targetPath → targetPath + ".old" (the backup). Move is rename(2) on unix — atomic on the same filesystem — so a crashed install leaves either the old binary in place or the new one in place; never a half-written file at targetPath.
  4. Copy stagedBinaryPath → targetPath.
  5. chmod 0755 on targetPath (the staging dir might have lost the +x bit during extraction under some umasks).

Errors before step 3 are pure: nothing on disk has changed. Errors during step 4 attempt to roll back by renaming targetPath.old back to targetPath. If the rollback also fails the error wraps the rollback so the operator knows to run `mv <target>.old <target>` by hand.

type ProgressFunc

type ProgressFunc func(downloaded int64, total int64, elapsed time.Duration)

ProgressFunc is called periodically during Download with the current bytes read, total bytes (when known), and elapsed wall time. Implementations typically render an ASCII progress bar to the terminal. Callers may pass nil to skip progress reporting entirely (faster path for tests / quiet mode).

Frequency is best-effort: the downloader flushes a progress event on every chunk boundary AND on every Tick interval (200ms), whichever fires first. Total may be -1 if the server did not send Content-Length.

func NewASCIIProgressBar

func NewASCIIProgressBar(out io.Writer, total int64) ProgressFunc

NewASCIIProgressBar returns a ProgressFunc that renders a single-line ASCII bar to out. The bar overwrites itself with \r on every tick and the caller prints a final newline (Download flushes an empty event by virtue of total == done).

Layout (width = 30 cells):

[==============              ]  47% 1.2 MB / 2.6 MB  4.3 MB/s  ETA 5s

total <= 0 (server omitted Content-Length) renders an indeterminate bar that only shows downloaded bytes — the bar cell count grows as bytes arrive.

type Release

type Release struct {
	TagName string  `json:"tag_name"`
	Assets  []Asset `json:"assets"`
}

Release is the subset of the GitHub release payload we read. We don't decode every field — the asset list and the tag name are all that Lookup consumers need.

func Lookup

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

Lookup queries GitHub for the release that matches the given tag (e.g. "v0.3.7" or "0.3.7"). When tag is empty the API serves the latest non-prerelease release.

repo is "owner/name" on GitHub. Tests override this to point at an httptest server via LookupURL.

Jump to

Keyboard shortcuts

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