Help us shape the future of Go! Take the 2026 Go Developer Survey

update

package
v0.45.0 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package update implements filex's release awareness: it learns which versions exist, decides what may be applied automatically, and (on installs that own their binary) applies them.

The policy is expressed in terms of which part of x.y.z moves:

z (patch) → applied automatically when the policy allows it
y (minor) → announced; the operator confirms with one click
x (major) → announced only, with upgrade instructions

The asymmetry is deliberate. A patch is a bugfix on a known-good shape; a minor may add a migration or change an embedded API; a major is a migration the operator should read about first. See docs/UPDATES.md.

Index

Constants

View Source
const (
	DefaultHomebrewCask = "filex"
	DefaultWingetID     = "BRFTech.filex"
	DefaultSnapName     = "filex"
)

The names filex is published under. They are used when the operator names only the manager (FILEX_INSTALL_MODE=homebrew) and the path does not say more; a detected install reads its own name from where it lives, so a fork or a renamed package is told the command for ITS package.

View Source
const DefaultManifestURL = "https://filex.sh/updates/stable.json"

DefaultManifestURL is where filex looks unless FILEX_UPDATE_MANIFEST_URL overrides it. Static JSON, no auth, no telemetry sent — the request carries only the User-Agent below.

Variables

View Source
var ErrNotSelfApplicable = errors.New("this install cannot upgrade itself (container image is immutable); upgrade the image instead")

ErrNotSelfApplicable is returned when the install cannot replace itself — the container case. It is a permanent condition, not a transient failure. A package-managed install answers with a *PackageManagedError, which is also this error under errors.Is (see Install.Refusal).

Functions

This section is empty.

Types

type Action

type Action string

Action is what the install should do about an available release.

const (
	// ActionNone — already current.
	ActionNone Action = "none"
	// ActionAuto — apply without asking (patch under an allowing policy).
	ActionAuto Action = "auto"
	// ActionConfirm — offer a one-click upgrade; the operator decides.
	ActionConfirm Action = "confirm"
	// ActionInstruct — show upgrade instructions; filex cannot do it itself.
	ActionInstruct Action = "instruct"
)

type Asset

type Asset struct {
	OS     string `json:"os"`     // "linux", "darwin", "windows"
	Arch   string `json:"arch"`   // "amd64", "arm64"
	URL    string `json:"url"`    // absolute https URL of a .tar.gz/.zip
	SHA256 string `json:"sha256"` // lowercase hex digest of the archive
	// Binary is the path of the filex executable INSIDE the archive. Empty
	// defaults to "filex" (goreleaser's layout).
	Binary string `json:"binary,omitempty"`
}

Asset is one downloadable build artifact.

type Behavior added in v0.45.0

type Behavior string

Behavior is what an install does about new releases by itself: its saved policy, as far as this install can carry it out. It is what the admin page's policy badge says, so it is worked out HERE, once, from the same rules Decide applies — a client renders it and never re-derives it from the mode and the policy.

const (
	// BehaviorOff — nothing is checked, so nothing is announced.
	BehaviorOff Behavior = "off"
	// BehaviorAnnounce — new releases are checked for and announced; nothing
	// is applied by itself.
	BehaviorAnnounce Behavior = "announce"
	// BehaviorPatch — patch releases are applied by themselves.
	BehaviorPatch Behavior = "patch"
	// BehaviorMinor — patch and minor releases are applied by themselves.
	BehaviorMinor Behavior = "minor"
)

type Config

