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

update

package
v0.43.0 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 22 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 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.

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 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
}

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) → instruct for anything it would otherwise have applied, confirm stays confirm.
  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 Input

type Input struct {
	Current  Version
	Manifest *Manifest
	Policy   Policy
	Mode     InstallMode
	// 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 InstallMode

type InstallMode string

InstallMode is how this filex was installed, which decides whether it can replace itself at all.

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"
)

func DetectInstallMode

func DetectInstallMode() InstallMode

DetectInstallMode inspects the runtime. 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).

func (InstallMode) CanSelfApply

func (m InstallMode) CanSelfApply() bool

CanSelfApply reports whether this install may replace its own binary.

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 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 installs (ghost-upgrade trap)
  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) Enabled

func (s *Service) Enabled() bool

Enabled reports whether checking is on.

func (*Service) Mode

func (s *Service) Mode() InstallMode

Mode exposes the detected install mode (binary vs container).

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