Skip to content

Latest commit

 

History

History
1108 lines (837 loc) · 36 KB

File metadata and controls

1108 lines (837 loc) · 36 KB

Copilot Instructions for finpilot bootc Image Template

CRITICAL: GitHub API Usage

ALWAYS use GitHub API for external references:

  • When researching other repositories (e.g., projectbluefin/distroless, ublue-os/bluefin)
  • When checking Containerfiles, build scripts, or configuration files
  • Use the github-mcp-server-get_file_contents tool instead of curl/wget
  • This ensures consistent, authenticated access and better error handling

CRITICAL: Pre-Commit Checklist

Execute before EVERY commit:

  1. Conventional Commits - ALL commits MUST follow conventional commit format (see below)
  2. Shellcheck - shellcheck *.sh on all modified shell files
  3. YAML validation - python3 -c "import yaml; yaml.safe_load(open('file.yml'))" on all modified YAML
  4. Justfile syntax - just --list to verify
  5. Confirm with user - Always confirm before committing and pushing

Never commit files with syntax errors.

REQUIRED: Conventional Commit Format

ALL commits MUST use conventional commits format

<type>[optional scope]: <description>

CRITICAL: Template Initialization

When this repository is used as a template, you MUST:

1. Rename ALL instances of finpilot

Source of truth: Containerfile line 9: # Name: finpilot

Files to update:

  • Containerfile (line 9)
  • Justfile (line 1)
  • README.md (line 1)
  • artifacthub-repo.yml (line 5)
  • custom/ujust/README.md (~line 175)
  • .github/workflows/ghcr-pruner.yml (line 22)

2. Create "What's Different" section in README

CRITICAL: Add this section near the top of README.md (after the title/intro, before detailed docs):

## What Makes this Raptor Different?

Here are the changes from [Base Image Name]. This image is based on [Bluefin/Bazzite/Aurora/etc] and includes these customizations:

### Added Packages (Build-time)
- **System packages**: tmux, micro, mosh - [brief explanation of why]

### Added Applications (Runtime)
- **CLI Tools (Homebrew)**: neovim, helix - [brief explanation]
- **GUI Apps (Flatpak)**: Spotify, Thunderbird - [brief explanation]

### Removed/Disabled
- List anything removed from base image

### Configuration Changes
- Any systemd services enabled/disabled
- Desktop environment changes
- Other notable modifications

*Last updated: [date]*

Maintenance requirement:

  • ALWAYS update this section when you modify packages or configuration
  • Keep descriptions brief and user-focused (explain "why", not just "what")
  • Write for typical Linux users, not developers
  • Update the "Last updated" date with each change

3. Verify copilot instructions are present

Confirm .github/copilot-instructions.md exists in the new repository. This file should be automatically copied when using this as a GitHub template.

4. Explain signing is optional

Signing is DISABLED by default. First builds succeed immediately. Enable later for production (see README).

These 4 steps are REQUIRED for every new template instance.


Repository Structure

├── Containerfile          # Main build definition (multi-stage build with OCI imports)
├── Justfile              # Local build automation (image name, build commands)
├── build/                # Build-time scripts (10-build.sh, 20-chrome.sh, etc.)
│   ├── 10-build.sh      # Main build script (copy custom files, install packages)
│   ├── 20-*.sh.example  # Example third-party repos (rename to use)
│   ├── 30-*.sh.example  # Example desktop replacement (rename to use)
│   ├── copr-helpers.sh  # Helper functions for COPR repositories
│   └── README.md        # Build scripts documentation
├── custom/               # User customizations (NOT in container, installed at runtime/first boot)
│   ├── brew/            # Homebrew Brewfiles (CLI tools, dev tools)
│   │   ├── default.Brewfile      # General CLI tools
│   │   ├── development.Brewfile  # Dev environments
│   │   ├── fonts.Brewfile        # Font packages
│   │   └── README.md             # Homebrew documentation
│   ├── flatpaks/        # Flatpak preinstall (GUI apps, post-first-boot)
│   │   ├── default.preinstall    # Default GUI apps (INI format)
│   │   └── README.md             # Flatpak documentation
│   └── ujust/           # User commands (shortcuts to Brewfiles, system tasks)
│       ├── custom-apps.just      # App installation shortcuts
│       ├── custom-system.just    # System maintenance commands
│       └── README.md             # ujust documentation
├── iso/                  # Local testing only (no CI/CD)
│   ├── disk.toml        # VM/disk image config (QCOW2/RAW)
│   ├── iso.toml         # ISO installer config (bootc switch URL)
│   └── rclone/          # Upload configs (Cloudflare R2, AWS S3, etc.)
├── .github/              # GitHub configuration and CI/CD
│   ├── workflows/       # GitHub Actions workflows
│   │   ├── build.yml               # Builds :stable on main
│   │   ├── clean.yml               # Deletes images >90 days old
│   │   ├── renovate.yml            # Renovate bot updates (6h interval)
│   │   ├── validate-*.yml          # Pre-merge validation checks
│   │   └── ...
│   ├── copilot-instructions.md  # THIS FILE - Instructions for Copilot
│   ├── SETUP_CHECKLIST.md       # Quick setup checklist for users
│   ├── commit-convention.md     # Conventional commits guide
│   └── renovate.json5           # Renovate configuration
├── .pre-commit-config.yaml   # Pre-commit hooks (optional local use)
└── .gitignore                # Prevents committing secrets (cosign.key, etc.)

