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
- Variables
- type Action
- type Asset
- type Config
- type Decision
- type Input
- type InstallMode
- type Manifest
- type Policy
- type PreApply
- type Release
- type Service
- func (s *Service) Apply(ctx context.Context, target Release) error
- func (s *Service) Check(ctx context.Context) (Decision, error)
- func (s *Service) Decision() (Decision, bool)
- func (s *Service) Enabled() bool
- func (s *Service) Mode() InstallMode
- func (s *Service) Policy() Policy
- func (s *Service) RestartRequired() bool
- func (s *Service) Run(ctx context.Context)
- func (s *Service) SetPreApply(fn PreApply)
- func (s *Service) State() State
- type State
- type Step
- type Version
- type Window
Constants ¶
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 ¶
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 ¶
Decide resolves what to do. The rules, in order:
- Nothing newer → none.
- Below a release's MinVersion → instruct (must step through).
- Major move → instruct, always. A major is a decision, not an event.
- Cannot self-apply (docker) → instruct for anything it would otherwise have applied, confirm stays confirm.
- Patch: auto when the policy allows AND the release is auto_ok AND no migration is involved; otherwise confirm.
- 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.
- 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 ¶
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.
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 ¶
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 ¶
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) IsSecurity ¶
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 (*Service) Apply ¶
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:
- refuse outright on container installs (ghost-upgrade trap)
- download + SHA-256 verify (against the manifest, over TLS)
- unpack to a temp file NEXT TO the current binary (same filesystem, so the final move is atomic rather than a cross-device copy)
- smoke-test the new binary by running `filex --version` — catches a truncated download, wrong architecture, or a corrupt archive
- PreApply hook (database snapshot)
- keep the old binary as filex.bak-<version>, then rename into place
- restart if a supervisor is detected; otherwise report "restart required"
Everything before step 6 is reversible by doing nothing.
func (*Service) Check ¶
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) Mode ¶
func (s *Service) Mode() InstallMode
Mode exposes the detected install mode (binary vs container).
func (*Service) RestartRequired ¶
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 ¶
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 ¶
SetPreApply installs the pre-upgrade hook.
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 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 ¶
ParseVersion accepts "v0.7.6", "0.7.6", "0.7.6-rc.1" and "0.1.0-dev".
func (Version) Compare ¶
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) Newer ¶
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.
type Window ¶
Window is a daily maintenance window, e.g. 03:00–05:00 local time. The zero value allows any time.
func ParseWindow ¶
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.