type Config struct {
	// Enabled turns the whole feature off (no outbound request at all).
	Enabled bool
	// Policy is how far this install may move on its own.
	Policy Policy
	// Channel selects the manifest flavor; informational unless ManifestURL is
	// templated by the operator.
	Channel string
	// ManifestURL overrides DefaultManifestURL (air-gapped mirrors, forks).
	ManifestURL string
	// Window restricts automatic application to a daily maintenance window.
	Window Window
	// Interval between checks. Zero → 24h. Values below an hour are raised to
	// an hour: a self-hosted install has no reason to poll harder, and the
	// endpoint is shared by everyone.
	Interval time.Duration
	// StateDir is where the last-seen state is persisted (usually DataDir).
	StateDir string
	// CurrentVersion is the running build (version.Version).
	CurrentVersion string
	// Install, when its Mode is set, is used instead of DetectInstall — for
	// tests, which must never describe the machine they run on. The
	// operator's switch is FILEX_INSTALL_MODE, which DetectInstall reads.
	Install Install
}

Config is the runtime configuration of the updater.

type Decision

type Decision struct {
	Action Action
	Step   Step
	Target Release
	// Reason is the decision in English, for the log and the webhook.
	Reason string
	// ReasonKey + ReasonVars are the same sentence as a key of the server
	// catalogue (srvtext, `server.update.reason.*`): the admin page says it
	// in the reader's language. ⚠ It printed Reason, so the Turkish panel read
	// "policy is manual — updates are announced, not applied"
	// (release-candidate sweep, 2026-09-21).
	ReasonKey  string
	ReasonVars map[string]string
	Skipped    []Release // releases between current and target (informational)
}

Decision is the full answer, including WHY — the reason is shown in the UI and logged, because "there is an update but I am not taking it" is exactly the state an operator needs explained.

func Decide

func Decide(in Input) Decision

Decide resolves what to do. The rules, in order:

  1. Nothing newer → none.
  2. Below a release's MinVersion → instruct (must step through).
  3. Major move → instruct, always. A major is a decision, not an event.
  4. Cannot self-apply (docker, package manager) → instruct, for anything it would otherwise have applied as well as for what it would have offered.
  5. Patch: auto when the policy allows AND the release is auto_ok AND no migration is involved; otherwise confirm.
  6. Minor: auto only under PolicyMinor and only when Current.Major > 0 — while a project is on 0.x, semver gives minor no compatibility promise. Otherwise confirm.
  7. Outside the maintenance window, an auto becomes a deferred auto (still ActionAuto, but Ready reports false — see Window.Allows).

type Effective added in v0.45.0

type Effective struct {
	// Policy is the saved policy, unchanged.
	Policy   Policy
	Behavior Behavior
	// Limit is why Behavior is less than Policy; LimitNone when it is not.
	Limit Limit
}

Effective is a saved policy as one install carries it out.

func EffectiveOf added in v0.45.0

func EffectiveOf(p Policy, checking bool, mode InstallMode, current Version) Effective