Core Principles

Multi-Stage Build Architecture

This template follows the Bluefin architecture pattern from @projectbluefin/distroless:

Architecture Layers:

  1. Context Stage (ctx) - Combines resources from multiple sources:

    • Local build scripts (/build)
    • Local custom files (/custom)
    • @projectbluefin/common - Desktop configuration shared with Aurora (/oci/common)
    • @projectbluefin/branding - Branding assets (/oci/branding)
    • @ublue-os/artwork - Artwork shared with Aurora and Bazzite (/oci/artwork)
    • @ublue-os/brew - Homebrew integration (/oci/brew)
  2. Base Image Options:

    • ghcr.io/ublue-os/silverblue-main:42 (Fedora-based, default)
    • quay.io/centos-bootc/centos-bootc:stream10 (CentOS-based)

OCI Container Resources:

  • Resources from OCI containers are copied to distinct subdirectories (/oci/*) to avoid file conflicts
  • Renovate automatically updates :latest tags to SHA digests for reproducibility
  • All OCI resources are mounted at build-time via the ctx stage

Reference: See Bluefin Contributing Guide for architecture diagram

Build-time vs Runtime

  • Build-time (build/): Baked into container. Use dnf5 install. Services, configs, system packages.
  • Runtime (custom/): User installs after deployment. Use Brewfiles, Flatpaks. CLI tools, GUI apps, dev environments.

Bluefin Convention Compliance

ALWAYS follow @ublue-os/bluefin patterns. Confirm before deviating.

  • Use dnf5 exclusively (never dnf, yum, rpm-ostree)
  • Always -y flag for non-interactive
  • COPRs: enable → install → DISABLE (critical, prevents repo persistence)
  • Use copr_install_isolated function pattern
  • Numbered scripts: 10-build.sh, 20-chrome.sh, 30-cosmic.sh
  • Check @bootc-dev for container best practices

Branch Strategy

  • main = Production releases ONLY. Never push directly. Builds :stable images.
  • Conventional Commits = REQUIRED. feat:, fix:, chore:, etc.
  • Workflows = All validation happens on PRs. Merging to main triggers stable builds.

Validation Workflows

The repository includes automated validation on pull requests:

  • validate-shellcheck.yml - Runs shellcheck on all build/*.sh scripts
  • validate-brewfiles.yml - Validates Homebrew Brewfile syntax
  • validate-flatpaks.yml - Checks Flatpak app IDs exist on Flathub
  • validate-justfiles.yml - Validates just file syntax
  • validate-renovate.yml - Validates Renovate configuration

When adding files: These validations run automatically on PRs. Fix any errors before merge.


Where to Add Packages

This section provides clear guidance on where to add different types of packages.

System Packages (dnf5 - Build-time)

Location: build/10-build.sh

System packages are installed at build-time and baked into the container image. Use dnf5 exclusively.

Example:

# In build/10-build.sh
dnf5 install -y vim git htop neovim tmux

When to use:

  • System utilities and services
  • Dependencies required for other build-time operations
  • Packages that need to be available immediately on first boot
  • Services that need to be enabled with systemctl enable

Important:

  • Always use dnf5 (never dnf, yum, or rpm-ostree)
  • Always add -y flag for non-interactive installs
  • For COPR repositories, use copr_install_isolated pattern and disable after use
  • For third-party repos, see example scripts: build/20-onepassword.sh.example

Script Naming Convention:

  • 10-build.sh - Main build script (always runs first)
  • 20-*.sh - Additional scripts (run in numerical order)
  • 30-*.sh - Desktop environment changes
  • .example suffix - Rename to .sh to activate

Homebrew Packages (Brew - Runtime)

Location: custom/brew/*.Brewfile

Homebrew packages are installed by users after deployment. Best for CLI tools and development environments.

Files:

  • custom/brew/default.Brewfile - General purpose CLI tools
  • custom/brew/development.Brewfile - Development tools and environments
  • custom/brew/fonts.Brewfile - Font packages
  • Create custom *.Brewfile as needed

Example:

# In custom/brew/default.Brewfile
brew "bat"        # cat with syntax highlighting
brew "eza"        # Modern replacement for ls
brew "ripgrep"    # Faster grep
brew "fd"         # Simple alternative to find

When to use:

  • CLI tools and utilities
  • Development tools (node, python, go, etc.)
  • User-specific tools that don't need to be in the base image
  • Tools that update frequently

Important:

  • Brewfiles use Ruby syntax
  • Users install via ujust commands (e.g., ujust install-default-apps)
  • Not installed in ISO/container - users install after deployment

Flatpak Applications (GUI Apps - Runtime)

Location: custom/flatpaks/*.preinstall

Flatpak applications are GUI apps installed after first boot. Use INI format.

Files:

  • custom/flatpaks/default.preinstall - Default GUI applications
  • Create custom *.preinstall files as needed

Example:

# In custom/flatpaks/default.preinstall
[Flatpak Preinstall org.mozilla.firefox]
Branch=stable

[Flatpak Preinstall com.visualstudio.code]
Branch=stable

[Flatpak Preinstall org.gnome.Calculator]
Branch=stable

When to use:

  • GUI applications
  • Desktop apps (browsers, editors, media players)
  • Apps that users expect to have immediately available
  • Apps from Flathub (https://flathub.org/)

Important:

  • Installed post-first-boot (not in ISO/container)
  • Requires internet connection
  • Find app IDs at https://flathub.org/
  • Use INI format with [Flatpak Preinstall APP_ID] sections
  • Always specify Branch=stable (or another branch)

Quick Reference: Common User Requests

Request Action Location
Add package (build-time) dnf5 install -y pkg build/10-build.sh
Add package (runtime) brew "pkg" custom/brew/default.Brewfile
Add GUI app [Flatpak Preinstall org.app.id] custom/flatpaks/default.preinstall
Add user command Create shortcut (NO dnf5) custom/ujust/*.just
Add third-party repo Use example scripts build/20-*.sh.example (rename)
Replace desktop Use example script build/30-cosmic-desktop.sh.example
Switch base image Update FROM line Containerfile line 38
Add OCI containers Uncomment COPY --from= lines Containerfile lines 13-18 (ctx stage)
Test locally just build && just build-qcow2 && just run-vm-qcow2 Terminal
Deploy (production) sudo bootc switch ghcr.io/user/repo:stable Terminal
Enable service systemctl enable service.name build/10-build.sh
Add COPR enable → install → DISABLE build/10-build.sh
Validate changes Automatic on PR .github/workflows/validate-*.yml

Detailed Workflows

1. Multi-Stage Build Architecture

File: Containerfile

This template uses a multi-stage build following the @projectbluefin/distroless pattern.

Stage 1: Context (ctx) - Line 39 Combines resources from multiple OCI containers:

FROM scratch AS ctx

COPY build /build
COPY custom /custom
# Import from OCI containers - Renovate updates :latest to SHA-256 digests
COPY --from=ghcr.io/ublue-os/base-main:latest /system_files /oci/base
COPY --from=ghcr.io/projectbluefin/common:latest /system_files /oci/common
COPY --from=ghcr.io/projectbluefin/branding:latest /system_files /oci/branding
COPY --from=ghcr.io/ublue-os/artwork:latest /system_files /oci/artwork
COPY --from=ghcr.io/ublue-os/brew:latest /system_files /oci/brew

Stage 2: Base Image - Line 52

FROM ghcr.io/ublue-os/silverblue-main:latest  # Default (Fedora-based)
# OR
FROM quay.io/centos-bootc/centos-bootc:stream10  # CentOS-based

Common alternative base images:

FROM ghcr.io/ublue-os/bluefin:stable      # Dev, GNOME, `:stable` or `:gts`
FROM ghcr.io/ublue-os/bazzite:stable      # Gaming, Steam Deck
FROM ghcr.io/ublue-os/aurora:stable       # KDE Plasma
FROM quay.io/fedora/fedora-bootc:42       # Upstream Fedora

Tags: :stable (recommended), :latest (bleeding edge), -nvidia variants available

Renovate: Base image SHA and OCI container tags are auto-updated by Renovate bot every 6 hours (see .github/renovate.json5)

OCI Container Resources:

  • @ublue-os/base-main - Base system configuration
  • @projectbluefin/common - Desktop configuration shared with Aurora
  • @projectbluefin/branding - Branding assets
  • @ublue-os/artwork - Artwork shared with Aurora and Bazzite
  • @ublue-os/brew - Homebrew integration

File Locations in Build Scripts:

  • Local build scripts: /ctx/build/
  • Local custom files: /ctx/custom/
  • Base files: /ctx/oci/base/
  • Common files: /ctx/oci/common/
  • Branding files: /ctx/oci/branding/
  • Artwork files: /ctx/oci/artwork/
  • Brew files: /ctx/oci/brew/

2. OCI Containers for Additional System Files

File: Containerfile (ctx stage, lines 6-18)

Following the @projectbluefin/distroless pattern, you can layer in additional system files from OCI containers. These are commented out by default in the template.

Available OCI Containers:

# Artwork and Branding from projectbluefin/common
COPY --from=ghcr.io/projectbluefin/common:latest /system_files/bluefin /files/bluefin
COPY --from=ghcr.io/projectbluefin/common:latest /system_files/shared /files/shared

# Homebrew system files from ublue-os/brew
COPY --from=ghcr.io/ublue-os/brew:latest /system_files /files/brew

What's included:

  • projectbluefin/common:latest - Bluefin wallpapers, themes, branding assets, ujust completions, udev rules
  • ublue-os/brew:latest - Homebrew system integration files

When to use:

  • You want Bluefin-specific artwork and wallpapers in your custom image
  • You want additional system integration beyond what the base image provides
  • You're building a Bluefin derivative and want to maintain brand consistency

Important:

  • These are commented out by default as template examples
  • Uncomment only if you specifically want these additional system files
  • The files are copied into the ctx stage and made available to your build scripts
  • To use the files in your build, you'll need to copy them from /ctx/files/* to appropriate system locations in your build scripts

3. Build Scripts (build/)

Pattern: Numbered files (10-build.sh, 20-chrome.sh, 30-cosmic.sh) run in order.

Example - build/10-build.sh:

#!/usr/bin/env bash
set -euo pipefail

# Install packages
dnf5 install -y vim git htop neovim

# Enable services
systemctl enable podman.socket

# Download binaries
curl -L https://example.com/tool -o /usr/local/bin/tool
chmod +x /usr/local/bin/tool

Example - COPR pattern (see build/20-onepassword.sh):

#!/usr/bin/env bash
set -euo pipefail

source /ctx/copr-install-functions.sh

# Chrome
dnf config-manager addrepo --from-repofile=https://dl.google.com/linux/linux_signing_key.pub
dnf5 install -y google-chrome-stable

# 1Password via COPR (isolated)
copr_install_isolated username/repo package-name

Example - Desktop swap (see build/30-cosmic.sh):

#!/usr/bin/env bash
set -euo pipefail

# Remove GNOME, install COSMIC
dnf5 group remove -y "GNOME Desktop Environment"
dnf5 copr enable -y ryanabx/cosmic-epoch
dnf5 install -y cosmic-desktop
dnf5 copr disable -y ryanabx/cosmic-epoch
systemctl set-default graphical.target

CRITICAL: Use copr_install_isolated function. Always disable COPRs.

Example scripts: See build/20-onepassword.sh.example and build/30-cosmic-desktop.sh.example for complete working examples.

4. Homebrew (custom/brew/)

Files: *.Brewfile (Ruby syntax)

Example - custom/brew/default.Brewfile:

# CLI tools
brew "bat"        # Better cat
brew "eza"        # Better ls
brew "ripgrep"    # Better grep
brew "fd"         # Better find

# Dev tools
tap "homebrew/cask"
brew "node"
brew "python"

Users install via: ujust install-default-apps (create shortcut in custom/ujust/)

5. ujust Commands (custom/ujust/)

Files: *.just (all auto-consolidated)

Example - custom/ujust/apps.just:

[group('Apps')]
install-default-apps:
    #!/usr/bin/env bash
    brew bundle --file /usr/share/ublue-os/homebrew/default.Brewfile

[group('Apps')]
install-dev-tools:
    #!/usr/bin/env bash
    brew bundle --file /usr/share/ublue-os/homebrew/development.Brewfile

RULES:

  • NEVER use dnf5 in ujust - only Brewfile/Flatpak shortcuts
  • Use [group('Category')] for organization
  • All .just files merged during build

6. Flatpaks (custom/flatpaks/)

Files: *.preinstall (INI format, installed after first boot)

Example - custom/flatpaks/default.preinstall:

[Flatpak Preinstall org.mozilla.firefox]
Branch=stable

[Flatpak Preinstall org.gnome.Calculator]
Branch=stable

[Flatpak Preinstall com.visualstudio.code]
Branch=stable

Important: Not in ISO/container. Installed post-first-boot. Requires internet. Find IDs at https://flathub.org/

7. ISO/Disk Images (iso/)

For local testing only. No CI/CD.

Files:

  • iso/disk.toml - VM images (QCOW2/RAW): just build-qcow2
  • iso/iso.toml - Installer ISO: just build-iso

CRITICAL - Update bootc switch URL in iso/iso.toml:

[customizations.installer.kickstart]
contents = """
%post
bootc switch --mutate-in-place --transport registry ghcr.io/USERNAME/REPO:stable
%end
"""

Upload: Use iso/rclone/ configs (Cloudflare R2, AWS S3, Backblaze B2, SFTP)

8. Release Workflow

Branches:

  • main - Production only. Builds :stable images. Never push directly.

Workflows:

  • build.yml - Builds :stable on main
  • renovate.yml - Monitors base image updates (every 6 hours)
  • clean.yml - Deletes images >90 days (weekly)
  • validate-*.yml - Pre-merge validation (shellcheck, Brewfile, Flatpak, etc.)

Image Tags:

  • :stable - Latest stable release from main branch
  • :stable.YYYYMMDD - Datestamped stable release
  • :YYYYMMDD - Date only
  • :pr-123 - Pull request builds (for testing)
  • :sha-abc123 - Git commit SHA (short)

Renovate Bot:

  • Automatically updates base image SHAs in Containerfile
  • Runs every 6 hours (configured in .github/renovate.json5)
  • Creates PRs for updates - review and merge to keep images current

8. Understanding the Multi-Stage Build Architecture

This template implements a multi-stage build pattern following @projectbluefin/distroless.

Why Multi-Stage?

  • Modularity: Combine resources from multiple OCI containers
  • Reusability: Share common components across different images
  • Maintainability: Update shared components independently
  • Reproducibility: Renovate updates OCI container tags to SHA digests

Stage Breakdown:

Stage 1: Context (ctx)

FROM scratch AS ctx
COPY build /build                    # Local build scripts
COPY custom /custom                  # Local customizations
COPY --from=ghcr.io/projectbluefin/common:latest /system_files /oci/common
COPY --from=ghcr.io/projectbluefin/branding:latest /system_files /oci/branding
COPY --from=ghcr.io/ublue-os/artwork:latest /system_files /oci/artwork
COPY --from=ghcr.io/ublue-os/brew:latest /system_files /oci/brew

This stage combines:

  • Local resources (build scripts, custom files)
  • OCI container resources from upstream projects
  • Resources are copied to distinct subdirectories to avoid conflicts

Stage 2: Final Image

FROM ghcr.io/ublue-os/silverblue-main:42

RUN --mount=type=bind,from=ctx,source=/,target=/ctx \
    /ctx/build/10-build.sh

The final stage:

  • Starts from base image
  • Mounts the ctx stage at /ctx
  • Runs build scripts with access to all resources

Accessing OCI Resources in Build Scripts:

Build scripts can access files from OCI containers:

#!/usr/bin/env bash
# Example: Copy branding files
cp -r /ctx/oci/branding/* /usr/share/branding/

# Example: Copy common desktop config
cp /ctx/oci/common/config.yaml /etc/myapp/

# Example: Use brew files
cp /ctx/oci/brew/*.sh /usr/local/bin/

Renovate Integration:

  • Renovate monitors OCI container tags (:latest)
  • Automatically updates to SHA digests for reproducibility
  • Example: :latest@sha256:abc123...
  • Ensures builds are reproducible and verifiable

Reference: See Bluefin Contributing Guide for architecture diagram

9. Image Signing (Optional, Recommended for Production)

Default: DISABLED (commented out in workflows) to allow first builds.

# Generate keys
COSIGN_PASSWORD="" cosign generate-key-pair
# Creates: cosign.key (SECRET), cosign.pub (COMMIT)

# Add to GitHub
# Settings → Secrets and Variables → Actions → New secret
# Name: SIGNING_SECRET
# Value: <paste entire contents of cosign.key>

# Uncomment signing sections in:
# - .github/workflows/build.yml
# - .github/workflows/build-testing.yml

NEVER commit cosign.key. Already in .gitignore.


Critical Rules (Enforced)

  1. ALWAYS use Conventional Commits format for ALL commits (required for Release Please)
    • Format: <type>[scope]: <description>
    • Valid types: feat:, fix:, docs:, chore:, build:, ci:, refactor:, test:
    • Breaking changes: Add ! or BREAKING CHANGE: in footer
    • See .github/commit-convention.md for examples
  2. NEVER commit cosign.key to repository
  3. ALWAYS disable COPRs after use (copr_install_isolated function)
  4. ALWAYS use dnf5 exclusively (never dnf, yum, rpm-ostree)
  5. ALWAYS use -y flag for non-interactive installs
  6. NEVER use dnf5 in ujust files - only Brewfile/Flatpak shortcuts
  7. ALWAYS work on testing branch for development
  8. ALWAYS let Release Please handle testing→main merges
  9. NEVER push directly to main (only via Release Please)
  10. ALWAYS confirm with user before deviating from @ublue-os/bluefin patterns
  11. ALWAYS run shellcheck/YAML validation before committing
  12. ALWAYS update bootc switch URL in iso/iso.toml to match user's repo
  13. ALWAYS follow numbered script convention: 10-*.sh, 20-*.sh, 30-*.sh
  14. ALWAYS check example scripts before creating new patterns (.example files in build/)
  15. ALWAYS validate that new Flatpak IDs exist on Flathub before adding
  16. NEVER modify validation workflows without understanding impact on PR checks

Troubleshooting

Symptom Cause Solution
Build fails: "permission denied" Signing misconfigured Verify signing commented out OR SIGNING_SECRET set
Build fails: "package not found" Typo or unavailable Check spelling, verify on RPMfusion, add COPR if needed
Build fails: "base image not found" Invalid FROM line Check syntax in Containerfile line 24
Build fails: "shellcheck error" Script syntax error Run shellcheck build/*.sh locally, fix errors
PR validation fails: Brewfile Invalid Brewfile syntax Check Ruby syntax, ensure packages exist
PR validation fails: Flatpak Invalid app ID Verify app ID exists on https://flathub.org/
PR validation fails: justfile Invalid just syntax Run just --list locally to test
Changes not in production Wrong workflow Push to main (via PR) to trigger stable builds
ISO missing customizations Wrong bootc URL Update iso/iso.toml bootc switch URL to match repo
COPR packages missing after boot COPR not disabled COPRs persist if not disabled - use copr_install_isolated
ujust commands not working Wrong install location Files must be in custom/ujust/ and copied to /usr/share/ublue-os/just/
Flatpaks not installed Expected behavior Flatpaks install post-first-boot, not in ISO/container
Local build fails Wrong environment Must run on bootc-based system or have podman installed
Renovate not creating PRs Configuration issue Check .github/renovate.json5 syntax
Third-party repo not working Repo file persists Remove repo file at end of script (see examples)

Common Patterns & Examples

Pattern 1: Adding Third-Party RPM Repositories

Use case: Installing Google Chrome, 1Password, VS Code, etc.

Example: See build/20-onepassword.sh.example

Steps:

  1. Add GPG key (if required)
  2. Create repo file in /etc/yum.repos.d/
  3. Install packages with dnf5 install -y
  4. CRITICAL: Remove repo file at end
# Add repo
cat > /etc/yum.repos.d/google-chrome.repo << 'EOF'
[google-chrome]
name=google-chrome
baseurl=https://dl.google.com/linux/chrome/rpm/stable/x86_64
enabled=1
gpgcheck=1
gpgkey=https://dl.google.com/linux/linux_signing_key.pub
EOF

# Install
dnf5 install -y google-chrome-stable

# Clean up (required!)
rm -f /etc/yum.repos.d/google-chrome.repo

Pattern 2: Using COPR Repositories

Use case: Installing packages from Fedora COPR (community repos)

Example: See build/copr-helpers.sh and build/30-cosmic-desktop.sh.example

Always use copr_install_isolated function:

source /ctx/build/copr-helpers.sh

# Install from COPR (isolated - auto-disables after install)
copr_install_isolated "ublue-os/staging" package-name

# Install multiple packages
copr_install_isolated "ryanabx/cosmic-epoch" \
    cosmic-session \
    cosmic-greeter \
    cosmic-comp

Pattern 3: Replacing Desktop Environment

Use case: Swap GNOME for KDE, COSMIC, etc.

Example: See build/30-cosmic-desktop.sh.example

Steps:

  1. Remove old desktop: dnf5 remove -y gnome-shell ...
  2. Install new desktop: copr_install_isolated ...
  3. Configure display manager: systemctl enable ...
  4. Set default session

Pattern 4: Enabling System Services

Location: build/10-build.sh

# Enable service
systemctl enable podman.socket

# Mask unwanted service
systemctl mask unwanted-service

# Set default target
systemctl set-default graphical.target

Pattern 5: Creating Custom ujust Commands

Location: custom/ujust/*.just

Example structure:

# vim: set ft=make :

# Install development tools
[group('Apps')]
install-dev-tools:
    #!/usr/bin/env bash
    echo "Installing development tools..."
    brew bundle --file /usr/share/ublue-os/homebrew/development.Brewfile

# Custom system command
[group('System')]
my-custom-command:
    #!/usr/bin/env bash
    echo "Running custom command..."
    # Your logic here (NO dnf5!)

Pattern 6: Local Testing Workflow

Complete local testing cycle:

# 1. Build container image
just build

# 2. Build QCOW2 disk image
just build-qcow2

# 3. Run in VM
just run-vm-qcow2

# Or combine all steps
just build && just build-qcow2 && just run-vm-qcow2

Alternative: Build ISO for installation testing

just build
just build-iso
just run-vm-iso

Pattern 7: Pre-commit Validation (Optional)

Setup pre-commit hooks locally:

# Install pre-commit
pip install pre-commit

# Install hooks
pre-commit install

# Run manually
pre-commit run --all-files

Note: Pre-commit config exists (.pre-commit-config.yaml) but is optional. CI validation runs automatically on PRs.


Advanced Topics

/opt Immutability

Some packages (Chrome, Docker Desktop) write to /opt. On Fedora, it's symlinked to /var/opt (mutable). To make immutable:

Uncomment Containerfile line 20:

RUN rm /opt && mkdir /opt

Multi-Architecture

  • Local just commands support your platform
  • Most UBlue images support amd64/arm64
  • Add -arm64 suffix if needed: bluefin-arm64:stable
  • Cross-platform builds require additional setup

Custom Build Functions

See build/copr-install-functions.sh for reusable patterns:

  • copr_install_isolated - Enable COPR, install packages, disable COPR
  • Follow @ublue-os/bluefin conventions exactly

Understanding the Build Process

Container Build Flow

  1. Base Image - Pulls base image specified in Containerfile FROM line
  2. Context Stage - Mounts build/ and custom/ directories
  3. Build Scripts - Runs scripts in build/ directory in numerical order:
    • 10-build.sh - Always runs first (copies custom files, installs packages)
    • 20-*.sh - Additional scripts (if present and not .example)
    • 30-*.sh - More scripts (if present and not .example)
  4. Container Lint - Validates final image with bootc container lint
  5. Push to Registry - Uploads to GitHub Container Registry (ghcr.io)

What Gets Included in the Image

Build-time (baked into image):

  • System packages from dnf5 install
  • Enabled systemd services
  • Custom files copied from /ctx/custom/ to standard locations:
    • Brewfiles → /usr/share/ublue-os/homebrew/
    • ujust files → /usr/share/ublue-os/just/60-custom.just
    • Flatpak preinstall → /etc/flatpak/preinstall.d/

Runtime (installed after deployment):

  • Homebrew packages (user runs ujust install-*)
  • Flatpak applications (installed on first boot, requires internet)

Local vs CI Builds

Local builds (with just build):

  • Uses your local podman
  • Faster for testing
  • No signing
  • No automatic push to registry

CI builds (GitHub Actions):

  • Uses GitHub runners
  • Automatic on push/PR
  • Includes validation steps
  • Can include signing
  • Automatic push to ghcr.io

Image Layers and Caching

Efficient layering:

  • Each RUN command creates a new layer
  • Layers are cached between builds
  • Changes near end of Containerfile = faster rebuilds
  • Use --mount=type=cache for package managers

Best practices:

  • Group related dnf5 install commands together
  • Don't install and remove in same layer
  • Clean up in same RUN command as install

Image Tags Reference

Main branch (production releases):

  • stable - Latest stable release (recommended)
  • stable.20250129 - Datestamped stable release
  • 20250129 - Date only
  • v1.0.0 - Version from Release Please

PR builds:

  • pr-123 - Pull request number
  • sha-abc123 - Git commit SHA (short)

File Modification Priority

When user requests customization, check in this order:

  1. build/10-build.sh (50%) - Build-time packages, services, system configs
  2. custom/brew/ (20%) - Runtime CLI tools, dev environments
  3. custom/ujust/ (15%) - User convenience commands
  4. custom/flatpaks/ (5%) - GUI applications
  5. Containerfile (5%) - Base image, /opt config, advanced builds
  6. Justfile (2%) - Image name, build parameters
  7. iso/*.toml (2%) - ISO/disk customization for testing
  8. .github/workflows/ (1%) - Metadata, triggers, workflow config

Files to AVOID Modifying

Do NOT modify unless specifically requested or necessary:

  • .github/renovate.json5 - Renovate configuration (auto-updates)
  • .github/workflows/validate-*.yml - Validation workflows
  • .gitignore - Prevents committing secrets
  • build/copr-helpers.sh - Helper functions (stable patterns)
  • LICENSE - Repository license
  • cosign.pub - Public signing key (regenerate if changing keys)

Modify with extreme caution:

  • .github/workflows/build.yml - Core build workflow
  • .github/workflows/clean.yml - Image cleanup
  • Justfile - Local build automation (users rely on these commands)

Debugging Tips

Local Debugging

Build failures:

# Build with verbose output
podman build --log-level=debug .

# Check build script syntax
shellcheck build/*.sh

# Test specific script in container
podman run --rm -it ghcr.io/ublue-os/bluefin:stable bash
# Then run your script commands manually

Brewfile issues:

# Validate Brewfile syntax
brew bundle check --file custom/brew/default.Brewfile

# List what would be installed
brew bundle list --file custom/brew/default.Brewfile

Just file issues:

# Check syntax
just --list

# Check specific file
just --unstable --fmt --check -f custom/ujust/custom-apps.just

# Run specific command with debug
just --verbose install-default-apps

CI Debugging

Check workflow logs:

  1. Go to Actions tab in GitHub
  2. Click on failed workflow run
  3. Expand failed step
  4. Look for error messages

Common CI failures:

  • Shellcheck errors: Fix script syntax
  • Brewfile validation: Check package names exist
  • Flatpak validation: Verify app IDs on Flathub
  • Image pull failures: Check base image SHA/tag

Test PR before merge:

# PR builds are tagged as :pr-NUMBER
podman pull ghcr.io/YOUR_USERNAME/YOUR_REPO:pr-123
podman run --rm -it ghcr.io/YOUR_USERNAME/YOUR_REPO:pr-123 bash

Runtime Debugging

After deployment:

# Check system info
bootc status

# Check running services
systemctl list-units --failed

# Check logs
journalctl -b -p err

# Check ujust commands available
ujust --list

# Check Brewfiles location
ls -la /usr/share/ublue-os/homebrew/

# Check Flatpak preinstall
ls -la /etc/flatpak/preinstall.d/

Flatpak debugging:

# Check Flatpak remotes
flatpak remotes

# Check installed Flatpaks
flatpak list

# Install Flatpak manually
flatpak install -y flathub org.mozilla.firefox

Homebrew debugging:

# Check Homebrew status
brew doctor

# Check Brewfile
cat /usr/share/ublue-os/homebrew/default.Brewfile

# Install manually
brew install package-name

Resources & Documentation


Other Rules that are Important to the Maintainers

  • Ensure that conventional commits are used and enforced for every commit and pull request title.
  • Always be surgical with the least amount of code, the project strives to be easy to maintain.

Attribution Requirements

AI agents must disclose what tool and model they are using in the "Assisted-by" commit footer:

Assisted-by: [Model Name] via [Tool Name]

Example:

Assisted-by: Claude 3.5 Sonnet via GitHub Copilot

Last Updated: 2025-11-14
Template Version: finpilot (Enhanced with comprehensive Copilot instructions)
Maintainer: Universal Blue Community