A Go library (SDK) and CLI tool for managing systemd-sysext images, replicating the functionality of systemd-sysupdate for url-file transfers.
See the quality loop (quality dashboard) and public metrics index for the project's live quality, review, and automation signals.
updex provides two ways to manage system extensions:
- Go Library (SDK): Import
github.com/frostyard/updex/v2/updexin your Go applications for programmatic control - CLI Tool: Use the
updexcommand-line tool as a thin wrapper around the SDK
Designed for systems like Debian Trixie that don't ship with systemd-sysupdate.
- Feature-based management of sysext images (enable/disable groups of transfers)
- systemd-sysupdate "component" discovery (
sysupdate.<name>.d/, see sysupdate.d(5) "Components"), with the legacy defaultsysupdate.d/directory folded into the same domain - Catalog integration (
updex catalog list/search/add/remove) for one-command installs from sysext catalogs like fedora-sysexts - Download sysext images from remote HTTP sources
- SHA256 hash verification via size-bounded
SHA256SUMSmanifests - Bounded retry with exponential backoff for transient network failures and HTTP 5xx/429 responses
- GPG signature verification by default, matching systemd-sysupdate (
Verify=noopts out) - Automatic decompression (xz, gz, zstd)
- Version management with configurable retention (
InstancesMax) - Automatic update daemon via systemd timers
- Compatible with standard
.transferand.featureconfiguration files - JSON output for scripting (
--json)
Download the latest CLI package for your system from the GitHub releases page:
| System | Release artifact |
|---|---|
| Debian/Ubuntu | frostyard-updex_<version>_amd64.deb or frostyard-updex_<version>_arm64.deb |
| Fedora/RHEL | frostyard-updex-<version>-1.x86_64.rpm or frostyard-updex-<version>-1.aarch64.rpm |
| Alpine | frostyard-updex_<version>_x86_64.apk or frostyard-updex_<version>_aarch64.apk |
| Other Linux distributions | updex_<version>_linux_amd64.tar.gz or updex_<version>_linux_arm64.tar.gz |
Download checksums.txt from the same release and verify the package before
installing it:
sha256sum --ignore-missing --check checksums.txt
gh attestation verify <downloaded-artifact> --repo frostyard/updexThe checksum detects a corrupt or truncated download; the attestation
(GitHub build provenance, attached by the release workflow) binds the artifact
to a tag release built by frostyard/updex's own workflow, so a file that
merely matches a checksums.txt served from the same place isn't enough.
gh attestation verify needs the GitHub CLI and
works on checksums.txt too.
Running the packaged CLI does not require Go or make. It requires a
systemd-based Linux system with systemd-sysext; operations that modify system
state also require root privileges.
Building the CLI or library and running unit tests requires
Go 1.26.7 (go.mod's toolchain line is the
only Go build pin; the module's minimum is go 1.26.6) and make for the
Makefile commands below. Building and testing do not require systemd.
make build
# Install to GOPATH/bin
make installUsing updex as a Go library requires Go 1.26.6 or newer:
go get github.com/frostyard/updex/v2/updexCreate a module for the example:
mkdir updex-quickstart
cd updex-quickstart
go mod init example.com/updex-quickstart
go get github.com/frostyard/updex/v2/updexSave the following as main.go. The SDK is built around a Client struct
that provides all operations:
package main
import (
"context"
"fmt"
"log"
"github.com/frostyard/updex/v2/updex"
)
func main() {
client := updex.NewClient(updex.ClientConfig{
Verify: true,
})
ctx := context.Background()
// List all features (union of the legacy default directory and every
// discovered systemd-sysupdate component). opts is variadic: omit it
// for the default domain, or pass updex.FeaturesOptions{Component: "docker"}
// to scope to one component.
features, err := client.Features(ctx)
if err != nil {
log.Fatal(err)
}
for _, f := range features {
fmt.Printf("%s: enabled=%v (%s)\n", f.Name, f.Enabled, f.Description)
}
// List discovered components
components, err := client.Components(ctx)
if err != nil {
log.Fatal(err)
}
for _, c := range components {
fmt.Printf("%s: %s (%d features)\n", c.Name, c.SourceDir, c.FeatureCount)
}
// Inspect the reusable automatic-update daemon lifecycle.
daemon, err := client.DaemonStatus(ctx, updex.DaemonStatusOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("daemon installed=%v active=%v\n", daemon.Installed, daemon.Active)
// Enable a feature and download extensions immediately
result, err := client.EnableFeature(ctx, "docker", updex.EnableFeatureOptions{
Now: true,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.NextActionMessage)
// Check for available updates. A non-nil error means at least one
// component could not be checked; the returned results are still
// populated, with CheckResult.Error set on the failed components.
checks, err := client.CheckFeatures(ctx, updex.CheckFeaturesOptions{})
if err != nil {
log.Printf("check incomplete: %v", err)
}
for _, fc := range checks {
for _, c := range fc.Results {
if c.Error != "" {
fmt.Printf("%s: could not check: %s\n", c.Component, c.Error)
continue
}
if c.UpdateAvailable {
fmt.Printf("%s: %s → %s\n", c.Component, c.CurrentVersion, c.NewestVersion)
}
}
}
// Update all enabled features
updates, err := client.UpdateFeatures(ctx, updex.UpdateFeaturesOptions{})
if err != nil {
log.Fatal(err)
}
for _, fu := range updates {
for _, u := range fu.Results {
fmt.Printf("%s: version %s (downloaded=%v)\n", u.Component, u.Version, u.Downloaded)
}
}
// Disable a feature
_, err = client.DisableFeature(ctx, "docker", updex.DisableFeatureOptions{
Now: true,
Force: true,
})
if err != nil {
log.Fatal(err)
}
}Run the example from the module directory:
go run .The enable, update, and disable calls change system state. Run the complete example only on a configured test system with permission to manage system extensions.
| Method | Signature | Description |
|---|---|---|
Features |
Features(ctx, opts ...FeaturesOptions) ([]FeatureInfo, error) |
List all features with status and associated transfers |
EnableFeature |
EnableFeature(ctx, name, EnableFeatureOptions) (*FeatureActionResult, error) |
Enable a feature via drop-in config |
DisableFeature |
DisableFeature(ctx, name, DisableFeatureOptions) (*FeatureActionResult, error) |
Disable a feature via drop-in config |
UpdateFeatures |
UpdateFeatures(ctx, UpdateFeaturesOptions) ([]UpdateFeaturesResult, error) |
Download and install newest versions for all enabled features |
CheckFeatures |
CheckFeatures(ctx, CheckFeaturesOptions) ([]CheckFeaturesResult, error) |
Check if newer versions are available |
Components |
Components(ctx) ([]ComponentInfo, error) |
List discovered systemd-sysupdate components (name, source directory, feature count) |
CatalogList |
CatalogList(ctx, CatalogListOptions) ([]CatalogEntry, error) |
Enumerate sysexts available from configured catalogs |
CatalogAdd |
CatalogAdd(ctx, name, CatalogAddOptions) (*CatalogAddResult, error) |
Install a sysext from a catalog (write definitions, enable, download) |
CatalogRemove |
CatalogRemove(ctx, name, CatalogRemoveOptions) (*CatalogRemoveResult, error) |
Remove a catalog-added sysext and its generated definitions |
EnableDaemon |
EnableDaemon(ctx, EnableDaemonOptions) (*DaemonActionResult, error) |
Install, enable, and start the automatic-update timer |
DisableDaemon |
DisableDaemon(ctx, DisableDaemonOptions) (*DaemonActionResult, error) |
Stop, disable, and remove the automatic-update timer |
DaemonStatus |
DaemonStatus(ctx, DaemonStatusOptions) (*DaemonStatusResult, error) |
Inspect installed, enabled, active, and schedule state |
FeaturesOptions, EnableFeatureOptions, DisableFeatureOptions, UpdateFeaturesOptions, and CheckFeaturesOptions all carry a Component string field that scopes the operation to a single named systemd-sysupdate component instead of the default union domain (see "systemd-sysupdate Components" below). It cannot be combined with a Definitions override on ClientConfig.
type ClientConfig struct {
Definitions string // Custom path to .transfer/.feature files (default: standard paths)
Verify bool // Enable GPG signature verification
Verbose bool // Enable debug-level output
Progress reporter.Reporter // Optional progress reporter
SysextRunner sysext.SysextRunner // Optional mock runner for testing
SystemdManager *systemd.Manager // Optional unit manager and runner for daemon operations
OnDownloadProgress download.ProgressFunc // Optional download progress callback
HTTPClient *http.Client // Optional shared HTTP client
Paths RuntimePaths // Optional instance-scoped filesystem paths (see below)
}
// RuntimePaths holds the filesystem paths an updex.Client consults at runtime.
// Zero values resolve to current production defaults at NewClient time.
// Use this to give two clients different filesystem trees in one process.
type RuntimePaths struct {
DefinitionRoots []string // Roots for sysupdate.d directories
OSReleasePaths []string // os-release files for specifier expansion and image naming
CatalogConfigRoots []string // Dirs scanned for *.catalog repo definitions
CatalogCacheDir string // Cache dir for catalog listings; "" = default user cache; DisableCatalogCache = off
CatalogTargetPath string // Trusted staging dir for catalog transfer files
SysextLinkDir string // Dir where systemd-sysext looks for extension images
RunExtensionsDir string // Dir containing images merged by systemd-sysext; default /run/extensions
}NewClient captures Paths once at construction: zero fields resolve to production defaults by reading each package-level compatibility variable at that moment. After construction the client is not affected by later mutations to those variables, so two clients with different Paths can safely coexist in one process.
type FeaturesOptions struct {
Component string // Scope to a single named component (default: union of all)
}
type EnableFeatureOptions struct {
Now bool // Immediately download extensions after enabling
DryRun bool // Preview changes without modifying filesystem
NoRefresh bool // Skip systemd-sysext refresh after download
Component string // Scope to a single named component (default: union of all)
}
type DisableFeatureOptions struct {
Now bool // Immediately unmerge and remove extension files
Force bool // Allow removal of merged extensions (requires reboot)
DryRun bool // Preview changes without modifying filesystem
NoRefresh bool // Skip systemd-sysext refresh
Component string // Scope to a single named component (default: union of all)
}
type UpdateFeaturesOptions struct {
DryRun bool // Preview changes without modifying filesystem or sysext state
NoRefresh bool // Skip systemd-sysext refresh after update
NoVacuum bool // Skip removing old versions after update
Component string // Scope to a single named component (default: union of all)
}
type CheckFeaturesOptions struct {
Component string // Scope to a single named component (default: union of all)
}# List all features, including where each one came from
updex features list
# FEATURE DESCRIPTION ENABLED CATALOG TRANSFERS
# docker Docker CE yes image:ucore docker
# zoxide zoxide sysext yes fedora zoxide
# mytool Hand-written no local:etc mytool
# Enable a feature (downloads on next update)
sudo updex features enable docker
# Enable and download immediately
sudo updex features enable docker --now
# Disable a feature (stops future updates)
sudo updex features disable docker
# Disable and remove files immediately
sudo updex features disable docker --now
# Force removal of merged extensions
sudo updex features disable docker --now --force
# Update all enabled features
sudo updex features update
# Update without removing old versions
sudo updex features update --no-vacuum
# Preview downloads, installs, refreshes, and vacuum removals
sudo updex --dry-run features update
# If the closing `systemd-sysext refresh` fails after enable --now, disable --now,
# or update, the command reports what it did, prints "Error: sysext refresh
# failed: ..." with the next step (a manual `systemd-sysext refresh` or a reboot),
# and exits non-zero; JSON results carry "refresh_error". After disable --now the
# host's extensions stay unmerged until that refresh happens.
# Check for available updates (read-only). A component whose manifest cannot be
# fetched or verified is listed with UPDATE=error (JSON: "error" set) and the
# command exits non-zero; healthy components in the same run are still reported.
updex features check
# Scope any of the above to a single named component
updex features list --component=docker
sudo updex features update --component=docker
# List discovered systemd-sysupdate components
updex components
# Browse configured sysext catalogs (see "Sysext Catalogs" below)
updex catalog list
updex catalog search zoxide
# Install a sysext from a catalog (writes definitions, enables, downloads)
sudo updex catalog add fedora/zoxide
# Remove it again (definitions, images, and links)
sudo updex catalog remove zoxide
# Enable automatic daily updates
sudo updex daemon enable
# Check auto-update status
updex daemon status
# Disable automatic updates
sudo updex daemon disable| Flag | Description |
|---|---|
-C, --definitions |
Path to directory containing .transfer and .feature files |
--verify |
Verify GPG signatures on SHA256SUMS |
--no-refresh |
Skip running systemd-sysext refresh after install/update |
--json |
Output in JSON format (jq-compatible) |
-n, --dry-run |
Preview changes without modifying filesystem |
-v, --verbose |
Enable verbose output |
-s, --silent |
Suppress progress/reporting noise; with --json, still emit the final machine-readable result |
| Flag | Description |
|---|---|
--component |
Scope the operation to a single named systemd-sysupdate component instead of the default union of the legacy default directory and every discovered component. Cannot be combined with -C, --definitions. |
By default updex reads .transfer and .feature files from the union of two kinds of sources:
The legacy default directories (in priority order):
/etc/sysupdate.d/(highest priority)/run/sysupdate.d//usr/local/lib/sysupdate.d//usr/lib/sysupdate.d/
Every discovered systemd-sysupdate component — a named grouping of .transfer/.feature files under a sysupdate.<name>.d/ directory (see sysupdate.d(5) "Components"), searched across the same four roots with the same priority order, e.g. /etc/sysupdate.docker.d/ overrides /usr/lib/sysupdate.docker.d/. Run updex components to see what's discovered.
Within each source, only the first occurrence of a given filename is used. Feature and transfer names are expected to be globally unique across the whole union (they're derived from distinct sysext names); if a name is defined in more than one source, the most specific source wins — a named component beats the legacy default directory — and the collision is logged as a warning.
Non-sysext transfers that may share the legacy default directory on native OS images (GPT partition targets for an A/B root, or the UKI's regular-file target relative to the ESP) are silently skipped rather than erroring.
The -C, --definitions flag overrides all of the above with a single explicit directory (no component discovery, no union) — the original, pre-component behavior. It cannot be combined with --component. The --component=<name> flag instead scopes the read/write domain to one named component's own search paths.
Create /etc/sysupdate.d/myext.transfer:
[Transfer]
MinVersion=1.0.0
InstancesMax=3
Verify=yes
[Source]
Type=url-file
Path=https://example.com/sysexts
MatchPattern=myext_@v.raw.xz
[Target]
Type=regular-file
Path=/var/lib/extensions.d
MatchPattern=myext_@v.raw
Mode=0644| Option | Description | Default |
|---|---|---|
MinVersion |
Minimum version to consider | (none) |
ProtectVersion |
Version to never remove (supports %A specifiers) |
(none) |
Verify |
Verify GPG signatures | yes |
InstancesMax |
Maximum versions to keep | 2 |
Features |
Space-separated feature names (OR logic) | (none) |
RequisiteFeatures |
Space-separated feature names (AND logic) | (none) |
Omitting Verify= enables signature verification, matching systemd-sysupdate. Set Verify=no explicitly to disable it; the global --verify flag forces verification even for transfers that opt out.
| Option | Description |
|---|---|
Type |
Must be url-file |
Path |
Base URL containing SHA256SUMS and image files |
MatchPattern |
Filename pattern with @v version placeholder |
| Option | Description | Default |
|---|---|---|
Type |
May be omitted (implicit regular-file) or set to regular-file; any other non-empty value is silently skipped |
(none) |
Path |
Target staging directory for downloaded versions | /var/lib/extensions.d |
PathRelativeTo |
If set, updex treats the transfer as a non-sysext OS transfer (an A/B partition or UKI update, per sysupdate.d(5)) and ignores it entirely — it is downloaded and installed by nothing in updex |
(none) |
MatchPattern |
Output filename pattern with @v |
- |
CurrentSymlink |
Optional legacy staging symlink name; if present, updex removes it during update | (none) |
Mode |
File permissions (octal) | 0644 |
ReadOnly |
Parsed for sysupdate.d(5) compatibility; updex does not currently act on it |
no |
The @v placeholder matches version strings in filenames:
myext_@v.raw.xz → matches myext_1.2.3.raw.xz, myext_2.0.0-rc1.raw.xz
kernel_@v.efi → matches kernel_6.1.0.efi
Optional features allow grouping transfers that can be enabled or disabled together. This is useful for optional system components like development tools or proprietary drivers.
Features are defined in .feature files in the same directories as .transfer files.
Create /usr/lib/sysupdate.d/devel.feature:
[Feature]
Description=Development Tools
Documentation=https://example.com/docs/devel
Enabled=falseAdd Features= to a transfer file to associate it with a feature:
[Transfer]
Features=devel
InstancesMax=2
[Source]
Type=url-file
Path=https://example.com/sysexts
MatchPattern=devel-tools_@v.raw.xz
[Target]
Type=regular-file
Path=/var/lib/extensions.d
MatchPattern=devel-tools_@v.rawTransfers with Features= are only active when at least one of the listed features is enabled (OR logic).
Use RequisiteFeatures= when ALL listed features must be enabled (AND logic).
Features are enabled via drop-in configuration files:
# Using updex
sudo updex features enable devel
# Enable and download extensions immediately
sudo updex features enable devel --now
# Or manually create a drop-in
mkdir -p /etc/sysupdate.d/devel.feature.d
echo -e "[Feature]\nEnabled=true" > /etc/sysupdate.d/devel.feature.d/enable.conf| Option | Description | Default |
|---|---|---|
Description |
Human-readable feature description | (none) |
Documentation |
URL to feature documentation | (none) |
AppStream |
URL to AppStream catalog XML | (none) |
Enabled |
Whether the feature is enabled | false |
To completely hide a feature, create a symlink to /dev/null:
ln -s /dev/null /etc/sysupdate.d/devel.featureThe same /dev/null symlink idiom masks a .transfer file.
A component is a named grouping of .transfer/.feature files, used to give a
sysext its own versioning scope separate from the shared default directory
(see sysupdate.d(5) "Components"). Move a sysext's files out of
/usr/lib/sysupdate.d/ into /usr/lib/sysupdate.<name>.d/ and updex picks
them up automatically as component <name>, with the same
/etc > /run > /usr/local/lib > /usr/lib override precedence used for
the legacy default directory:
/usr/lib/sysupdate.docker.d/docker.transfer
/usr/lib/sysupdate.docker.d/docker.feature
List what's discovered:
updex components
# COMPONENT SOURCE FEATURES
# docker /usr/lib/sysupdate.docker.d 1
# incus /usr/lib/sysupdate.incus.d 1updex features list (and every other features subcommand) reads the union
of the legacy default directory and every discovered component by default;
pass --component=<name> to scope to just one. Enabling or disabling a
feature writes its drop-in under the matching scope — a component-scoped
feature gets /etc/sysupdate.<name>.d/<feature>.feature.d/00-updex.conf, a
legacy default (or -C/--definitions-loaded) feature keeps
/etc/sysupdate.d/<feature>.feature.d/00-updex.conf — so reads and writes
always agree on where a feature's overrides live.
updex catalog installs sysexts published by catalogs such as
fedora-sysexts, which serves prebuilt
extensions for Fedora image-based systems (CoreOS/ucore, Silverblue,
Kinoite) from GitHub releases behind stable URLs.
updex ships no built-in catalogs — they only apply to specific systems —
so configure them with <name>.catalog INI files searched across
/etc/updex/catalogs.d/, /run/updex/catalogs.d/,
/usr/local/lib/updex/catalogs.d/, and /usr/lib/updex/catalogs.d/
(earlier directories win per filename). For fedora-sysexts, copy these two
files:
# /etc/updex/catalogs.d/fedora.catalog
[Catalog]
SiteURL=https://extensions.fcos.fr/fedora
ListURL=https://api.github.com/repos/fedora-sysexts/fedora/contents/
# AllowInsecure=no# /etc/updex/catalogs.d/community.catalog
[Catalog]
SiteURL=https://extensions.fcos.fr/community
ListURL=https://api.github.com/repos/fedora-sysexts/community/contents/
# AllowInsecure=noSiteURL(required) — base URL the catalog serves artifacts from; the published<sysext>.conf,SHA256SUMS, and.rawimages all resolve beneath<SiteURL>/<sysext>/. Must use HTTPS unlessAllowInsecure=yes.ListURL(optional) — GitHub contents API endpoint used bycatalog list/searchto enumerate available sysexts.add/removenever use it. Set theGITHUB_TOKENenvironment variable to raise the API rate limit forhttps://api.github.com; credentials are not sent to custom catalog origins, cleartext URLs, or cross-origin redirects. Must use HTTPS unlessAllowInsecure=yes.Component(optional) — systemd-sysupdate component the generated files are written under; defaults tocatalog-<name>(e.g./etc/sysupdate.catalog-fedora.d/).AllowInsecure(optional, defaultno) — permits non-HTTPSSiteURLandListURLvalues only for explicitly trusted development and test endpoints. It does not permitGITHUB_TOKENtransmission to cleartext or custom origins.
Existing catalog files with http:// URLs are a breaking configuration
change: they now fail to load until AllowInsecure=yes is added. Production
catalogs should migrate to HTTPS instead of enabling the escape hatch.
The SDK's default HTTP client also refuses redirects from HTTPS to HTTP, so a
catalog cannot pass initial URL validation and then downgrade .conf,
SHA256SUMS, image, or listing requests to cleartext. A caller-supplied
ClientConfig.HTTPClient retains its own redirect policy.
sudo updex catalog add fedora/zoxide fetches the catalog's published
transfer definition, writes a standard .transfer (with Features=zoxide
injected and security-sensitive source/target fields canonicalized) plus a
generated .feature into the catalog's component directory, enables the
feature, and downloads the image. Catalog transfers must use a url-file
source at the configured <SiteURL>/<sysext>/ path and unquoted,
basename-only match patterns. Their target is always a regular 0644 file under the trusted
staging path (default /var/lib/extensions.d, configurable per client via
RuntimePaths.CatalogTargetPath); catalog-provided target paths, modes,
PathRelativeTo, and CurrentSymlink values cannot redirect root-owned
writes. updex manages the /var/lib/extensions link itself (configurable
per client via RuntimePaths.SysextLinkDir). The %w/%a
specifiers in the catalog's match patterns are kept unexpanded, so updates
keep tracking the running Fedora release across OS upgrades.
From then on the sysext is a completely normal feature: updex features list/enable/disable/update/check and the update daemon manage it like any
hand-written one. The CATALOG column of updex features list still shows
which catalog it came from, so catalog-added sysexts stay distinguishable
from image-shipped (image:<id>) and hand-written (local:etc) ones. sudo updex catalog remove zoxide reverses the add —
disable, unmerge, delete images and the extensions link, and delete the
generated definition files (--force required while the extension is
merged, as with features disable --now).
Generated files carry a # Generated by updex catalog (repo: <name>)
header as an ownership marker, and the repo it names is part of the
check: catalog add refuses to overwrite definitions it did not generate
or that another catalog generated, and catalog remove only touches
files marked by the repo it is acting for — so hand-written,
package-shipped, or other-catalog definitions sharing a name or component
are safe. A failed add restores the previous state exactly: a fresh add
leaves no generated definitions, enable drop-in, staged image, or sysext link,
while a re-add restores its previous definitions, matching staged images, and
link target even after a download, link, vacuum, or final-refresh failure.
Matching staged files are streamed to same-directory rollback snapshots, so
memory use stays bounded while temporary disk use scales with retained image
size; those snapshots are removed after success or rollback. A matching
staging entry or link destination that is neither a regular file nor a symlink
is refused before install rather than replaced. remove refuses up front if the
sysext's .transfer was replaced by one it doesn't own — rather than
tearing down images that definition describes — and deletes only updex's
own 00-updex.conf drop-in, leaving any administrator drop-ins in
<name>.feature.d in place.
Sysexts are referenced as NAME or REPO/NAME; a bare NAME works
whenever it is unambiguous across the configured catalogs, and --repo is
equivalent to the REPO/ prefix.
catalog list/search cache each repo's listing locally (in
~/.cache/updex/, or /root/.cache/updex/ under sudo) for 60 minutes.
After the TTL the listing is revalidated with a conditional request — a
304 Not Modified from the GitHub API costs no rate limit and just
refreshes the cache. --no-cache forces a live query, and when a live
fetch fails (offline, rate-limited) an expired cache is served with a
warning so listing keeps working. add/remove never use the cache.
The source URL must contain a SHA256SUMS file:
a1b2c3d4... myext_1.0.0.raw.xz
e5f6g7h8... myext_1.1.0.raw.xz
i9j0k1l2... myext_1.2.0.raw.xz
For GPG verification, also provide SHA256SUMS.gpg (detached signature).
Fetching SHA256SUMS retries transient network failures and HTTP 5xx/429 responses with exponential backoff. Manifest responses are limited to 4 MiB and detached signature responses to 1 MiB; oversized responses are rejected before parsing or signature verification. The detached signature fetch is verified after the manifest body is fetched.
Use --json for machine-readable output:
updex features list --json | jq '.[] | select(.enabled)'
updex features check --json
# Components that could not be checked carry an "error" field (and the
# command exits non-zero); do not read their absence of update_available as
# "up to date"
updex features check --json | jq '.[].results[] | select(.error != null)'
# Everything added from the fedora catalog
updex features list --json | jq '.[] | select(.origin=="catalog" and .origin_name=="fedora")'The terminal download bar is suppressed in JSON mode, so stdout remains a
valid JSON stream that is safe to pipe directly into parsers such as jq.
updex follows an SDK-first architecture:
- SDK Layer (
updex/package): All operations are implemented as methods on theClientstruct - CLI Layer (
cmd/package): Thin Cobra wrappers that parse flags, call SDK methods, and format output
SDK conventions:
- All methods take
context.Contextas first parameter - Operations use dedicated option structs (e.g.,
EnableFeatureOptions) for future extensibility - Return dedicated result structs with status fields + error
- Error messages: lowercase, no trailing punctuation, wrapped with
fmt.Errorf
When adding features:
- Implement as a method on
Clientinupdex/*.go - Create CLI wrapper in
cmd/updex/*.go - CLI commands should only handle argument parsing and output formatting
# Format code (always run after changes)
make fmt
# Run linters
make lint
# Run tests
make test
# Quick format, lint, and test loop (rewrites files via gofmt -w)
make check
# Non-mutating, credential-free gate for read-only review: tidy diff, vet, gofmt -l, lint, tests
make verify
# Run the credential-free gate that mirrors CI
make ci
# Build binaries
make build
# Clean build artifacts
make clean
# Run end-to-end tests (builds and runs the real updex binary)
go test -v ./tests/e2e/...tests/e2e/ contains black-box tests that build the real updex binary
and run it as a subprocess against a fake HTTP transfer source, the same
way an operator would invoke it. The suite covers help, version and shell
completion output; argument and exit-code handling for every command variant;
configuration errors; and text/JSON feature listing and update checks.
Successful operations are read-only so the tests run without root privileges.
Additional CLI integration tests in cmd/updex/ use temporary search roots
and a fake catalog server to cover default component discovery and
GITHUB_TOKEN request authentication.
See AGENTS.md (CONTRIBUTING.md resolves to it) for the full
guide. In short:
- Keep the SDK layer free of CLI dependencies (no Cobra, pflag, etc.)
- SDK functions should return structured data, not formatted output
- CLI commands should be thin wrappers around SDK functions
- Write tests for both SDK and CLI layers
- Run
make cibefore submitting PRs
MIT