EffectiveOf is what an install does by itself, from its saved policy, whether checking is on, who owns the binary and the running version. The rules are Decide's, read without a release in hand:

  • policy off is off, and that is the policy in force;
  • with checking switched off nothing happens, whatever the policy says;
  • a policy that would apply something, on an install that cannot replace its own binary (a container, a package manager's), only announces;
  • policy minor on 0.x applies patches only.

func (Effective) InForce added in v0.45.0

func (e Effective) InForce() bool

InForce reports whether the install does what its saved policy says.

type Input

type Input struct {
	Current  Version
	Manifest *Manifest
	Policy   Policy
	Mode     InstallMode
	// Manager is the package manager of a ModePackage install, named in the
	// reason; empty when it is not known.
	Manager PackageManager
	// Now and Window gate automatic application to a maintenance window. A zero
	// Window means "any time".
	Now    time.Time
	Window Window
}

Input is everything Decide needs. It takes no globals so the whole policy is testable as a pure function.

type Install added in v0.44.2

type Install struct {
	Mode InstallMode
	// Manager owns the binary (ModePackage only). Empty when
	// FILEX_INSTALL_MODE=package was set and nothing could be detected — a
	// distribution package (.deb, .rpm, AUR) whose unit file says so.
	Manager PackageManager
	// Package is the name the manager knows filex by: the cask token or
	// formula name, the winget package id, the snap instance name.
	Package string
	// Cask is true for a Homebrew cask, false for a formula. The two are
	// upgraded by different commands.
	Cask bool
}

Install is how this filex was installed: the mode decides whether it may replace its own binary, the rest says who does it instead.

func DetectInstall added in v0.44.2

func DetectInstall() Install

DetectInstall inspects the runtime: FILEX_INSTALL_MODE first, then a container, then a package manager's layout around the running binary, and otherwise a plain binary.

func (Install) Refusal added in v0.44.2

func (i Install) Refusal() error

Refusal is the error a self-upgrade of this install answers with, nil when the install can replace itself.

func (Install) UpgradeCommand added in v0.44.2

func (i Install) UpgradeCommand() string

UpgradeCommand is the command that upgrades a packaged install, or "" when the manager is unknown (or the install is not packaged).

type InstallMode

type InstallMode string

InstallMode is how this filex was installed, which decides whether it can replace itself at all. It answers "who owns the binary on disk?" — filex, an image, or a package manager — and stays a small closed set: the policy and the admin page branch on it, while WHICH package manager (install.go) only changes the command an operator is shown.

const (
	// ModeBinary — a binary on disk (systemd, bare process). filex can swap it.
	ModeBinary InstallMode = "binary"
	// ModeDocker — running inside a container. filex CANNOT upgrade itself: the
	// image layer is immutable, so a replaced binary vanishes at the next
	// `docker compose up` and the version silently reverts. Container installs
	// get instructions (or an external updater), never a self-apply.
	ModeDocker InstallMode = "docker"
	// ModePackage — a package manager (Homebrew, winget, Snap) owns the
	// binary. filex does not replace it either: the manager's record would
	// still name the old version, its next upgrade would write over whatever
	// filex put there, and a snap's squashfs is read-only to begin with. The
	// manager's own upgrade command is the instruction.
	ModePackage InstallMode = "package"
)

func DetectInstallMode

func DetectInstallMode() InstallMode

DetectInstallMode inspects the runtime (see DetectInstall). FILEX_INSTALL_MODE overrides it for setups the heuristics cannot see (e.g. a binary inside a container image that is intentionally managed as a binary, or a distribution package whose unit file sets `package`).

func (InstallMode) CanSelfApply

func (m InstallMode) CanSelfApply() bool

CanSelfApply reports whether this install may replace its own binary.

type Limit added in v0.45.0

type Limit string

Limit is why an install does less than its saved policy asks for. The policy itself is kept as the operator set it: it takes effect again when the reason goes (the install becomes a plain binary, checking is switched back on, filex reaches 1.0), so the page says it is inert here instead of pretending it was never set.

const (
	// LimitNone — the saved policy is what the install does.
	LimitNone Limit = ""
	// LimitDisabled — checking is switched off (FILEX_UPDATE_CHECK=0).
	LimitDisabled Limit = "disabled"
	// LimitContainer — the image owns the binary (ModeDocker).
	LimitContainer Limit = "container"
	// LimitPackage — a package manager owns the binary (ModePackage).
	LimitPackage Limit = "package"
	// LimitZeroMajor — policy minor on a 0.x version, where a minor release
	// is never automatic (Decide rule 6): patches only.
	LimitZeroMajor Limit = "zero_major"
)

type Manifest

type Manifest struct {
	// Channel this document describes ("stable", "beta"…). Informational.
	Channel string `json:"channel"`
	// Releases, newest first is NOT required — Latest sorts them.
	Releases []Release `json:"releases"`
}

Manifest is the release index an install polls. It is deliberately OUR document rather than a straight read of the git tags, because two of its fields cannot be derived from a tag:

  • AutoOK lets a bad release be pulled back from automatic distribution without deleting the tag — a kill switch. Once a version is out, the only other lever is asking every operator to act.
  • Migrations marks releases that change the schema. Patch releases must not carry migrations (see docs/UPDATES.md); the flag makes that checkable instead of a promise, and the policy engine refuses to auto-apply a patch that sets it.

The document is served as static JSON, so it can live anywhere: a CDN, a GitHub release asset, or an air-gapped mirror (FILEX_UPDATE_MANIFEST_URL).

func Fetch

func Fetch(ctx context.Context, client *http.Client, url, currentVersion string) (*Manifest, error)

Fetch downloads and parses the manifest. Failures are ordinary: an install with no outbound network must not log errors every hour, so callers treat a fetch error as "unknown", not as a fault.

func (*Manifest) Between

func (m *Manifest) Between(from, to Version) []Release

Between returns the releases strictly newer than from, up to and including to, oldest first. Used to answer "what am I about to skip over?" — the upgrade dialog shows it, and the policy engine scans it for migrations.

func (*Manifest) Latest

func (m *Manifest) Latest() (Release, bool)

Latest returns the newest non-pre-release in the manifest, or false when the document is empty. Ordering is computed here rather than trusted from the document, so a hand-edited manifest cannot mis-order releases.

type PackageManagedError added in v0.44.2

type PackageManagedError struct{ Install Install }

PackageManagedError refuses a self-upgrade of a binary a package manager owns. It is also ErrNotSelfApplicable (errors.Is), so a caller that only asks "can this install replace itself?" needs no second check.

func (*PackageManagedError) Error added in v0.44.2

func (e *PackageManagedError) Error() string

func (*PackageManagedError) Is added in v0.44.2

func (e *PackageManagedError) Is(target error) bool

Is makes errors.Is(err, ErrNotSelfApplicable) hold.

type PackageManager added in v0.44.2

type PackageManager string

PackageManager names the package manager that owns a packaged install.

const (
	// ManagerHomebrew — a cask (Caskroom) or a formula (Cellar), macOS and Linux.
	ManagerHomebrew PackageManager = "homebrew"
	// ManagerWinget — a winget "portable" package, unpacked under
	// …\WinGet\Packages\<id>_<source>\ and linked from …\WinGet\Links.
	ManagerWinget PackageManager = "winget"
	// ManagerSnap — a snap: a read-only squashfs mounted at $SNAP.
	ManagerSnap PackageManager = "snap"
)

func (PackageManager) Label added in v0.44.2

func (m PackageManager) Label() string

Label is the manager's product name as a person writes it. A proper noun: the same in every language, so it is not a catalogue entry.

type Policy

type Policy string

Policy is how far an install is willing to move on its own.

const (
	// PolicyOff disables the update check entirely — no outbound request.
	PolicyOff Policy = "off"
	// PolicyManual checks and announces, but never applies anything by itself.
	PolicyManual Policy = "manual"
	// PolicyPatch (the default) applies z-moves automatically; y and x are
	// announced. This is what AUTO_UPGRADE=true selects.
	PolicyPatch Policy = "patch"
	// PolicyMinor also applies y-moves automatically. Refused while the
	// install is on a 0.x version — see Decide.
	PolicyMinor Policy = "minor"
)

func ParsePolicy

func ParsePolicy(s string) Policy

ParsePolicy maps config text to a Policy, falling back to PolicyManual for anything unrecognized: an unreadable setting must not silently grant MORE automation than the operator asked for.

type PreApply

type PreApply func(ctx context.Context, target Release) error

PreApply, when set, runs immediately before the binary is swapped — this is where a database snapshot is taken. A non-nil error aborts the upgrade with nothing changed, which is the point: no backup, no upgrade.

type Release

type Release struct {
	Version string `json:"version"` // "v0.7.6"
	Date    string `json:"date"`    // ISO-8601 date, display only
	// AutoOK false = never apply automatically (kill switch), even for a patch.
	// Absent is treated as TRUE by the JSON default, so the field must be
	// written explicitly by the release tooling; MarshalJSON on the producer
	// side always emits it.
	AutoOK bool `json:"auto_ok"`
	// Migrations true = this release changes the database schema.
	Migrations bool `json:"migrations"`
	// MinVersion, when set, is the oldest version that may upgrade DIRECTLY to
	// this one. Older installs must step through it. Empty = no floor.
	MinVersion string `json:"min_version,omitempty"`
	// Notes is a short human summary; NotesURL points at the full changelog.
	Notes    string `json:"notes,omitempty"`
	NotesURL string `json:"notes_url,omitempty"`
	// Severity ("security" | "normal") — a security release is surfaced even
	// when the policy would otherwise stay quiet.
	Severity string `json:"severity,omitempty"`
	// Assets are the downloadable binaries for a self-applying install, each
	// with its SHA-256. The digest lives HERE rather than in a separate
	// checksums file so there is one authenticated document to trust: the
	// manifest arrives over TLS from a host we control, and everything else is
	// verified against it.
	Assets []Asset `json:"assets,omitempty"`
	// Image is the container reference for this release, shown in the upgrade
	// instructions a container install receives.
	Image string `json:"image,omitempty"`
}

Release is one published version.

func (Release) AssetFor

func (r Release) AssetFor(goos, goarch string) (Asset, bool)

AssetFor returns the asset matching an os/arch pair.

func (Release) IsSecurity

func (r Release) IsSecurity() bool

IsSecurity reports whether this release is flagged as a security fix.

type Service

type Service struct {

	// OnNewRelease, when set, is called once per newly discovered version —
	// wired to notifications. "Once" is tracked in the persisted state, so a
	// restart loop cannot spam the operator.
	OnNewRelease func(Decision)
	// contains filtered or unexported fields
}

Service owns the periodic release check and caches its outcome so the admin UI can render instantly without touching the network on every page load.

It is intentionally passive: Check() only LEARNS. Applying is a separate, explicit call (Apply), so a bug in the checker can never move an install.

func New

func New(cfg Config) *Service

New builds a Service. It does not perform any I/O.

func (*Service) Apply

func (s *Service) Apply(ctx context.Context, target Release) error

Apply downloads, verifies and installs the target release, then asks the supervisor to restart. The sequence is deliberately ordered so that every failure mode leaves a WORKING install behind:

  1. refuse outright on container and package-manager installs (ghost-upgrade trap; a binary the package manager does not know about)
  2. download + SHA-256 verify (against the manifest, over TLS)
  3. unpack to a temp file NEXT TO the current binary (same filesystem, so the final move is atomic rather than a cross-device copy)
  4. smoke-test the new binary by running `filex --version` — catches a truncated download, wrong architecture, or a corrupt archive
  5. PreApply hook (database snapshot)
  6. keep the old binary as filex.bak-<version>, then rename into place
  7. restart if a supervisor is detected; otherwise report "restart required"

Everything before step 6 is reversible by doing nothing.

func (*Service) Check

func (s *Service) Check(ctx context.Context) (Decision, error)

Check fetches the manifest and recomputes the decision. A network failure is recorded, not returned as a fault to the caller's health: an install without outbound access is a valid deployment, not a broken one.

func (*Service) Decision

func (s *Service) Decision() (Decision, bool)

Decision returns the cached decision without touching the network.

func (*Service) Effective added in v0.45.0

func (s *Service) Effective() Effective

Effective is the saved policy as this install carries it out (EffectiveOf): what the admin page's policy badge says. An unparsable running version counts as 0.x — the reading that promises less.

func (*Service) Enabled

func (s *Service) Enabled() bool

Enabled reports whether checking is on.

func (*Service) Install added in v0.44.2

func (s *Service) Install() Install

Install exposes the whole detection: for a packaged install, which manager owns the binary and the command that upgrades it.

func (*Service) Mode

func (s *Service) Mode() InstallMode

Mode exposes the detected install mode (binary, container, package).

func (*Service) Policy

func (s *Service) Policy() Policy

Policy exposes the configured policy.

func (*Service) RestartRequired

func (s *Service) RestartRequired() bool

RestartRequired reports whether a new binary is installed but the running process is still the old one — surfaced in the admin UI so the state is never invisible.

func (*Service) Run

func (s *Service) Run(ctx context.Context)

Run starts the periodic loop until ctx is cancelled. The first check is delayed by a short jitter-free grace period so a fleet restarting together does not stampede the endpoint at boot.

func (*Service) SetPreApply

func (s *Service) SetPreApply(fn PreApply)

SetPreApply installs the pre-upgrade hook.

func (*Service) State

func (s *Service) State() State

State returns a copy of the persisted state (for the admin API).

type State

type State struct {
	LastCheck   time.Time `json:"last_check"`
	LastError   string    `json:"last_error,omitempty"`
	LatestSeen  string    `json:"latest_seen,omitempty"`
	NotifiedFor string    `json:"notified_for,omitempty"`
	// LastApplied records a self-update this install performed, for the audit
	// trail the UI shows ("upgraded 0.7.5 → 0.7.6 on …").
	LastApplied    string    `json:"last_applied,omitempty"`
	LastAppliedAt  time.Time `json:"last_applied_at,omitempty"`
	LastApplyError string    `json:"last_apply_error,omitempty"`
	// contains filtered or unexported fields
}

State is the cached result of the last check, persisted across restarts.

type Step

type Step string

Step names the largest version component that moved between v and o.

const (
	StepNone  Step = "none"  // same version, or o is older
	StepPatch Step = "patch" // z moved
	StepMinor Step = "minor" // y moved
	StepMajor Step = "major" // x moved
)

type Version

type Version struct {
	Major, Minor, Patch int
	Pre                 string // "rc.1" in 1.2.3-rc.1 (without the dash)
	Raw                 string // exactly as parsed, e.g. "v0.7.6"
}

Version is a parsed semantic version. Pre-release and build metadata are kept for display but ignored when ranking (a pre-release never wins an automatic upgrade — see Newer).

func ParseVersion

func ParseVersion(s string) (Version, error)

ParseVersion accepts "v0.7.6", "0.7.6", "0.7.6-rc.1" and "0.1.0-dev".

func (Version) Compare

func (v Version) Compare(o Version) int

Compare returns -1, 0 or +1 ordering v against o. A pre-release sorts BELOW the same x.y.z release (0.7.6-rc.1 < 0.7.6), per semver §11.

func (Version) IsPre

func (v Version) IsPre() bool

IsPre reports whether this is a pre-release (rc, dev, beta…).

func (Version) Newer

func (v Version) Newer(o Version) bool

Newer reports whether o is a release strictly newer than v. Pre-releases are never "newer" for update purposes: an install must not drift onto an rc because someone tagged one.

func (Version) StepTo

func (v Version) StepTo(o Version) Step

StepTo classifies the jump from v to o.

func (Version) String

func (v Version) String() string

String renders the canonical "v0.7.6" / "v0.7.6-rc.1" form.

type Window

type Window struct {
	FromMin, ToMin int // minutes since midnight; equal values = always
	Set            bool
}

Window is a daily maintenance window, e.g. 03:00–05:00 local time. The zero value allows any time.

func ParseWindow

func ParseWindow(s string) Window

ParseWindow reads "03:00-05:00". An unparsable value yields an unset window (always allowed) rather than an error: a typo in a schedule must not stop security patches from landing.

func (Window) Allows

func (w Window) Allows(t time.Time) bool

Allows reports whether an automatic apply may run at t. Windows that cross midnight (22:00-04:00) are supported.

Jump to

Keyboard shortcuts

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