Universal Blue Build and Update System

Universal Blue Build and Update System

The Universal Blue Build and Update System is the comprehensive infrastructure encompassing GitHub Actions CI/CD pipelines, OCI container delivery, and multi-layered automatic update architecture that powers Universal Blue and its desktop variants including Bluefin and Aurora. This system represents a distinct technical area separate from both the Universal Blue platform and the underlying rpm-ostree/bootc tooling, providing project-specific build orchestration and update mechanisms.

The infrastructure consists of three integrated components: (1) a GitHub Actions-based CI/CD pipeline that triggers builds on every main branch commit, with lts branch builds triggered exclusively via a dispatcher workflow that runs weekly (Tuesday 6 AM UTC / 1 AM EST / 2 AM EDT) or manual dispatch, typically delivering changes within 30 minutes to 2 hours; (2) OCI container-based delivery where operating systems are delivered as complete container images published to ghcr.io; and (3) a three-layer update architecture combining bootc for system updates, Flatpak for GUI applications, and Homebrew for CLI tools. The system implements intelligent build triggers using digest-based version tracking, comprehensive validation including secure boot verification, and advanced optimizations like rechunking for efficient delta updates.

Built on Fedora Silverblue and Kinoite foundations, the system underwent major architectural refactoring in 2025 from monolithic distributions to modular OCI containers, fully embracing bootc (bootable containers) as the primary upgrade mechanism. The architecture ensures system images remain pristine with no package-based degradation over time while enabling users to switch between variants and update streams seamlessly.

Build Infrastructure and CI/CD Pipeline

GitHub Actions Architecture

The Universal Blue project employs a reusable workflow system with a two-job structure: the check-build-required job determines if builds are needed, and the build_ublue job executes the actual build pipeline. Bluefin extends this with a matrix strategy for parallel builds across multiple variants and flavors.

The pipeline executes these key stages:

  1. Checkout code
  2. Install Cosign for image signing
  3. Build container image using just build-container
  4. Verify secure boot with just secureboot
  5. Push to GitHub Container Registry
  6. Sign container with Cosign
  7. Generate and attach SBOM (Bluefin only)

Multi-Stage Container Build

The build process uses a four-stage Containerfile:

Variant-specific builds integrate ghcr.io/projectbluefin/common for shared configurations and ghcr.io/ublue-os/brew for Homebrew support.

Intelligent Build Triggers

The system implements digest-based triggering that compares SHA256 hashes in image-versions.yaml between branches to determine if builds are required. Build trigger behavior differs by branch:

Main branch triggers:

LTS branch triggers:

This architecture prevents accidental production tag publishing while maintaining validation builds on merges. The validation builds on lts push events provide a third layer of defense against branch pollution by verifying code integrity when promotions complete.

Dispatcher Workflow Architecture:

The dispatcher pattern solves a GitHub Actions constraint where scheduled triggers always execute on the default branch. The scheduled-lts-release.yml workflow runs on main and uses GitHub CLI to trigger all 5 build workflows on the lts branch via workflow_dispatch events. This ensures production releases build from the stable lts branch code rather than the more frequently updated main branch. See docs/plans/2026-03-02-fix-lts-tag-publishing.md for complete implementation details.

Branch Management and Promotion Strategy:

The system implements a three-layer defense strategy to protect the lts production branch from accidental pollution:

Layer 1: Manual Promotion Workflow - A create-lts-pr.yml workflow automates PR creation from main to lts when content differs. The workflow:

NEVER squash-merge promotion PRs. Squash-merge creates orphan commits that permanently break the merge base between main and lts, causing every future promotion PR to accumulate all historical commits in its diff. Regular merge preserves the merge base and keeps future PRs clean.

Layer 2: Renovate Restriction - Renovate configuration restricts dependency updates to target only the main branch via baseBranchPatterns: ["main"]. This prevents automated dependency PRs from targeting the lts branch, ensuring the production branch receives updates only through manual promotion.

