update

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package update reports whether a newer fft release is available.

It is deliberately a best-effort background chore: nothing it does may delay or fail a command. The result of the last check is cached for Interval, so the network is consulted at most once a day, and every failure — no network, a rate limit, a repository with no releases yet — is swallowed.

Index

Constants

View Source
const (
	// DefaultURL is GitHub's latest-release endpoint for fft. It is
	// unauthenticated: the rate limit it shares with the rest of the machine is
	// 60 requests an hour, and one a day per user is comfortably inside it.
	DefaultURL = "https://api.github.com/repos/Joessst-Dev/fft-cli/releases/latest"

	// Interval is how long a cached answer is trusted before GitHub is asked
	// again.
	Interval = 24 * time.Hour

	// Timeout bounds a single check. It is short on purpose: the check runs
	// alongside the command, and a user on a plane must not wait for it.
	Timeout = 1500 * time.Millisecond
)

Variables

This section is empty.

Functions

func Comparable

func Comparable(v string) bool

Comparable reports whether v has a place in the version ordering at all — that is, whether it is a real semver version rather than "dev", "", or a branch name.

A build that is not comparable is never told to upgrade, and must never be told it is up to date either: the honest answer about it is "unknown".

func DefaultCachePath

func DefaultCachePath() (string, error)

DefaultCachePath is $XDG_CACHE_HOME/fft/update.json, falling back to ~/.cache/fft/update.json.

func Newer

func Newer(current, latest string) bool

Newer reports whether latest is a later release than current.

The comparison is a real semver parse, because the obvious string comparison gets v1.10.0 vs v1.9.0 exactly backwards. Anything that is not a semver version — "dev", "", a branch name — compares as no answer at all, which is what suppresses the notice on a local build.

Types

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker asks GitHub for the latest release and remembers the answer.

The zero value is not usable; call New. A Checker is safe to use from the background goroutine that owns it, and holds no state beyond its configuration — the cache lives in a file.

func New

func New(version, cachePath string, opts ...Option) *Checker

New returns a Checker for the running fft version, caching its answer in the file at cachePath.

version is the fft build's own version, used both as the User-Agent GitHub asks callers to send and as the left-hand side of the version comparison.

func (*Checker) Cached

func (c *Checker) Cached() (State, bool)

Cached returns the last stored result, and whether it is fresh enough to use instead of asking GitHub again.

A missing, unreadable or corrupt cache file is reported as a zero State that is not fresh: a cache we cannot understand is one we do not have. A CheckedAt in the future is not fresh either — a clock that jumped must not silence the check forever.

func (*Checker) Claim

func (c *Checker) Claim() error

Claim records that a check is being made, before it is made.

The process does not necessarily outlive the check. A command that finishes in milliseconds exits while the request to GitHub is still in flight, and the background goroutine dies with it, having written nothing at all — so the next invocation would find the same stale cache and ask again, and so would the one after that. A user whose commands are fast, or whose network is simply down, would hit GitHub every single time they typed fft.

Stamping the cache first costs one small write a day and makes that impossible. What was already known is carried over: the claim records when we asked, not what we learned.

func (*Checker) Current

func (c *Checker) Current() string

Current is the fft version this Checker compares releases against.

func (*Checker) Notice

func (c *Checker) Notice(s State) string

Notice is the line to show the user, or "" when there is nothing to say — fft is current, the release is older, or either version is not a version we can compare.

func (*Checker) Refresh

func (c *Checker) Refresh(ctx context.Context) (State, error)

Refresh asks GitHub for the latest release, ignoring the cache's age, and stores what it learns.

The cache is stamped with the current time even when the request fails, so that a broken network costs one request a day rather than one per command. On failure the previously known release is kept: it is old information, but it is not wrong, and dropping it would silence the notice for a day.

type Error added in v0.8.0

type Error struct {
	// Status is what GitHub answered, or 0 when the request never got an answer.
	Status int

	// Limit and Reset are GitHub's own account of the hourly budget, and are set
	// only when that budget is what failed the check. Either may be absent: they
	// are read from headers, and a header is not a promise.
	Limit int
	Reset time.Time
	// contains filtered or unexported fields
}

Error is a check GitHub did not answer.

Its whole job is to carry an exit code and, where GitHub said enough for one, a remedy. Every failure of the check gets one — a status, a network that is not there, a body that is not a release — because the answer to all of them is the same: nothing here is broken, ask again later.

func (*Error) Error added in v0.8.0

func (e *Error) Error() string

func (*Error) ExitCode added in v0.8.0

func (e *Error) ExitCode() int

ExitCode is exitcode.Unavailable for every failed check, whatever failed it. Not Forbidden, even for a 403: that code means the *tenant* refused an authenticated request, and a script that reacts to it by trying other credentials would be reacting to the wrong thing entirely. This is GitHub declining to answer a question about a release, and the only useful response to it is to ask again later.

func (*Error) Hint added in v0.8.0

func (e *Error) Hint() string

Hint is where the remedy goes, on its own line under the message.

It names the thing a user cannot guess: the budget is not fft's, and fft asking once a day is very unlikely to be what spent it. FFT_NO_UPDATE_CHECK is offered for the machine where this is chronic — it silences the *background* check, not this command, which always asks.

func (*Error) RateLimited added in v0.8.0

func (e *Error) RateLimited() bool

RateLimited reports whether GitHub refused because the hour's budget is spent, rather than for a reason of its own.

func (*Error) Unwrap added in v0.8.0

func (e *Error) Unwrap() error

Unwrap keeps the cause reachable, which is load-bearing for context.Canceled: exitcode.FromError tests for it before it asks an error for its own code, so without this a Ctrl-C during the check would exit 9 instead of 130.

type Option

type Option func(*Checker)

Option configures a Checker.

func WithClock

func WithClock(now func() time.Time) Option

WithClock replaces time.Now, so that a spec can age the cache without sleeping.

func WithURL

func WithURL(url string) Option

WithURL replaces the release endpoint. Specs point it at an httptest server; production leaves it at DefaultURL.

func WithUpgradeHint added in v0.9.0

func WithUpgradeHint(hint string) Option

WithUpgradeHint replaces the upgrade advice the notice ends with. Production derives it from where the running binary lives; specs pin it so the notice they assert on does not depend on where the test binary was built.

type State

type State struct {
	CheckedAt     time.Time `json:"checkedAt"`
	LatestVersion string    `json:"latestVersion,omitempty"`
	URL           string    `json:"url,omitempty"`
}

State is what the cache file holds: the outcome of the last check.

CheckedAt is stamped even when the check failed, which is the point of it — without it a user with no network would ask GitHub on every single invocation.

Jump to

Keyboard shortcuts

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