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 Install
- type InstallMode
- type Manifest
- type PackageManagedError
- type PackageManager
- 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) Install() Install
- 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 ( 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.
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. 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 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 ¶
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, package manager) → instruct, for anything it would otherwise have applied as well as for what it would have offered.
- 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
// 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
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
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 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 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 ¶
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 and package-manager installs (ghost-upgrade trap; a binary the package manager does not know about)
- 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) Install ¶ added in v0.44.2
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) 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.