Layer 3: Validation Build Triggers - All 5 build workflows (build-dx-hwe.yml, build-dx.yml, build-gdx.yml, build-regular-hwe.yml, build-regular.yml) include lts in their push triggers. When promotions push to lts, validation builds execute to verify code integrity, but these builds do NOT publish production tags. Publishing only occurs via workflow_dispatch events (weekly dispatcher or manual triggers).

This three-layer architecture prevents branch pollution incidents where AI agents or automated tools merge ltsmain (wrong direction) when detecting divergence in image-versions.yaml digests. Such divergence is intentional—main has newer versions for testing while lts maintains stable production versions.

Changes to image-versions.yaml itself are explicitly excluded to prevent build loops.

Build Validation and Safety Checks

The pipeline enforces multiple validation layers:

Bootc Container Lint: Validates bootable container standards during build using the bootc container lint command embedded in the Containerfile.

Secure Boot Verification: Comprehensive kernel signature verification using sbverify:

  1. Extracts vmlinuz from built container image
  2. Downloads public certificates from akmods repository
  3. Verifies kernel signatures against both certificates

If sbverify is unavailable locally, the validation spawns an Alpine container with sbsigntool for verification.

Image Signing with Cosign

Container image signing occurs after successful push using Cosign, with SIGNING_SECRET stored in GitHub secrets. The signing process uses the digest from the push step to ensure signature covers the exact pushed image.

SBOM Generation

SBOM (Software Bill of Materials) generation occurs only on the lts branch when images are published. The system uses Syft to scan container images and generate SBOMs, which are attached as attestations to signed images for supply chain transparency. All SBOM-related steps include continue-on-error: true to ensure that SBOM failures (such as Sigstore/Rekor service outages) never block image publishing.

SBOM generation behavior:

SBOMs are generated exclusively when github.ref == 'refs/heads/lts' && inputs.publish evaluates to true:

The sbom: input parameter has been removed from reusable-build-image.yml. SBOM behavior is controlled entirely by step-level conditions checking the branch reference and publish status. The workflow defaults publish to false for safety—callers must explicitly opt in to publishing.

For LTS production releases, SBOMs are generated weekly through the dispatcher pattern. The ublue-os/main repository has SBOM generation disabled but implementation preserved.

Rechunking for Efficient Updates

Bluefin implements a three-stage rechunking process using ghcr.io/ublue-os/legacy-rechunk.0.1 to optimize container layer boundaries for delta updates:

PhaseOperationPurpose
1. PrunePrepares filesystem for ostree conversionRemoves incompatible files
2. Create OSTreeBuilds ostree repository from container filesystemEnables content-addressable storage
3. RechunkOptimizes layer boundaries against previous versionMinimizes delta update size

Phase 3 compares against the previous image version using PREV_REF to intelligently reorganize layer boundaries, reducing bandwidth requirements for updates.

Matrix Builds and Parallelization

Build parallelization strategies differ between repositories:

ublue-os/main: Executes 6 parallel builds (3 images × 2 variants):

Bluefin: Runs 4 builds per stream (2 base_names × 2 flavors):

Separate workflow files handle each version stream (gts, stable, latest, beta) with stream-specific kernel pinning and scheduling.

Container Registry Delivery

Images are pushed to ghcr.io with retry logic to handle transient network failures:

Conditional Publishing:

