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_contentstool instead of curl/wget - This ensures consistent, authenticated access and better error handling
Execute before EVERY commit:
- Conventional Commits - ALL commits MUST follow conventional commit format (see below)
- Shellcheck -
shellcheck *.shon all modified shell files - YAML validation -
python3 -c "import yaml; yaml.safe_load(open('file.yml'))"on all modified YAML - Justfile syntax -
just --listto verify - Confirm with user - Always confirm before committing and pushing
Never commit files with syntax errors.
ALL commits MUST use conventional commits format
<type>[optional scope]: <description>
When this repository is used as a template, you MUST:
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)
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
Confirm .github/copilot-instructions.md exists in the new repository. This file should be automatically copied when using this as a GitHub template.
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.
├── 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.)
This template follows the Bluefin architecture pattern from @projectbluefin/distroless:
Architecture Layers:
-
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)
- Local build scripts (
-
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
:latesttags to SHA digests for reproducibility - All OCI resources are mounted at build-time via the
ctxstage
Reference: See Bluefin Contributing Guide for architecture diagram
- Build-time (
build/): Baked into container. Usednf5 install. Services, configs, system packages. - Runtime (
custom/): User installs after deployment. Use Brewfiles, Flatpaks. CLI tools, GUI apps, dev environments.
ALWAYS follow @ublue-os/bluefin patterns. Confirm before deviating.
- Use
dnf5exclusively (neverdnf,yum,rpm-ostree) - Always
-yflag for non-interactive - COPRs: enable → install → DISABLE (critical, prevents repo persistence)
- Use
copr_install_isolatedfunction pattern - Numbered scripts:
10-build.sh,20-chrome.sh,30-cosmic.sh - Check @bootc-dev for container best practices
- main = Production releases ONLY. Never push directly. Builds
:stableimages. - Conventional Commits = REQUIRED.
feat:,fix:,chore:, etc. - Workflows = All validation happens on PRs. Merging to main triggers stable builds.
The repository includes automated validation on pull requests:
- validate-shellcheck.yml - Runs shellcheck on all
build/*.shscripts - 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.
This section provides clear guidance on where to add different types of packages.
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 tmuxWhen 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(neverdnf,yum, orrpm-ostree) - Always add
-yflag for non-interactive installs - For COPR repositories, use
copr_install_isolatedpattern 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.examplesuffix - Rename to.shto activate
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 toolscustom/brew/development.Brewfile- Development tools and environmentscustom/brew/fonts.Brewfile- Font packages- Create custom
*.Brewfileas 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 findWhen 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
ujustcommands (e.g.,ujust install-default-apps) - Not installed in ISO/container - users install after deployment
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
*.preinstallfiles 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=stableWhen 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)
| 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 |
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/brewStage 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-basedCommon 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 FedoraTags: :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/
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/brewWhat's included:
projectbluefin/common:latest- Bluefin wallpapers, themes, branding assets, ujust completions, udev rulesublue-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
ctxstage 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
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/toolExample - 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-nameExample - 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.targetCRITICAL: 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.
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/)
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.BrewfileRULES:
- NEVER use
dnf5in ujust - only Brewfile/Flatpak shortcuts - Use
[group('Category')]for organization - All
.justfiles merged during build
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=stableImportant: Not in ISO/container. Installed post-first-boot. Requires internet. Find IDs at https://flathub.org/
For local testing only. No CI/CD.
Files:
iso/disk.toml- VM images (QCOW2/RAW):just build-qcow2iso/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)
Branches:
main- Production only. Builds:stableimages. Never push directly.
Workflows:
build.yml- Builds:stableon mainrenovate.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
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/brewThis 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.shThe final stage:
- Starts from base image
- Mounts the
ctxstage 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
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.ymlNEVER commit cosign.key. Already in .gitignore.
- 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
!orBREAKING CHANGE:in footer - See
.github/commit-convention.mdfor examples
- Format:
- NEVER commit
cosign.keyto repository - ALWAYS disable COPRs after use (
copr_install_isolatedfunction) - ALWAYS use
dnf5exclusively (neverdnf,yum,rpm-ostree) - ALWAYS use
-yflag for non-interactive installs - NEVER use
dnf5in ujust files - only Brewfile/Flatpak shortcuts - ALWAYS work on testing branch for development
- ALWAYS let Release Please handle testing→main merges
- NEVER push directly to main (only via Release Please)
- ALWAYS confirm with user before deviating from @ublue-os/bluefin patterns
- ALWAYS run shellcheck/YAML validation before committing
- ALWAYS update bootc switch URL in
iso/iso.tomlto match user's repo - ALWAYS follow numbered script convention:
10-*.sh,20-*.sh,30-*.sh - ALWAYS check example scripts before creating new patterns (
.examplefiles inbuild/) - ALWAYS validate that new Flatpak IDs exist on Flathub before adding
- NEVER modify validation workflows without understanding impact on PR checks
| 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) |
Use case: Installing Google Chrome, 1Password, VS Code, etc.
Example: See build/20-onepassword.sh.example
Steps:
- Add GPG key (if required)
- Create repo file in
/etc/yum.repos.d/ - Install packages with
dnf5 install -y - 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.repoUse 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-compUse case: Swap GNOME for KDE, COSMIC, etc.
Example: See build/30-cosmic-desktop.sh.example
Steps:
- Remove old desktop:
dnf5 remove -y gnome-shell ... - Install new desktop:
copr_install_isolated ... - Configure display manager:
systemctl enable ... - Set default session
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.targetLocation: 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!)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-qcow2Alternative: Build ISO for installation testing
just build
just build-iso
just run-vm-isoSetup pre-commit hooks locally:
# Install pre-commit
pip install pre-commit
# Install hooks
pre-commit install
# Run manually
pre-commit run --all-filesNote: Pre-commit config exists (.pre-commit-config.yaml) but is optional. CI validation runs automatically on PRs.
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- Local
justcommands support your platform - Most UBlue images support amd64/arm64
- Add
-arm64suffix if needed:bluefin-arm64:stable - Cross-platform builds require additional setup
See build/copr-install-functions.sh for reusable patterns:
copr_install_isolated- Enable COPR, install packages, disable COPR- Follow @ublue-os/bluefin conventions exactly
- Base Image - Pulls base image specified in
ContainerfileFROM line - Context Stage - Mounts
build/andcustom/directories - 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)
- Container Lint - Validates final image with
bootc container lint - Push to Registry - Uploads to GitHub Container Registry (ghcr.io)
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/
- Brewfiles →
Runtime (installed after deployment):
- Homebrew packages (user runs
ujust install-*) - Flatpak applications (installed on first boot, requires internet)
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
Efficient layering:
- Each
RUNcommand creates a new layer - Layers are cached between builds
- Changes near end of Containerfile = faster rebuilds
- Use
--mount=type=cachefor package managers
Best practices:
- Group related
dnf5 installcommands together - Don't install and remove in same layer
- Clean up in same RUN command as install
Main branch (production releases):
stable- Latest stable release (recommended)stable.20250129- Datestamped stable release20250129- Date onlyv1.0.0- Version from Release Please
PR builds:
pr-123- Pull request numbersha-abc123- Git commit SHA (short)
When user requests customization, check in this order:
build/10-build.sh(50%) - Build-time packages, services, system configscustom/brew/(20%) - Runtime CLI tools, dev environmentscustom/ujust/(15%) - User convenience commandscustom/flatpaks/(5%) - GUI applicationsContainerfile(5%) - Base image, /opt config, advanced buildsJustfile(2%) - Image name, build parametersiso/*.toml(2%) - ISO/disk customization for testing.github/workflows/(1%) - Metadata, triggers, workflow config
Do NOT modify unless specifically requested or necessary:
.github/renovate.json5- Renovate configuration (auto-updates).github/workflows/validate-*.yml- Validation workflows.gitignore- Prevents committing secretsbuild/copr-helpers.sh- Helper functions (stable patterns)LICENSE- Repository licensecosign.pub- Public signing key (regenerate if changing keys)
Modify with extreme caution:
.github/workflows/build.yml- Core build workflow.github/workflows/clean.yml- Image cleanupJustfile- Local build automation (users rely on these commands)
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 manuallyBrewfile issues:
# Validate Brewfile syntax
brew bundle check --file custom/brew/default.Brewfile
# List what would be installed
brew bundle list --file custom/brew/default.BrewfileJust 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-appsCheck workflow logs:
- Go to Actions tab in GitHub
- Click on failed workflow run
- Expand failed step
- 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 bashAfter 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.firefoxHomebrew debugging:
# Check Homebrew status
brew doctor
# Check Brewfile
cat /usr/share/ublue-os/homebrew/default.Brewfile
# Install manually
brew install package-name- Bluefin patterns: https://github.com/ublue-os/bluefin
- bootc documentation: https://github.com/containers/bootc
- Conventional Commits: https://www.conventionalcommits.org/
- RPMfusion packages: https://mirrors.rpmfusion.org/
- Flatpak IDs: https://flathub.org/
- Homebrew: https://brew.sh/
- Universal Blue: https://universal-blue.org/
- Renovate: https://docs.renovatebot.com/
- GitHub Actions: https://docs.github.com/en/actions
- Podman: https://podman.io/
- Justfile: https://just.systems/
- 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.
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