Bug Fix (PR #1154 & #1157): Critical bugs were fixed in the manifest generation and publishing steps:

  • Manifest Step (PR #1154): The manifest generation step had inconsistent conditional logic compared to the build step. The build step correctly added -testing suffix for all non-production branches, but the manifest generation step only added it for pull requests and merge groups—omitting pushes to main. This caused main branch merges to accidentally push images to the production :lts tag instead of :lts-testing. PR #1154 resolved this by aligning manifest step logic with build step logic: both now use if [ "${REF_NAME}" != "${PRODUCTION_BRANCH}" ], ensuring consistent tagging behavior.
  • Push Manifest and Sign Steps (PR #1157): Both the "Push Manifest" and "sign" steps used github.event_name != 'pull_request' conditionals, which caused them to fire even when inputs.publish was false. This resulted in "image not known" errors during validation builds on lts push events. PR #1157 fixed this by gating both steps on inputs.publish instead, ensuring they only execute when publishing is intended. The same fix was applied to signing steps in the build_push job to prevent signing non-existent images.

Changes on the main branch typically go live within 30 minutes to 2 hours. Production releases on the lts branch occur weekly via the automated dispatcher or on-demand via manual dispatch.

Version Management

The image-versions.yaml file tracks SHA256 digests for all base images and dependencies, ensuring reproducible builds and enabling the CI system to detect upstream changes. Digests are pinned in the Containerfile using ARG variables extracted during the build process.

OCI Container Delivery Mechanism

Container Format and Standards

Operating systems are delivered as OCI container images where system updates are complete new images, not individual packages. The standard OCI image format includes the kernel used to boot the system, enabling bootable containers.

Container Registry Integration

All images are published to ghcr.io/ublue-os/ with standard container registry authentication. bootc replaces ostree's HTTP transport with OCI container fetching, allowing the system to leverage existing container registry infrastructure, CDNs, and authentication mechanisms. Images are signed with Cosign for cryptographic verification.

Base Images Layer (ublue-os/main)

The ublue-os/main repository provides a common main image for all Universal Blue variants with key modifications to Fedora:

As of September 2025, ublue-os/main builds only base, kinoite, and silverblue images, streamlining the build matrix.

GNOME Build Process (Bluefin LTS)

For EL10-based builds (Bluefin LTS), the system uses a parallel build architecture where both GNOME 49 and GNOME 50 variants are built independently through the same full build pipeline. Both versions source from COPR repositories (jreilly1821/c10s-gnome-49 and jreilly1821/c10s-gnome-50-fresh) that track Fedora dist-git.

The Containerfile accepts ARG GNOME_VERSION="49" (defaulting to GNOME 49), and build scripts conditionally branch on this value to select the appropriate COPR repository and compatibility packages. This architecture ensures both GNOME versions receive identical build treatment without layering complexity.

GNOME Version Selection

The build process uses the GNOME_VERSION build argument to determine which GNOME stack to install:

Build Script Branching

The base image build script (10-packages-image-base.sh) conditionally enables the appropriate COPR and installs version-specific packages based on $GNOME_VERSION:

GNOME VersionCOPR RepositoryCompatibility PackageCritical Pre-Upgrades
49c10s-gnome-49gnome49-el10-compatglib2, fontconfig, gobject-introspection, gjs
50c10s-gnome-50-freshgnome50-el10-compatglib2, fontconfig, selinux-policy

Critical packages are upgraded before the GNOME group install to prevent runtime crashes:

The package build script (20-packages.sh) applies version locks to GNOME components based on $GNOME_VERSION:

# GNOME 49 versionlock
dnf versionlock add gnome-shell gdm gnome-session-wayland-session gobject-introspection gjs pango

# GNOME 50 versionlock
dnf versionlock add gnome-shell gdm mutter gnome-session-wayland-session \
    gnome-settings-daemon gnome-control-center gsettings-desktop-schemas \
    gtk4 libadwaita pango fontconfig

Build Workflow Architecture

The build-gnome50.yml workflow invokes reusable-build-image.yml with gnome-version: "50", which passes the version through to the Justfile's build recipe as the 7th positional argument. This triggers a full parallel build using --build-arg GNOME_VERSION=50 rather than layering on top of GNOME 49 images. The workflow runs only on the main branch (never on lts) and includes four jobs:

This mirrors the pattern used in standard build workflows, ensuring both bluefin and bluefin-dx variants receive GNOME 50 testing images.

EL10-Specific Workarounds:

ComponentPurposeImplementation
dbus-daemonGDM's gdm-wayland-session requires dbus-daemon for session message bus; only a Recommends: dependency that bootc builds pruneExplicitly installed alongside GNOME packages
gnome49-el10-compatBundles PAM configuration and SELinux policy fixes for GNOME 49Base image compatibility package
gnome50-el10-compatBundles PAM configuration and SELinux policy fixes for GNOME 50's Varlink userdb architectureProvides pam_permit.so for transient users and SELinux rules for /run/systemd/userdb/ socket operations (GNOME 50 layer only)

All workarounds were validated on quay.io/centos-bootc/centos-bootc:stream10 with GDM greeter reaching successfully and gnome-shell starting under enforcing SELinux.

Modular Component Design

Following 2025 refactoring from monolithic to modular OCI containers, the system comprises:

ModulePurpose
@projectbluefin/commonCore experience: ujust, MOTD, service units, GNOME config
@projectbluefin/brandingBranding assets
@ublue-os/artworkArt assets shared with Aurora and Bazzite
@ublue-os/brewHomebrew integration
@ublue-os/homebrew-tapCustom Homebrew packages

Kernel Module Infrastructure (akmods)

The akmods infrastructure provides pre-compiled, signed kernel modules for immutable distributions. Modules are built daily and version-locked alongside kernel packages to prevent ABI mismatches, enabling out-of-tree drivers and hardware enablement without compromising system immutability.

Update Architecture and Technologies

Core Technologies

The update system combines two complementary technologies:

bootc implements transactional, in-place OS updates using OCI/Docker container images and depends on ostree as a storage backend but replaces ostree's HTTP transport with OCI container fetching.

Immutability Model

The system enforces strict immutability: LockLayering=true in /etc/rpm-ostreed.conf locks local package layering by default, which prevents mutation of the base OSTree commit including package overlays, overrides, and initramfs changes. This ensures the system image remains pristine with no package-based degradation over time. Running rpm-ostree reset and rebooting always restores pure image mode.

Filesystem Layout

The OS is immutable by default using composefs with a specific filesystem organization:

Mount PointPurposeCharacteristics
/sysrootPhysical root filesystemMounted host root with deployed system in chroot-like environment
/usrImmutable OS contentRead-only when composefs enabled, contains all system binaries
/etcMutable persistent configurationOSTree 3-way merge reconciles image defaults with local modifications
/varPersistent application dataSurvives across all deployments, exactly one /var shared across bootloader entries

bootc Commands and Operations

Key bootc commands for system management:

# System updates
bootc upgrade # Query source, queue update for next boot
bootc upgrade --apply # Upgrade and immediately reboot
bootc upgrade --check # Check for updates without side effects

# Switching images
bootc switch <image> # Change tracked image, preserves /etc and /var

# Status and recovery
bootc status # Display current system state
bootc rollback # Swap bootloader ordering to previous entry

bootc switch changes the tracked container image while preserving /etc and /var state, enabling seamless variant switching and blue/green deployments. bootc status displays the current system state including booted deployment and staged deployments, while bootc rollback swaps bootloader ordering to the previous boot entry for quick recovery.

System Update Flow

Updates are delivered as complete new images, not individual packages. The system checks for updates automatically every 6 hours and applies them on reboot, keeping the running system stable while staging changes. The A/B style upgrade system maintains both current and previous deployments, with staged updates downloading and preparing without affecting the running system.

graph TD A["System Checks for Updates<br/>(Every 6 Hours)"] --> B{"Update Available?"} B -->|No| A B -->|Yes| C["Download New Image<br/>(Background)"] C --> D["Stage Update<br/>(Prepare Next Boot)"] D --> E["Running System<br/>Unchanged"] E --> F["User Reboots"] F --> G["Boot into New Image"] G --> H{"System OK?"} H -->|Yes| I["Keep New Image"] H -->|No| J["Rollback to Previous"]

Three-Layer Package Management Strategy

Overview

The system implements a three-layer package management strategy: immutable system layer, Flatpak for GUI applications, and Homebrew for CLI tools. This separation ensures system stability remains decoupled from application flexibility.

System Layer (bootc)

The immutable base OS is delivered as complete images with updates every 6 hours applied on reboot. bootc upgrade fetches updates via OCI container mechanisms, staging them for the next boot without affecting the running system.

Application Layer (Flatpak)

GUI applications are installed from Flathub with automatic updates enabled by default via systemd timers. Updates run daily at 4:00 AM, requiring no reboots. Applications are sandboxed via Flatpak with system-wide installations by default.

CLI Layer (Homebrew)

Homebrew is managed via the projectbluefin/common image and includes containerd support, enabling Docker installation directly from Homebrew. The system maintains a custom tap at ublue-os/homebrew-tap for project-specific packages not available in official Homebrew.

Update Orchestration with uupd

uupd Service

The uupd service coordinates system and Flatpak updates, replacing or supplementing rpm-ostreed-automatic and Flatpak timers. If uupd.timer exists, it is enabled by default; otherwise, individual timers are enabled.

Users can customize the update schedule:

sudo systemctl edit uupd.timer

Update Behavior

The ujust update command uses bootc upgrade by default but falls back to rpm-ostree upgrade if package layering is detected. This reflects the 2025 drastic refactoring to fully embrace bootc as the primary update mechanism.

Update Streams and Variants

Update Streams

Universal Blue provides multiple update streams with different stability and kernel configurations:

StreamFedora VersionKernelBuild ScheduleTarget Users
gts42 with kernel 6.17.12-200.fc42.x86_64Pinned for ZFSWeeklyLong-term stability
stable43 with kernel 6.17.12-300.fc43.x86_64Pinned for ZFSTuesdaysBalanced stability/features
latestLatest FedoraUnpinnedWeeklyLatest features
betaLatest FedoraUnpinnedAs-neededTesting/early adopters

Kernel pinning in gts and stable streams ensures ZFS compatibility by maintaining consistent kernel versions.

Aurora and Bluefin Variants

The two primary desktop variants share the same update infrastructure:

Both variants support rebasing between each other using bootc switch, which preserves the home directory, Flatpak apps, and configurations during rebase. The variants share artwork and infrastructure components.

Usage Examples

System Updates

# Check for updates without applying
bootc upgrade --check

# Update system (applied on next reboot)
ujust update

# Update and reboot immediately
bootc upgrade --apply

Switching Between Variants

# Switch from Bluefin to Aurora
sudo bootc switch ghcr.io/ublue-os/aurora:latest

# Switch between update streams
sudo bootc switch ghcr.io/ublue-os/bluefin:stable

Managing Automatic Updates

# Toggle automatic updates on/off
ujust toggle-updates

# Check current update configuration
systemctl status uupd.timer

Application Management

# Install Flatpak application
flatpak install flathub org.mozilla.firefox

# Install CLI tool via Homebrew
brew install kubectl

# Update Flatpaks manually
flatpak update

Relevant Code Files

File PathDescriptionURL
.github/workflows/scheduled-lts-release.ymlDispatcher workflow for weekly LTS releasesView
.github/workflows/create-lts-pr.ymlAutomated PR creation workflow from main to ltsView
.github/renovate.json5Renovate configuration with branch restrictionsView
.github/workflows/reusable-build.ymlReusable CI/CD workflow (ublue-os/main)View
.github/workflows/reusable-build.ymlReusable CI/CD with rechunking (Bluefin)View
ContainerfileMulti-stage container build (ublue-os/main)View
ContainerfileBluefin variant container buildView
JustfileBuild orchestration and validation (ublue-os/main)View
JustfileBuild recipes including rechunking (Bluefin)View
image-versions.yamlBase image digest tracking (ublue-os/main)View
image-versions.ymlVersion tracking for BluefinView
build_files/base/03-install-kernel-akmods.shKernel installation with version lockingView
build_files/base/04-packages.shBase package installation scriptView
.github/workflows/build-latest.ymlLatest stream build configurationView
.github/workflows/build-image-gts.ymlGTS stream build configurationView
.github/workflows/build-image-stable.ymlStable stream build configurationView
.github/workflows/build-gnome50.ymlGNOME 50 testing builds (lts-testing-50)View

Key Repositories

RepositoryPurposeURL
ublue-os/mainBase images and shared build infrastructureView
ublue-os/bluefinBluefin variant source and build configurationView
projectbluefin/commonShared components (ujust, MOTD, services)View
ublue-os/akmodsPre-compiled kernel modules infrastructureView
ublue-os/toolboxesDevelopment container environmentsView
ublue-os/uupdUpdate orchestration serviceView
ublue-os/homebrew-tapCustom Homebrew package repositoryView

See Also