Initial commit

This commit is contained in:
humocs-man
2026-02-17 17:27:26 +01:00
committed by GitHub
commit 302bbf2239
44 changed files with 4200 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# Repository Setup Checklist
## Initial Setup
### 1. Rename Template
- [ ] Update `finpilot` to your name in: Containerfile, Justfile, README.md, artifacthub-repo.yml
### 2. Enable GitHub Actions
- [ ] Settings → Actions → General → Enable workflows
- [ ] Set "Read and write permissions"
### 3. First Push
```bash
git add .
git commit -m "feat: initial customization"
git push origin main
```
### 4. Deploy
```bash
sudo bootc switch --transport registry ghcr.io/YOUR_USERNAME/YOUR_REPO:stable
sudo systemctl reboot
```
## Optional: Production Features
### Enable Signing (Recommended)
```bash
cosign generate-key-pair
# Add cosign.key to GitHub Secrets as SIGNING_SECRET
# Uncomment signing in .github/workflows/build.yml
```
+116
View File
@@ -0,0 +1,116 @@
# Conventional Commits for bootc Image Template
This repository follows [Conventional Commits v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) specification.
## Commit Message Format
```
<type>(<scope>): <subject>
[optional body]
[optional footer(s)]
```
## Types
Use these commit types for changes to this bootc image template:
- **build**: Changes to the image build process (Containerfile, build.sh)
- **ci**: Changes to GitHub Actions workflows
- **config**: Changes to disk image configurations (TOML files)
- **feat**: New features or packages added to the image
- **fix**: Bug fixes or corrections
- **docs**: Documentation changes only
- **chore**: Maintenance tasks (dependencies, metadata)
- **refactor**: Code restructuring without changing behavior
## Scopes
Use these scopes to indicate what part of the image is affected:
- **base**: Base image changes (FROM line in Containerfile)
- **packages**: Package installation/removal
- **services**: Systemd service configuration
- **disk**: Disk image configuration (ISO, QCOW2, RAW)
- **workflow**: GitHub Actions workflows
- **metadata**: Image metadata and branding
- **signing**: Cosign key and signing configuration
## Examples
### Adding Packages
```
feat(packages): add development tools and editors
- Install neovim, git, and htop
- Add Development Tools group
```
### Changing Base Image
```
build(base): switch from Bazzite to Bluefin
Changing to Bluefin for better developer tooling support
```
### Configuring Services
```
feat(services): enable SSH and container services
- Enable sshd.service for remote access
- Enable podman.socket for rootless containers
```
### ISO Configuration
```
config(disk): update ISO to use custom registry
Update bootc switch URL to point to ghcr.io/username/repo
```
### Workflow Changes
```
ci(workflow): enable automatic ISO builds
Configure build-disk.yml to trigger on main branch pushes
```
### Metadata Updates
```
chore(metadata): update image description and keywords
Update ArtifactHub metadata with project-specific information
```
### Documentation
```
docs: add installation instructions for new features
```
### Bug Fixes
```
fix(packages): disable COPR after package installation
Ensure COPR repositories don't persist in final image
```
## Breaking Changes
If a change is breaking (e.g., removing packages, changing base image significantly), add `!` after the type/scope:
```
build(base)!: migrate from Fedora 40 to Fedora 41
BREAKING CHANGE: This upgrade requires a clean reinstall for existing users.
```
## Tips
- Keep subject line under 50 characters
- Use imperative mood ("add" not "added" or "adds")
- Don't end subject line with a period
- Separate subject from body with a blank line
- Use body to explain *what* and *why*, not *how*
- Be short and concise, avoid long commit messages, keep it simple
- Reference issues/PRs in footer when applicable
+16
View File
@@ -0,0 +1,16 @@
# Copilot Instructions for finpilot
This repository uses comprehensive agent instructions. Please refer to the main documentation file:
**[AGENTS.md](../AGENTS.md)** in the repository root.
This file contains:
- Repository structure and organization
- Build system details and workflows
- Package management guidelines
- Development best practices
- Validation and testing procedures
- Troubleshooting guides
- Attribution requirements for AI agents
All coding agents should consult AGENTS.md for detailed instructions on working with this bootc image template.
+63
View File
@@ -0,0 +1,63 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices",
],
// Do not rebase when the default branch is updated
"rebaseWhen": "never",
customManagers: [
{
customType: 'regex',
description: 'Match container images in Justfile with tag and digest',
managerFilePatterns: [
'^Justfile$',
],
matchStrings: [
'(?<justName>.+?)\\s:=\\s(?:env\\(.+?,\\s)?["\'](?<packageName>\\S+?):(?<currentValue>\\S+?)@(?<currentDigest>sha256:[a-f0-9]+?)["\']\\)?',
],
datasourceTemplate: 'docker',
},
{
customType: 'regex',
description: 'Match container images in Justfile run_args without tag or digest',
managerFilePatterns: [
'^Justfile$',
],
matchStrings: [
'run_args\\+=\\((?<packageName>[a-zA-Z0-9._:/-]+):?(?<currentValue>\\S+?)\\)',
],
datasourceTemplate: 'docker',
versioningTemplate: 'docker',
},
],
"packageRules": [
{
"automerge": true,
"matchUpdateTypes": ["pin", "pinDigest"]
},
{
"description": "Auto-merge digest updates for base image and trigger build",
"automerge": true,
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["digest"],
"matchFileNames": ["Containerfile"],
"labels": ["renovate", "automerge"],
},
{
"description": "Disable automerge for all rechunk updates to allow manual review",
"matchUpdateTypes": ["major", "minor", "patch", "digest", "pinDigest"],
"matchPackageNames": ["ghcr.io/hhd-dev/rechunk"],
"automerge": false
},
{
"description": "Enable GitHub Actions updates since they are now pinned with SHA",
"enabled": true,
"matchUpdateTypes": ["digest", "pinDigest", "pin", "major", "minor", "patch"],
"matchDepTypes": ["action"],
"matchFileNames": [".github/workflows/**.yaml", ".github/workflows/**.yml"],
},
]
}
+218
View File
@@ -0,0 +1,218 @@
---
name: Build container image
on:
pull_request:
branches:
- main
schedule:
- cron: '05 10 * * *' # 10:05am UTC everyday
push:
branches:
- main
paths-ignore:
- '**/README.md'
workflow_dispatch:
env:
IMAGE_DESC: "My Customized Universal Blue Image"
IMAGE_KEYWORDS: "bootc,ublue,universal-blue"
IMAGE_LOGO_URL: "https://avatars.githubusercontent.com/u/120078124?s=200&v=4" # Put your own image here for a fancy profile on https://artifacthub.io/!
IMAGE_NAME: "${{ github.event.repository.name }}" # output image name, usually same as repo name
IMAGE_REGISTRY: "ghcr.io/${{ github.repository_owner }}" # do not edit
DEFAULT_TAG: "stable"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}-${{ inputs.brand_name}}-${{ inputs.stream_name }}
cancel-in-progress: true
jobs:
build_push:
name: Build and push image
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Prepare environment
run: |
# Lowercase the image uri
echo "IMAGE_REGISTRY=${IMAGE_REGISTRY,,}" >> ${GITHUB_ENV}
echo "IMAGE_NAME=${IMAGE_NAME,,}" >> ${GITHUB_ENV}
# These stage versions are pinned by https://github.com/renovatebot/renovate
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
# This is optional, but if you see that your builds are way too big for the runners, you can enable this by uncommenting the following lines:
# - name: Maximize build space
# uses: ublue-os/remove-unwanted-software@517622d6452028f266b7ba4cc9a123b5f58a6b53 # v7
# with:
# remove-codeql: true
- name: Mount BTRFS for podman storage
id: container-storage-action
uses: ublue-os/container-storage-action@911baca08baf30c8654933e9e9723cb399892140 # main
# Fallback to the remove-unwanted-software-action if github doesn't allocate enough space
# See: https://github.com/ublue-os/container-storage-action/pull/11
continue-on-error: true
with:
target-dir: /var/lib/containers
mount-opts: compress-force=zstd:2
- name: Get current date
id: date
run: |
# This generates a timestamp like what is defined on the ArtifactHub documentation
# E.G: 2022-02-08T15:38:15Z'
# https://artifacthub.io/docs/topics/repositories/container-images/
# https://linux.die.net/man/1/date
echo "date=$(date -u +%Y\-%m\-%d\T%H\:%M\:%S\Z)" >> $GITHUB_OUTPUT
# Image metadata for https://artifacthub.io/ - This is optional but is highly recommended so we all can get a index of all the custom images
# The metadata by itself is not going to do anything, you choose if you want your image to be on ArtifactHub or not.
- name: Image Metadata
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5
id: metadata
with:
# This generates all the tags for your image, you can add custom tags here too!
# Default tags are "$DEFAULT_TAG" and "$DEFAULT_TAG.$date".
tags: |
type=raw,value=${{ env.DEFAULT_TAG }}
type=raw,value=${{ env.DEFAULT_TAG }}.{{date 'YYYYMMDD'}}
type=raw,value={{date 'YYYYMMDD'}}
type=sha,enable=${{ github.event_name == 'pull_request' }}
type=ref,event=pr
labels: |
io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/${{ github.sha }}/README.md
org.opencontainers.image.created=${{ steps.date.outputs.date }}
org.opencontainers.image.description=${{ env.IMAGE_DESC }}
org.opencontainers.image.documentation=https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/${{ github.sha }}/README.md
org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/blob/${{ github.sha }}/Containerfile
org.opencontainers.image.title=${{ env.IMAGE_NAME }}
org.opencontainers.image.url=https://github.com/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}/tree/${{ github.sha }}
org.opencontainers.image.vendor=${{ github.repository_owner }}
org.opencontainers.image.version=${{ env.DEFAULT_TAG }}.{{date 'YYYYMMDD'}}
io.artifacthub.package.deprecated=false
io.artifacthub.package.keywords=${{ env.IMAGE_KEYWORDS }}
io.artifacthub.package.license=Apache-2.0
io.artifacthub.package.logo-url=${{ env.IMAGE_LOGO_URL }}
io.artifacthub.package.prerelease=false
containers.bootc=1
sep-tags: " "
sep-annotations: " "
- name: Build Image
id: build_image
uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2
with:
containerfiles: |
./Containerfile
# Postfix image name with -custom to make it a little more descriptive
# Syntax: https://docs.github.com/en/actions/learn-github-actions/expressions#format
image: ${{ env.IMAGE_NAME }}
tags: ${{ env.DEFAULT_TAG }}
labels: ${{ steps.metadata.outputs.labels }}
oci: false
# Tag for registry
- name: Tag for registry
run: |
for tag in ${{ steps.metadata.outputs.tags }}; do
podman tag ${{ env.IMAGE_NAME }}:${{ env.DEFAULT_TAG }} ${{ env.IMAGE_NAME }}:$tag
done
# SBOM (OPTIONAL): Software Bill of Materials generation using Syft
# Creates a detailed inventory of all software components in the image for supply chain security.
# To enable:
# 1. Uncomment the "Setup Syft" step below
# 2. Uncomment the "Generate SBOM" step below
# 3. If using image signing, also uncomment the "Add SBOM Attestation" step further down
# Documentation: https://github.com/anchore/syft
# - name: Setup Syft
# id: setup-syft
# uses: anchore/sbom-action/download-syft@8e94d75ddd33f69f691467e42275782e4bfefe84 # v0.20.9
# with:
# syft-version: v1.20.0
#
# - name: Generate SBOM
# id: generate-sbom
# env:
# IMAGE: ${{ env.IMAGE_NAME }}
# DEFAULT_TAG: ${{ env.DEFAULT_TAG }}
# SYFT_CMD: ${{ steps.setup-syft.outputs.cmd }}
# run: |
# OUTPUT_PATH="$(mktemp -d)/sbom.json"
# export SYFT_PARALLELISM=$(($(nproc)*2))
# $SYFT_CMD ${IMAGE}:${DEFAULT_TAG} -o spdx-json=${OUTPUT_PATH}
# echo "OUTPUT_PATH=${OUTPUT_PATH}" >> $GITHUB_OUTPUT
# These `if` statements are so that pull requests for your custom images do not make it publish any packages under your name without you knowing
# They also check if the runner is on the default branch so that things like the merge queue (if you enable it), are going to work
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push To GHCR
uses: redhat-actions/push-to-registry@5ed88d269cf581ea9ef6dd6806d01562096bee9c # v2
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
id: push
env:
REGISTRY_USER: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ github.token }}
with:
registry: ${{ env.IMAGE_REGISTRY }}
image: ${{ env.IMAGE_NAME }}
tags: ${{ steps.metadata.outputs.tags }}
username: ${{ env.REGISTRY_USER }}
password: ${{ env.REGISTRY_PASSWORD }}
# OPTIONAL: Image Signing with Cosign
# Signing is disabled by default. To enable, see README.md "Optional: Enable Image Signing" section.
# - name: Install Cosign
# uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
# if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
#
# - name: Sign container image
# if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
# run: |
# IMAGE_FULL="${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}"
# for tag in ${{ steps.metadata.outputs.tags }}; do
# cosign sign -y --key env://COSIGN_PRIVATE_KEY $IMAGE_FULL:$tag
# done
# env:
# TAGS: ${{ steps.push.outputs.digest }}
# COSIGN_EXPERIMENTAL: false
# COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
# OPTIONAL: SBOM Attestation
# Attaches SBOM (Software Bill of Materials) to your signed image for supply chain security.
# Requires image signing to be enabled first (see above).
# To enable:
# 1. First enable image signing (see above section)
# 2. Uncomment the "Add SBOM Attestation" step below
# Documentation: https://slsa.dev/spec/v1.0/requirements#provenance-available
# - name: Add SBOM Attestation
# if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
# env:
# IMAGE: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}
# DIGEST: ${{ steps.push.outputs.digest }}
# COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
# SBOM_OUTPUT: ${{ steps.generate-sbom.outputs.OUTPUT_PATH }}
# run: |
# cd "$(dirname "$SBOM_OUTPUT")"
# cosign attest -y \
# --predicate "$(basename "$SBOM_OUTPUT")" \
# --type spdxjson \
# --key env://COSIGN_PRIVATE_KEY \
# "${IMAGE}@${DIGEST}"
+27
View File
@@ -0,0 +1,27 @@
name: Cleanup Old Images
on:
schedule:
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
jobs:
delete-older-than-90:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Delete Images Older Than 90 Days
uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 # v1.0.16
with:
token: ${{ secrets.GITHUB_TOKEN }}
# IMPORTANT: Update this to match your image name from Containerfile (line 9)
# This should match the "# Name: your-repo-name" comment in your Containerfile
# Example: If your Containerfile has "# Name: my-custom-os", use "my-custom-os" here
packages: finpilot
older-than: 90 days
delete-orphaned-images: true
keep-n-tagged: 7
keep-n-untagged: 7
+31
View File
@@ -0,0 +1,31 @@
name: Renovate
on:
schedule:
# Run every 6 hours
- cron: '0 */6 * * *'
workflow_dispatch:
push:
branches:
- main
paths:
- '.github/renovate.json5'
- '.github/workflows/renovate.yml'
jobs:
renovate:
name: Run Renovate
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Self-hosted Renovate
uses: renovatebot/github-action@b9486682a19fe4ea2dd1263c11b4e0c1fe1838fe # v40.3.2
with:
configurationFile: .github/renovate.json5
token: ${{ secrets.GITHUB_TOKEN }}
env:
LOG_LEVEL: 'debug'
+38
View File
@@ -0,0 +1,38 @@
name: Validate Brewfiles
permissions:
contents: read
on:
pull_request:
paths:
- "custom/brew/**"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v5
- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@1ccc07ccd54b6048295516a3eb89b192c35057dc # master
- name: Validate Brewfiles
shell: bash
run: |
echo "Validating Brewfiles in custom/brew/ directory..."
find "custom/brew" -iname '*\.Brewfile*' -print0 | \
while IFS= read -r -d '' brewfile ; do \
echo "::group:: ===$(basename $brewfile)==="
grep -E -e "^tap" $brewfile > taps.Brewfile || echo "# No taps" > taps.Brewfile
set -xeuo pipefail
brew bundle --file=./taps.Brewfile
if brew bundle exec whoami --file="$brewfile" | grep -F -e "${USER}" ; then
echo "✓ $brewfile is valid"
else
echo "✗ $brewfile validation failed"
exit 1
fi
set +xeuo pipefail
echo "::endgroup::"
done
+37
View File
@@ -0,0 +1,37 @@
name: Validate Flatpaks
permissions:
contents: read
on:
pull_request:
paths:
- "custom/flatpak/**"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v5
- name: Set up Flathub
shell: bash
run: |
sudo apt install -y flatpak
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
- name: Validate Flatpaks
shell: bash
run: |
echo "Validating flatpak files in custom/flatpak/ directory..."
set -xeuo pipefail
find "custom/flatpak" -iname '*\.list*' -print0 | \
while IFS= read -r -d '' flatpaks_file ; do \
echo "::group:: ===$(basename "$flatpaks_file")==="
grep -v "#.*" "$flatpaks_file" | grep -v "^$" | \
while read -r flatpak ; do \
flatpak remote-info --user flathub "${flatpak}"
done
echo "::endgroup::"
done
+40
View File
@@ -0,0 +1,40 @@
name: Validate Justfiles
permissions:
contents: read
on:
pull_request:
paths:
- "custom/ujust/**"
- "Justfile"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v5
- name: Install just
uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2
- name: Validate Justfiles
shell: bash
run: |
echo "Validating justfiles in custom/ujust/ directory..."
set -xeuo pipefail
# Validate root Justfile
echo "::group:: Validating Justfile"
just --unstable --fmt --check -f Justfile
echo "::endgroup::"
# Validate custom ujust files
find "custom/ujust" -iname '*.just' -print0 | \
while IFS= read -r -d '' justfile ; do \
echo "::group:: ===$(basename "$justfile")==="
just --unstable --fmt --check -f "$justfile"
echo "::endgroup::"
done
echo "All justfiles are valid"
+35
View File
@@ -0,0 +1,35 @@
name: Validate Renovate Config
on:
pull_request:
paths:
- ".github/renovate.json5"
- ".github/workflows/renovate.yml"
push:
branches:
- main
paths:
- ".github/renovate.json5"
- ".github/workflows/renovate.yml"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v5
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v6
with:
node-version: latest
- name: Install dependencies
shell: bash
env:
RENOVATE_VERSION: latest
run: npm install -g renovate@${RENOVATE_VERSION}
- name: Validate Renovate config
shell: bash
run: renovate-config-validator --strict
+37
View File
@@ -0,0 +1,37 @@
name: Validate Shell Scripts
permissions:
contents: read
on:
pull_request:
paths:
- "build/**/*.sh"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v5
- name: Install ShellCheck
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y shellcheck
- name: Validate Shell Scripts
shell: bash
run: |
echo "Running shellcheck on build scripts..."
set -xeuo pipefail
find "build" -iname '*.sh' -print0 | \
while IFS= read -r -d '' script ; do \
echo "::group:: ===$(basename "$script")==="
shellcheck -x "$script"
echo "✓ $script passed shellcheck"
echo "::endgroup::"
done
echo "All shell scripts passed validation"
+4
View File
@@ -0,0 +1,4 @@
cosign.key
_build_*
output
_build-*/**
+20
View File
@@ -0,0 +1,20 @@
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-json
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: local
hooks:
- id: validate-brewfiles
name: Validate Brewfiles
entry: bash -c 'if command -v brew &> /dev/null; then for file in brew/*.Brewfile*; do [ -f "$file" ] || continue; brew bundle check --file="$file" || exit 1; done; else echo "⚠️ Homebrew not installed, skipping Brewfile validation"; fi'
language: system
files: '^brew/.*\.Brewfile.*$'
pass_filenames: false
+1108
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
# CONTRIBUTING
Thanks for helping out!
Check the [Contributing Guide](https://docs.projectbluefin.io/contributing) for contribution information.
This repository is for building the images, you are probably looking for [@projectbluefin/common](https://github.com/projectbluefin/common) to change something in Bluefin. Make sure you check [the architecture diagram](https://docs.projectbluefin.io/contributing#understanding-bluefins-architecture).
+87
View File
@@ -0,0 +1,87 @@
###############################################################################
# PROJECT NAME CONFIGURATION
###############################################################################
# Name: finpilot
#
# IMPORTANT: Change "finpilot" above to your desired project name.
# This name should be used consistently throughout the repository in:
# - Justfile: export image_name := env("IMAGE_NAME", "your-name-here")
# - README.md: # your-name-here (title)
# - artifacthub-repo.yml: repositoryID: your-name-here
# - custom/ujust/README.md: localhost/your-name-here:stable (in bootc switch example)
#
# The project name defined here is the single source of truth for your
# custom image's identity. When changing it, update all references above
# to maintain consistency.
###############################################################################
###############################################################################
# MULTI-STAGE BUILD ARCHITECTURE
###############################################################################
# This Containerfile follows the Bluefin architecture pattern as implemented in
# @projectbluefin/distroless. The architecture layers OCI containers together:
#
# 1. Context Stage (ctx) - Combines resources from:
# - Local build scripts and custom files
# - @projectbluefin/common - Desktop configuration shared with Aurora
# - @ublue-os/brew - Homebrew integration
#
# 2. Base Image Options:
# - `ghcr.io/ublue-os/silverblue-main:latest` (Fedora and GNOME)
# - `ghcr.io/ublue-os/base-main:latest` (Fedora and no desktop
# - `quay.io/centos-bootc/centos-bootc:stream10 (CentOS-based)`
#
# See: https://docs.projectbluefin.io/contributing/ for architecture diagram
###############################################################################
# Context stage - combine local and imported OCI container resources
FROM scratch AS ctx
COPY build /build
COPY custom /custom
# Copy from OCI containers to distinct subdirectories to avoid conflicts
# Note: Renovate can automatically update these :latest tags to SHA-256 digests for reproducibility
COPY --from=ghcr.io/projectbluefin/common:latest /system_files /oci/common
COPY --from=ghcr.io/ublue-os/brew:latest /system_files /oci/brew
# Base Image - GNOME included
FROM ghcr.io/ublue-os/silverblue-main:latest
## Alternative base images, no desktop included (uncomment to use):
# FROM ghcr.io/ublue-os/base-main:latest
# FROM quay.io/centos-bootc/centos-bootc:stream10
## Alternative GNOME OS base image (uncomment to use):
# FROM quay.io/gnome_infrastructure/gnome-build-meta:gnomeos-nightly
### /opt
## Some bootable images, like Fedora, have /opt symlinked to /var/opt, in order to
## make it mutable/writable for users. However, some packages write files to this directory,
## thus its contents might be wiped out when bootc deploys an image, making it troublesome for
## some packages. Eg, google-chrome, docker-desktop.
##
## Uncomment the following line if one desires to make /opt immutable and be able to be used
## by the package manager.
# RUN rm /opt && mkdir /opt
### MODIFICATIONS
## Make modifications desired in your image and install packages by modifying the build scripts.
## The following RUN directive mounts the ctx stage which includes:
## - Local build scripts from /build
## - Local custom files from /custom
## - Files from @projectbluefin/common at /oci/common
## - Files from @projectbluefin/branding at /oci/branding
## - Files from @ublue-os/artwork at /oci/artwork
## - Files from @ublue-os/brew at /oci/brew
## Scripts are run in numerical order (10-build.sh, 20-example.sh, etc.)
RUN --mount=type=bind,from=ctx,source=/,target=/ctx \
--mount=type=cache,dst=/var/cache \
--mount=type=cache,dst=/var/log \
--mount=type=tmpfs,dst=/tmp \
/ctx/build/10-build.sh
### LINTING
## Verify final image and contents are correct.
RUN bootc container lint
+318
View File
@@ -0,0 +1,318 @@
export image_name := env("IMAGE_NAME", "finpilot")
export default_tag := env("DEFAULT_TAG", "stable")
export bib_image := env("BIB_IMAGE", "quay.io/centos-bootc/bootc-image-builder:latest@sha256:903c01d110b8533f8891f07c69c0ba2377f8d4bc7e963311082b7028c04d529d")
alias build-vm := build-qcow2
alias rebuild-vm := rebuild-qcow2
alias run-vm := run-vm-qcow2
[private]
default:
@just --list
# Check Just Syntax
[group('Just')]
check:
#!/usr/bin/bash
find . -type f -name "*.just" | while read -r file; do
echo "Checking syntax: $file"
just --unstable --fmt --check -f $file
done
echo "Checking syntax: Justfile"
just --unstable --fmt --check -f Justfile
# Fix Just Syntax
[group('Just')]
fix:
#!/usr/bin/bash
find . -type f -name "*.just" | while read -r file; do
echo "Checking syntax: $file"
just --unstable --fmt -f $file
done
echo "Checking syntax: Justfile"
just --unstable --fmt -f Justfile || { exit 1; }
# Clean Repo
[group('Utility')]
clean:
#!/usr/bin/bash
set -eoux pipefail
touch _build
find *_build* -exec rm -rf {} \;
rm -f previous.manifest.json
rm -f changelog.md
rm -f output.env
rm -f output/
# Sudo Clean Repo
[group('Utility')]
[private]
sudo-clean:
just sudoif just clean
# sudoif bash function
[group('Utility')]
[private]
sudoif command *args:
#!/usr/bin/bash
function sudoif(){
if [[ "${UID}" -eq 0 ]]; then
"$@"
elif [[ "$(command -v sudo)" && -n "${SSH_ASKPASS:-}" ]] && [[ -n "${DISPLAY:-}" || -n "${WAYLAND_DISPLAY:-}" ]]; then
/usr/bin/sudo --askpass "$@" || exit 1
elif [[ "$(command -v sudo)" ]]; then
/usr/bin/sudo "$@" || exit 1
else
exit 1
fi
}
sudoif {{ command }} {{ args }}
# This Justfile recipe builds a container image using Podman.
#
# Arguments:
# $target_image - The tag you want to apply to the image (default: $image_name).
# $tag - The tag for the image (default: $default_tag).
#
# The script constructs the version string using the tag and the current date.
# If the git working directory is clean, it also includes the short SHA of the current HEAD.
#
# just build $target_image $tag
#
# Example usage:
# just build aurora lts
#
# This will build an image 'aurora:lts' with DX and GDX enabled.
#
# Build the image using the specified parameters
build $target_image=image_name $tag=default_tag:
#!/usr/bin/env bash
BUILD_ARGS=()
if [[ -z "$(git status -s)" ]]; then
BUILD_ARGS+=("--build-arg" "SHA_HEAD_SHORT=$(git rev-parse --short HEAD)")
fi
podman build \
"${BUILD_ARGS[@]}" \
--pull=newer \
--tag "${target_image}:${tag}" \
.
# Command: _rootful_load_image
# Description: This script checks if the current user is root or running under sudo. If not, it attempts to resolve the image tag using podman inspect.
# If the image is found, it loads it into rootful podman. If the image is not found, it pulls it from the repository.
#
# Parameters:
# $target_image - The name of the target image to be loaded or pulled.
# $tag - The tag of the target image to be loaded or pulled. Default is 'default_tag'.
#
# Example usage:
# _rootful_load_image my_image latest
#
# Steps:
# 1. Check if the script is already running as root or under sudo.
# 2. Check if target image is in the non-root podman container storage)
# 3. If the image is found, load it into rootful podman using podman scp.
# 4. If the image is not found, pull it from the remote repository into reootful podman.
_rootful_load_image $target_image=image_name $tag=default_tag:
#!/usr/bin/bash
set -eoux pipefail
# Check if already running as root or under sudo
if [[ -n "${SUDO_USER:-}" || "${UID}" -eq "0" ]]; then
echo "Already root or running under sudo, no need to load image from user podman."
exit 0
fi
# Try to resolve the image tag using podman inspect
set +e
resolved_tag=$(podman inspect -t image "${target_image}:${tag}" | jq -r '.[].RepoTags.[0]')
return_code=$?
set -e
USER_IMG_ID=$(podman images --filter reference="${target_image}:${tag}" --format "'{{ '{{.ID}}' }}'")
if [[ $return_code -eq 0 ]]; then
# If the image is found, load it into rootful podman
ID=$(just sudoif podman images --filter reference="${target_image}:${tag}" --format "'{{ '{{.ID}}' }}'")
if [[ "$ID" != "$USER_IMG_ID" ]]; then
# If the image ID is not found or different from user, copy the image from user podman to root podman
COPYTMP=$(mktemp -p "${PWD}" -d -t _build_podman_scp.XXXXXXXXXX)
just sudoif TMPDIR=${COPYTMP} podman image scp ${UID}@localhost::"${target_image}:${tag}" root@localhost::"${target_image}:${tag}"
rm -rf "${COPYTMP}"
fi
else
# If the image is not found, pull it from the repository
just sudoif podman pull "${target_image}:${tag}"
fi
# Build a bootc bootable image using Bootc Image Builder (BIB)
# Converts a container image to a bootable image
# Parameters:
# target_image: The name of the image to build (ex. localhost/fedora)
# tag: The tag of the image to build (ex. latest)
# type: The type of image to build (ex. qcow2, raw, iso)
# config: The configuration file to use for the build (default: iso/disk.toml)
# Example: just _rebuild-bib localhost/fedora latest qcow2 iso/disk.toml
_build-bib $target_image $tag $type $config: (_rootful_load_image target_image tag)
#!/usr/bin/env bash
set -euo pipefail
args="--type ${type} "
args+="--use-librepo=True "
args+="--rootfs=btrfs"
BUILDTMP=$(mktemp -p "${PWD}" -d -t _build-bib.XXXXXXXXXX)
sudo podman run \
--rm \
-it \
--privileged \
--pull=newer \
--net=host \
--security-opt label=type:unconfined_t \
-v $(pwd)/${config}:/config.toml:ro \
-v $BUILDTMP:/output \
-v /var/lib/containers/storage:/var/lib/containers/storage \
"${bib_image}" \
${args} \
"${target_image}:${tag}"
mkdir -p output
sudo mv -f $BUILDTMP/* output/
sudo rmdir $BUILDTMP
sudo chown -R $USER:$USER output/
# Podman builds the image from the Containerfile and creates a bootable image
# Parameters:
# target_image: The name of the image to build (ex. localhost/fedora)
# tag: The tag of the image to build (ex. latest)
# type: The type of image to build (ex. qcow2, raw, iso)
# config: The configuration file to use for the build (deafult: iso/disk.toml)
# Example: just _rebuild-bib localhost/fedora latest qcow2 iso/disk.toml
_rebuild-bib $target_image $tag $type $config: (build target_image tag) && (_build-bib target_image tag type config)
# Build a QCOW2 virtual machine image
[group('Build Virtal Machine Image')]
build-qcow2 $target_image=("localhost/" + image_name) $tag=default_tag: && (_build-bib target_image tag "qcow2" "iso/disk.toml")
# Build a RAW virtual machine image
[group('Build Virtal Machine Image')]
build-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_build-bib target_image tag "raw" "iso/disk.toml")
# Build an ISO virtual machine image
[group('Build Virtal Machine Image')]
build-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_build-bib target_image tag "iso" "iso/iso.toml")
# Rebuild a QCOW2 virtual machine image
[group('Build Virtal Machine Image')]
rebuild-qcow2 $target_image=("localhost/" + image_name) $tag=default_tag: && (_rebuild-bib target_image tag "qcow2" "iso/disk.toml")
# Rebuild a RAW virtual machine image
[group('Build Virtal Machine Image')]
rebuild-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_rebuild-bib target_image tag "raw" "iso/disk.toml")
# Rebuild an ISO virtual machine image
[group('Build Virtal Machine Image')]
rebuild-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_rebuild-bib target_image tag "iso" "iso/iso.toml")
# Run a virtual machine with the specified image type and configuration
_run-vm $target_image $tag $type $config:
#!/usr/bin/bash
set -eoux pipefail
# Determine the image file based on the type
image_file="output/${type}/disk.${type}"
if [[ $type == iso ]]; then
image_file="output/bootiso/install.iso"
fi
# Build the image if it does not exist
if [[ ! -f "${image_file}" ]]; then
just "build-${type}" "$target_image" "$tag"
fi
# Determine an available port to use
port=8006
while grep -q :${port} <<< $(ss -tunalp); do
port=$(( port + 1 ))
done
echo "Using Port: ${port}"
echo "Connect to http://localhost:${port}"
# Set up the arguments for running the VM
run_args=()
run_args+=(--rm --privileged)
run_args+=(--pull=newer)
run_args+=(--publish "127.0.0.1:${port}:8006")
run_args+=(--env "CPU_CORES=4")
run_args+=(--env "RAM_SIZE=8G")
run_args+=(--env "DISK_SIZE=64G")
run_args+=(--env "TPM=Y")
run_args+=(--env "GPU=Y")
run_args+=(--device=/dev/kvm)
run_args+=(--volume "${PWD}/${image_file}":"/boot.${type}")
run_args+=(docker.io/qemux/qemu)
# Run the VM and open the browser to connect
(sleep 30 && xdg-open http://localhost:"$port") &
podman run "${run_args[@]}"
# Run a virtual machine from a QCOW2 image
[group('Run Virtal Machine')]
run-vm-qcow2 $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "qcow2" "iso/disk.toml")
# Run a virtual machine from a RAW image
[group('Run Virtal Machine')]
run-vm-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "raw" "iso/disk.toml")
# Run a virtual machine from an ISO
[group('Run Virtal Machine')]
run-vm-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "iso" "iso/iso.toml")
# Run a virtual machine using systemd-vmspawn
[group('Run Virtal Machine')]
spawn-vm rebuild="0" type="qcow2" ram="6G":
#!/usr/bin/env bash
set -euo pipefail
[ "{{ rebuild }}" -eq 1 ] && echo "Rebuilding the ISO" && just build-vm {{ rebuild }} {{ type }}
systemd-vmspawn \
-M "bootc-image" \
--console=gui \
--cpus=2 \
--ram=$(echo {{ ram }}| /usr/bin/numfmt --from=iec) \
--network-user-mode \
--vsock=false --pass-ssh-key=false \
-i ./output/**/*.{{ type }}
# Runs shell check on all Bash scripts
lint:
#!/usr/bin/env bash
set -eoux pipefail
# Check if shellcheck is installed
if ! command -v shellcheck &> /dev/null; then
echo "shellcheck could not be found. Please install it."
exit 1
fi
# Run shellcheck on all Bash scripts
/usr/bin/find . -iname "*.sh" -type f -exec shellcheck "{}" ';'
# Runs shfmt on all Bash scripts
format:
#!/usr/bin/env bash
set -eoux pipefail
# Check if shfmt is installed
if ! command -v shfmt &> /dev/null; then
echo "shellcheck could not be found. Please install it."
exit 1
fi
# Run shfmt on all Bash scripts
/usr/bin/find . -iname "*.sh" -type f -exec shfmt --write "{}" ';'
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+349
View File
@@ -0,0 +1,349 @@
# finpilot
A template for building custom bootc operating system images based on the lessons from [Universal Blue](https://universal-blue.org/) and [Bluefin](https://projectbluefin.io). It is designed to be used manually, but is optimized to be bootstraped by GitHub Copilot. After set up you'll have your own custom Linux.
This template uses the **multi-stage build architecture** from , combining resources from multiple OCI containers for modularity and maintainability. See the [Architecture](#architecture) section below for details.
**Unlike previous templates, you are not modifying Bluefin and making changes.**: You are assembling your own Bluefin in the same exact way that Bluefin, Aurora, and Bluefin LTS are built. This is way more flexible and better for everyone since the image-agnostic and desktop things we love about Bluefin lives in @projectbluefin/common.
Instead, you create your own OS repository based on this template, allowing full customization while leveraging Bluefin's robust build system and shared components.
> Be the one who moves, not the one who is moved.
## Guided Copilot Mode
Here are the steps to guide copilot to make your own repo, or just use it like a regular image template.
1. Click the green "Use this as a template" button and create a new repository
2. Select your owner, pick a repo name for your OS, and a description
3. In the "Jumpstart your project with Copilot (optional)" add this, modify to your liking:
```
Use @projectbluefin/finpilot as a template, name the OS the repository name. Ensure the entire operating system is bootstrapped. Ensure all github actions are enabled and running. Ensure the README has the github setup instructions for cosign and the other steps required to finish the task.
```
## What's Included
### Build System
- Automated builds via GitHub Actions on every commit
- Awesome self hosted Renovate setup that keeps all your images and actions up to date.
- Automatic cleanup of old images (90+ days) to keep it tidy
- Pull request workflow - test changes before merging to main
- PRs build and validate before merge
- `main` branch builds `:stable` images
- Validates your files on pull requests so you never break a build:
- Brewfile, Justfile, ShellCheck, Renovate config, and it'll even check to make sure the flatpak you add exists on FlatHub
- Production Grade Features
- Container signing and SBOM Generation
- See checklist below to enable these as they take some manual configuration
### Homebrew Integration
- Pre-configured Brewfiles for easy package installation and customization
- Includes curated collections: development tools, fonts, CLI utilities. Go nuts.
- Users install packages at runtime with `brew bundle`, aliased to premade `ujust commands`
- See [custom/brew/README.md](custom/brew/README.md) for details
### Flatpak Support
- Ship your favorite flatpaks
- Automatically installed on first boot after user setup
- See [custom/flatpaks/README.md](custom/flatpaks/README.md) for details
### ujust Commands
- User-friendly command shortcuts via `ujust`
- Pre-configured examples for app installation and system maintenance for you to customize
- See [custom/ujust/README.md](custom/ujust/README.md) for details
### Build Scripts
- Modular numbered scripts (10-, 20-, 30-) run in order
- Example scripts included for third-party repositories and desktop replacement
- Helper functions for safe COPR usage
- See [build/README.md](build/README.md) for details
## Quick Start
### 1. Create Your Repository
Click "Use this template" to create a new repository from this template.
### 2. Rename the Project
Important: Change `finpilot` to your repository name in these 6 files:
1. `Containerfile` (line 4): `# Name: your-repo-name`
2. `Justfile` (line 1): `export image_name := env("IMAGE_NAME", "your-repo-name")`
3. `README.md` (line 1): `# your-repo-name`
4. `artifacthub-repo.yml` (line 5): `repositoryID: your-repo-name`
5. `custom/ujust/README.md` (~line 175): `localhost/your-repo-name:stable`
6. `.github/workflows/clean.yml` (line 23): `packages: your-repo-name`
### 3. Enable GitHub Actions
- Go to the "Actions" tab in your repository
- Click "I understand my workflows, go ahead and enable them"
Your first build will start automatically!
Note: Image signing is disabled by default. Your images will build successfully without any signing keys. Once you're ready for production, see "Optional: Enable Image Signing" below.
### 4. Customize Your Image
Choose your base image in `Containerfile` (line 23):
```dockerfile
FROM ghcr.io/ublue-os/bluefin:stable
```
Add your packages in `build/10-build.sh`:
```bash
dnf5 install -y package-name
```
Customize your apps:
- Add Brewfiles in `custom/brew/` ([guide](custom/brew/README.md))
- Add Flatpaks in `custom/flatpaks/` ([guide](custom/flatpaks/README.md))
- Add ujust commands in `custom/ujust/` ([guide](custom/ujust/README.md))
### 5. Development Workflow
All changes should be made via pull requests:
1. Open a pull request on GitHub with the change you want.
3. The PR will automatically trigger:
- Build validation
- Brewfile, Flatpak, Justfile, and shellcheck validation
- Test image build
4. Once checks pass, merge the PR
5. Merging triggers publishes a `:stable` image
### 6. Deploy Your Image
Switch to your image:
```bash
sudo bootc switch ghcr.io/your-username/your-repo-name:stable
sudo systemctl reboot
```
## Optional: Enable Image Signing
Image signing is disabled by default to let you start building immediately. However, signing is strongly recommended for production use.
### Why Sign Images?
- Verify image authenticity and integrity
- Prevent tampering and supply chain attacks
- Required for some enterprise/security-focused deployments
- Industry best practice for production images
### Setup Instructions
1. Generate signing keys:
```bash
cosign generate-key-pair
```
This creates two files:
- `cosign.key` (private key) - Keep this secret
- `cosign.pub` (public key) - Commit this to your repository
2. Add the private key to GitHub Secrets:
- Copy the entire contents of `cosign.key`
- Go to your repository on GitHub
- Navigate to Settings → Secrets and variables → Actions ([GitHub docs](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository))
- Click "New repository secret"
- Name: `SIGNING_SECRET`
- Value: Paste the entire contents of `cosign.key`
- Click "Add secret"
3. Replace the contents of `cosign.pub` with your public key:
- Open `cosign.pub` in your repository
- Replace the placeholder with your actual public key
- Commit and push the change
4. Enable signing in the workflow:
- Edit `.github/workflows/build.yml`
- Find the "OPTIONAL: Image Signing with Cosign" section.
- Uncomment the steps to install Cosign and sign the image (remove the `#` from the beginning of each line in that section).
- Commit and push the change
5. Your next build will produce signed images!
Important: Never commit `cosign.key` to the repository. It's already in `.gitignore`.
## Love Your Image? Let's Go to Production
Ready to take your custom OS to production? Enable these features for enhanced security, reliability, and performance:
### Production Checklist
- [ ] **Enable Image Signing** (Recommended)
- Provides cryptographic verification of your images
- Prevents tampering and ensures authenticity
- See "Optional: Enable Image Signing" section above for setup instructions
- Status: **Disabled by default** to allow immediate testing
- [ ] **Enable SBOM Attestation** (Recommended)
- Generates Software Bill of Materials for supply chain security
- Provides transparency about what's in your image
- Requires image signing to be enabled first
- To enable:
1. First complete image signing setup above
2. Edit `.github/workflows/build.yml`
3. Find the "OPTIONAL: SBOM Attestation" section around line 232
4. Uncomment the "Add SBOM Attestation" step
5. Commit and push
- Status: **Disabled by default** (requires signing first)
- [ ] **Enable Image Rechunking** (Recommended)
- Optimizes bootc image layers for better update performance
- Reduces update sizes by 5-10x
- Improves download resumability with evenly sized layers
- To enable:
1. Edit `.github/workflows/build.yml`
2. Find the "Build Image" step
3. Add a rechunk step after the build (see example below)
- Status: **Not enabled by default** (optional optimization)
#### Adding Image Rechunking
After building your bootc image, add a rechunk step before pushing to the registry. Here's an example based on the workflow used by [zirconium-dev/zirconium](https://github.com/zirconium-dev/zirconium):
```yaml
- name: Build image
id: build
run: sudo podman build -t "${IMAGE_NAME}:${DEFAULT_TAG}" -f ./Containerfile .
- name: Rechunk Image
run: |
sudo podman run --rm --privileged \
-v /var/lib/containers:/var/lib/containers \
--entrypoint /usr/libexec/bootc-base-imagectl \
"localhost/${IMAGE_NAME}:${DEFAULT_TAG}" \
rechunk --max-layers 96 \
"localhost/${IMAGE_NAME}:${DEFAULT_TAG}" \
"localhost/${IMAGE_NAME}:${DEFAULT_TAG}"
- name: Push to Registry
run: sudo podman push "localhost/${IMAGE_NAME}:${DEFAULT_TAG}" "${IMAGE_REGISTRY}/${IMAGE_NAME}:${DEFAULT_TAG}"
```
Alternative approach using a temporary tag for clarity:
```yaml
- name: Rechunk Image
run: |
sudo podman run --rm --privileged \
-v /var/lib/containers:/var/lib/containers \
--entrypoint /usr/libexec/bootc-base-imagectl \
"localhost/${IMAGE_NAME}:${DEFAULT_TAG}" \
rechunk --max-layers 67 \
"localhost/${IMAGE_NAME}:${DEFAULT_TAG}" \
"localhost/${IMAGE_NAME}:${DEFAULT_TAG}-rechunked"
# Tag the rechunked image with the original tag
sudo podman tag "localhost/${IMAGE_NAME}:${DEFAULT_TAG}-rechunked" "localhost/${IMAGE_NAME}:${DEFAULT_TAG}"
sudo podman rmi "localhost/${IMAGE_NAME}:${DEFAULT_TAG}-rechunked"
```
**Parameters:**
- `--max-layers`: Maximum number of layers for the rechunked image (typically 67 for optimal balance)
- The first image reference is the source (input)
- The second image reference is the destination (output)
- When using the same reference for both, the image is rechunked in-place
- You can also use different tags (e.g., `-rechunked` suffix) and then retag if preferred
**References:**
- [CoreOS rpm-ostree build-chunked-oci documentation](https://coreos.github.io/rpm-ostree/build-chunked-oci/)
- [bootc documentation](https://containers.github.io/bootc/)
### After Enabling Production Features
Your workflow will:
- Sign all images with your key
- Generate and attach SBOMs
- Provide full supply chain transparency
Users can verify your images with:
```bash
cosign verify --key cosign.pub ghcr.io/your-username/your-repo-name:stable
```
## Detailed Guides
- [Homebrew/Brewfiles](custom/brew/README.md) - Runtime package management
- [Flatpak Preinstall](custom/flatpaks/README.md) - GUI application setup
- [ujust Commands](custom/ujust/README.md) - User convenience commands
- [Build Scripts](build/README.md) - Build-time customization
## Architecture
This template follows the **multi-stage build architecture** from @projectbluefin/distroless, as documented in the [Bluefin Contributing Guide](https://docs.projectbluefin.io/contributing/).
### Multi-Stage Build Pattern
**Stage 1: Context (ctx)** - Combines resources from multiple sources:
- Local build scripts (`/build`)
- Local custom files (`/custom`)
- **@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
**Stage 2: Base Image** - Default options:
- `ghcr.io/ublue-os/silverblue-main:latest` (Fedora-based, default)
- `quay.io/centos-bootc/centos-bootc:stream10` (CentOS-based alternative)
### Benefits of This Architecture
- **Modularity**: Compose your image from reusable OCI containers
- **Maintainability**: Update shared components independently
- **Reproducibility**: Renovate automatically updates OCI tags to SHA digests
- **Consistency**: Share components across Bluefin, Aurora, and custom images
### OCI Container Resources
The template imports files from these OCI containers at build time:
```dockerfile
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/ublue-os/brew:latest /system_files /oci/brew
```
Your build scripts can access these files at:
- `/ctx/oci/base/` - Base system configuration
- `/ctx/oci/common/` - Shared desktop configuration
- `/ctx/oci/branding/` - Branding assets
- `/ctx/oci/artwork/` - Artwork files
- `/ctx/oci/brew/` - Homebrew integration files
**Note**: Renovate automatically updates `:latest` tags to SHA digests for reproducible builds.
## Local Testing
Test your changes before pushing:
```bash
just build # Build container image
just build-qcow2 # Build VM disk image
just run-vm-qcow2 # Test in browser-based VM
```
## Community
- [Universal Blue Discord](https://discord.gg/WEu6BdFEtp)
- [bootc Discussion](https://github.com/bootc-dev/bootc/discussions)
## Learn More
- [Universal Blue Documentation](https://universal-blue.org/)
- [bootc Documentation](https://containers.github.io/bootc/)
- [Video Tutorial by TesterTech](https://www.youtube.com/watch?v=IxBl11Zmq5wE)
## Security
This template provides security features for production use:
- Optional SBOM generation (Software Bill of Materials) for supply chain transparency
- Optional image signing with cosign for cryptographic verification
- Automated security updates via Renovate
- Build provenance tracking
These security features are disabled by default to allow immediate testing. When you're ready for production, see the "Love Your Image? Let's Go to Production" section above to enable them.
+8
View File
@@ -0,0 +1,8 @@
# This file is completely optional, but if you want to index your image on https://artifacthub.io/ you can
# Sign up and add the Repository ID to the right field. Owners fields are optional.
# Examples: https://artifacthub.io/packages/search?ts_query_web=ublue&sort=relevance&page=1
repositoryID: finpilot # Fill in with your own credentials
owners: # (optional, used to claim repository ownership)
- name: Jane Containerface
email: jane@blueuniversal.com
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/bash
set -eoux pipefail
###############################################################################
# Main Build Script
###############################################################################
# This script follows the @ublue-os/bluefin pattern for build scripts.
# It uses set -eoux pipefail for strict error handling and debugging.
###############################################################################
# Source helper functions
# shellcheck source=/dev/null
source /ctx/build/copr-helpers.sh
# Enable nullglob for all glob operations to prevent failures on empty matches
shopt -s nullglob
echo "::group:: Copy Bluefin Config from Common"
# Copy just files from @projectbluefin/common (includes 00-entry.just which imports 60-custom.just)
mkdir -p /usr/share/ublue-os/just/
shopt -s nullglob
cp -r /ctx/oci/common/bluefin/usr/share/ublue-os/just/* /usr/share/ublue-os/just/
shopt -u nullglob
echo "::endgroup::"
echo "::group:: Copy Custom Files"
# Copy Brewfiles to standard location
mkdir -p /usr/share/ublue-os/homebrew/
cp /ctx/custom/brew/*.Brewfile /usr/share/ublue-os/homebrew/
# Consolidate Just Files
find /ctx/custom/ujust -iname '*.just' -exec printf "\n\n" \; -exec cat {} \; >> /usr/share/ublue-os/just/60-custom.just
# Copy Flatpak preinstall files
mkdir -p /etc/flatpak/preinstall.d/
cp /ctx/custom/flatpaks/*.preinstall /etc/flatpak/preinstall.d/
echo "::endgroup::"
echo "::group:: Install Packages"
# Install packages using dnf5
# Example: dnf5 install -y tmux
# Example using COPR with isolated pattern:
# copr_install_isolated "ublue-os/staging" package-name
echo "::endgroup::"
echo "::group:: System Configuration"
# Enable/disable systemd services
systemctl enable podman.socket
# Example: systemctl mask unwanted-service
echo "::endgroup::"
# Restore default glob behavior
shopt -u nullglob
echo "Custom build complete!"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Tell build process to exit if there are any errors.
set -oue pipefail
###############################################################################
# Example: Installing 1Password and Google Chrome from Official Repositories
###############################################################################
# This is an EXAMPLE file showing how to add third-party RPM repositories
# and install packages from them following Universal Blue/Bluefin conventions.
#
# To use this script:
# 1. Rename this file to remove the .example extension: 20-onepassword.sh
# 2. The build system will automatically run scripts in numerical order
#
# IMPORTANT CONVENTIONS (from @ublue-os/bluefin):
# - Always clean up temporary repository files after installation
# - Use dnf5 exclusively (never dnf or yum)
# - Always use -y flag for non-interactive operations
# - Remove repo files to keep the image clean (repos don't work at runtime)
###############################################################################
### Install Google Chrome from Official Repository
echo "Installing Google Chrome..."
# Add Google Chrome RPM repository
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 Chrome
dnf5 install -y google-chrome-stable
# Clean up repo file (required - repos don't work at runtime in bootc images)
rm -f /etc/yum.repos.d/google-chrome.repo
echo "Google Chrome installed successfully"
### Install 1Password from Official Repository
echo "Installing 1Password..."
# Add 1Password RPM repository GPG key
rpm --import https://downloads.1password.com/linux/keys/1password.asc
# Add 1Password RPM repository
cat > /etc/yum.repos.d/1password.repo << 'EOF'
[1password]
name=1Password Stable Channel
baseurl=https://downloads.1password.com/linux/rpm/stable/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://downloads.1password.com/linux/keys/1password.asc
EOF
# Install 1Password
dnf5 install -y 1password
# Clean up repo file (required - repos don't work at runtime in bootc images)
rm -f /etc/yum.repos.d/1password.repo
echo "1Password installed successfully"
echo "Chrome and 1Password installation complete!"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/bash
set -eoux pipefail
###############################################################################
# Example: Swap GNOME Desktop with COSMIC Desktop
###############################################################################
# This example demonstrates replacing the GNOME desktop environment with
# System76's COSMIC desktop from their COPR repository.
#
# COSMIC is a new desktop environment built in Rust by System76.
# https://github.com/pop-os/cosmic-epoch
#
# To use this script:
# 1. Rename to remove .example extension: mv 30-cosmic-desktop.sh.example 30-cosmic-desktop.sh
# 2. Build - scripts run in numerical order automatically
#
# WARNING: This removes GNOME and replaces it with COSMIC. Only use this if
# you want COSMIC as your desktop environment instead of GNOME.
###############################################################################
# Source helper functions
# shellcheck source=/dev/null
source /ctx/build/copr-helpers.sh
echo "::group:: Remove GNOME Desktop"
# Remove GNOME Shell and related packages
dnf5 remove -y \
gnome-shell \
gnome-shell-extension* \
gnome-terminal \
gnome-software \
gnome-control-center \
nautilus \
gdm
echo "GNOME desktop removed"
echo "::endgroup::"
echo "::group:: Install COSMIC Desktop"
# Install COSMIC desktop from System76's COPR
# Using isolated pattern to prevent COPR from persisting
copr_install_isolated "ryanabx/cosmic-epoch" \
cosmic-session \
cosmic-greeter \
cosmic-comp \
cosmic-panel \
cosmic-launcher \
cosmic-applets \
cosmic-settings \
cosmic-files \
cosmic-edit \
cosmic-term \
cosmic-workspaces
echo "COSMIC desktop installed successfully"
echo "::endgroup::"
echo "::group:: Configure Display Manager"
# Enable cosmic-greeter (COSMIC's display manager)
systemctl enable cosmic-greeter
# Set COSMIC as default session
mkdir -p /etc/X11/sessions
cat > /etc/X11/sessions/cosmic.desktop << 'COSMICDESKTOP'
[Desktop Entry]
Name=COSMIC
Comment=COSMIC Desktop Environment
Exec=cosmic-session
Type=Application
DesktopNames=COSMIC
COSMICDESKTOP
echo "Display manager configured"
echo "::endgroup::"
echo "::group:: Install Additional Utilities"
# Install additional utilities that work well with COSMIC
dnf5 install -y \
kitty \
flatpak \
xdg-desktop-portal-cosmic
echo "Additional utilities installed"
echo "::endgroup::"
echo "COSMIC desktop installation complete!"
echo "After booting, select 'COSMIC' session at the login screen"
+77
View File
@@ -0,0 +1,77 @@
# Build Scripts
This directory contains build scripts that run during image creation. Scripts are executed in numerical order.
## How It Works
Scripts are named with a number prefix (e.g., `10-build.sh`, `20-onepassword.sh`) and run in ascending order during the container build process.
## Included Scripts
- **`10-build.sh`** - Main build script for base system modifications, package installation, and service configuration
## Example Scripts
- **`20-onepassword.sh.example`** - Example showing how to install software from third-party RPM repositories (Google Chrome, 1Password)
To use an example script:
1. Remove the `.example` extension
2. Make it executable: `chmod +x build/20-yourscript.sh`
3. The build system will automatically run it in numerical order
## Creating Your Own Scripts
Create numbered scripts for different purposes:
```bash
# 10-build.sh - Base system (already exists)
# 20-drivers.sh - Hardware drivers
# 30-development.sh - Development tools
# 40-gaming.sh - Gaming software
# 50-cleanup.sh - Final cleanup tasks
```
### Script Template
```bash
#!/usr/bin/env bash
set -oue pipefail
echo "Running custom setup..."
# Your commands here
```
### Best Practices
- **Use descriptive names**: `20-nvidia-drivers.sh` is better than `20-stuff.sh`
- **One purpose per script**: Easier to debug and maintain
- **Clean up after yourself**: Remove temporary files and disable temporary repos
- **Test incrementally**: Add one script at a time and test builds
- **Comment your code**: Future you will thank present you
### Disabling Scripts
To temporarily disable a script without deleting it:
- Rename it with `.disabled` extension: `20-script.sh.disabled`
- Or remove execute permission: `chmod -x build/20-script.sh`
## Execution Order
The Containerfile runs scripts like this:
```dockerfile
RUN /ctx/build/10-build.sh
```
If you want to run multiple scripts, you can:
1. **Modify Containerfile** to run each script explicitly
2. **Create a runner script** that executes all numbered scripts
3. **Use the default** and keep everything in `10-build.sh` (simplest)
## Notes
- Scripts run as root during build
- Build context is available at `/ctx`
- Use dnf5 for package management (not dnf or yum)
- Always use `-y` flag for non-interactive installs
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/bash
set -euo pipefail
###############################################################################
# COPR Helper Functions
###############################################################################
# These helper functions follow the @ublue-os/bluefin pattern for managing
# COPR repositories in a safe, isolated manner.
###############################################################################
copr_install_isolated() {
local copr_name="$1"
shift
local packages=("$@")
if [[ ${#packages[@]} -eq 0 ]]; then
echo "ERROR: No packages specified for copr_install_isolated"
return 1
fi
repo_id="copr:copr.fedorainfracloud.org:${copr_name//\//:}"
echo "Installing ${packages[*]} from COPR $copr_name (isolated)"
dnf5 -y copr enable "$copr_name"
dnf5 -y copr disable "$copr_name"
dnf5 -y install --enablerepo="$repo_id" "${packages[@]}"
echo "Installed ${packages[*]} from $copr_name"
}
+16
View File
@@ -0,0 +1,16 @@
-----BEGIN PUBLIC KEY-----
REPLACE THIS TEXT WITH YOUR ACTUAL PUBLIC KEY
Generated by running: cosign generate-key-pair
After generating, paste the contents of your cosign.pub file here
-----END PUBLIC KEY-----
Instructions:
1. Run: cosign generate-key-pair
2. This creates cosign.key (private) and cosign.pub (public)
3. Copy the entire contents of cosign.pub
4. Replace this entire file with that content
5. Commit and push this file
6. Add cosign.key contents to GitHub Secrets as SIGNING_SECRET
(See README.md for detailed instructions)
Never commit cosign.key - it must remain private!
+74
View File
@@ -0,0 +1,74 @@
# Homebrew Integration
This directory contains Brewfile declarations that will be copied into your custom image at `/usr/share/ublue-os/homebrew/`.
## What are Brewfiles?
Brewfiles are Homebrew's way of declaring packages in a declarative format. They allow you to specify which packages, taps, and casks you want installed.
## How It Works
1. **During Build**: Files in this directory are copied to `/usr/share/ublue-os/homebrew/` in the image
2. **After Installation**: Users install packages by running `brew bundle` commands
3. **User Experience**: Declarative package management via Homebrew
## Usage
### Adding Brewfiles to Your Image
1. Create `.Brewfile` files in this directory
2. Add your desired packages using Brewfile syntax
3. Build your image - the Brewfiles will be copied to `/usr/share/ublue-os/homebrew/`
**Example Files in this directory:**
- [`default.Brewfile`](default.Brewfile) - Essential command-line tools
- [`development.Brewfile`](development.Brewfile) - Development tools and languages
- [`fonts.Brewfile`](fonts.Brewfile) - Programming fonts
### Installing Packages from Brewfiles
After booting into your custom image, install packages with:
```bash
brew bundle --file /usr/share/ublue-os/homebrew/default.Brewfile
```
Or use the convenient ujust commands defined in [`custom/ujust/custom-apps.just`](../ujust/custom-apps.just):
```bash
ujust install-default-apps
ujust install-dev-tools
ujust install-fonts
```
## File Format
Brewfiles use Ruby syntax:
```ruby
# Add a tap (third-party repository)
tap "homebrew/cask"
# Install a formula (CLI tool)
brew "bat"
brew "eza"
brew "ripgrep"
# Install a cask (GUI application, macOS only)
cask "visual-studio-code"
```
## Customization
Edit the existing Brewfiles or create new ones:
- **[`default.Brewfile`](default.Brewfile)** - Modify for your essential tools
- **[`development.Brewfile`](development.Brewfile)** - Add your dev stack
- **[`fonts.Brewfile`](fonts.Brewfile)** - Add preferred fonts
- **Create new files** - `gaming.Brewfile`, `media.Brewfile`, etc.
When you add new Brewfiles, create corresponding ujust commands in [`custom/ujust/custom-apps.just`](../ujust/custom-apps.just) for easy installation.
## Resources
- [Homebrew Documentation](https://docs.brew.sh/)
- [Brewfile Documentation](https://github.com/Homebrew/homebrew-bundle)
- [Bluefin Homebrew Guide](https://docs.projectbluefin.io/administration#homebrew)
+22
View File
@@ -0,0 +1,22 @@
# Default Brewfile for bluepilot
# Add your favorite brew packages here
#
# Examples:
# Modern CLI tools
brew "bat" # cat with syntax highlighting
brew "eza" # Modern replacement for ls
brew "fd" # Simple, fast alternative to find
brew "rg" # ripgrep - faster grep
# Development tools
brew "gh" # GitHub CLI
brew "git" # Git version control
# Shell enhancements
brew "starship" # Cross-shell prompt
brew "zoxide" # Smarter cd command
# Utilities
brew "htop" # Interactive process viewer
brew "tmux" # Terminal multiplexer
+26
View File
@@ -0,0 +1,26 @@
# Brewfile for development tools
# Uncomment packages you want to install
# Container and orchestration tools
# brew "kind" # Kubernetes in Docker
# brew "kubectl" # Kubernetes CLI
# brew "k9s" # Kubernetes TUI
# brew "helm" # Kubernetes package manager
# Cloud tools
# brew "awscli" # AWS CLI
# brew "azure-cli" # Azure CLI
# Programming languages and tools
# brew "go" # Go programming language
# brew "node" # Node.js
# brew "python@3.12" # Python
# brew "rust" # Rust programming language
# Build tools
# brew "cmake" # Cross-platform build system
# brew "ninja" # Small build system
# Debugging and profiling
# brew "gdb" # GNU debugger
# brew "valgrind" # Memory debugging tool
+13
View File
@@ -0,0 +1,13 @@
# Brewfile for fonts
# Nerd Fonts with icons and glyphs for terminals and editors
# Popular coding fonts
cask "font-fira-code-nerd-font"
cask "font-jetbrains-mono-nerd-font"
cask "font-meslo-lg-nerd-font"
cask "font-hack-nerd-font"
# Additional font options (uncomment to install)
# cask "font-ubuntu-nerd-font"
# cask "font-source-code-pro"
# cask "font-cascadia-code-nerd-font"
+95
View File
@@ -0,0 +1,95 @@
# Flatpak Preinstall Integration
This directory contains Flatpak preinstall configuration files that will be copied into your custom image at `/etc/flatpak/preinstall.d/`.
## What is Flatpak Preinstall?
Flatpak preinstall is a feature that allows system administrators to define Flatpak applications that should be installed on first boot. These files are read by the Flatpak system integration and automatically install the specified applications.
## How It Works
1. **During Build**: Files in this directory are copied to `/etc/flatpak/preinstall.d/` in the image
2. **On First Boot**: After user setup completes, the system reads these files and installs the specified Flatpaks
3. **User Experience**: Applications appear automatically after first login
## Important: Installation Timing
**Flatpaks are NOT included in the ISO or container image.** They are downloaded and installed after:
- User completes initial system setup
- Network connection is established
- First boot process runs `flatpak preinstall`
This means:
- The ISO remains small and bootable offline
- Users need an internet connection after installation
- First boot may take longer while Flatpaks download and install
- This is NOT an offline ISO with pre-embedded applications
## File Format
Each file uses the INI format with `[Flatpak Preinstall NAME]` sections:
```ini
[Flatpak Preinstall org.mozilla.firefox]
Branch=stable
[Flatpak Preinstall org.gnome.Calculator]
Branch=stable
```
**Keys:**
- `Install` - (boolean) Whether to install (default: true)
- `Branch` - (string) Branch name (default: "master", commonly "stable")
- `IsRuntime` - (boolean) Whether this is a runtime (default: false for apps)
- `CollectionID` - (string) Collection ID of the remote, if any
See: https://docs.flatpak.org/en/latest/flatpak-command-reference.html#flatpak-preinstall
## Usage
### Adding Flatpaks to Your Image
1. Edit [`default.preinstall`](default.preinstall) or create new `.preinstall` files in this directory
2. Add Flatpak references in INI format with `[Flatpak Preinstall NAME]` sections
3. Build your image - the files will be copied to `/etc/flatpak/preinstall.d/`
4. After user setup completes, Flatpaks will be automatically installed
**Example Files in this directory:**
- [`default.preinstall`](default.preinstall) - Core applications from Bluefin
### Finding Flatpak IDs
To find the ID of a Flatpak:
```bash
flatpak search app-name
```
Or browse Flathub: https://flathub.org/
## Customization
Edit the existing file or create new ones:
- **[`default.preinstall`](default.preinstall)** - Modify the default application list
- **Create new files:**
- `development.preinstall` - Development tools
- `gaming.preinstall` - Gaming applications
- `media.preinstall` - Media editing tools
Each new `.preinstall` file will be automatically copied during the build process. See [`build/10-build.sh`](../../build/10-build.sh) for how files are copied.
## Important Notes
- Files must use the `.preinstall` extension
- Comments can be added with `#`
- Empty lines are ignored
- **Flatpaks are downloaded from Flathub on first boot** - not embedded in the image
- **Internet connection required** after installation for Flatpaks to install
- Installation happens automatically after user setup completes
- Users can still uninstall these applications if desired
- First boot will take longer while Flatpaks are being installed
## Resources
- [Flatpak Documentation](https://docs.flatpak.org/)
- [Flatpak Preinstall Reference](https://docs.flatpak.org/en/latest/flatpak-command-reference.html#flatpak-preinstall)
- [Flathub](https://flathub.org/)
+114
View File
@@ -0,0 +1,114 @@
# Default Flatpak applications
# These will be installed on first boot
# Format: INI file with [Flatpak Preinstall NAME] groups
# See: https://docs.flatpak.org/en/latest/flatpak-command-reference.html#flatpak-preinstall
# Web Browsers
[Flatpak Preinstall org.mozilla.firefox]
Branch=stable
[Flatpak Preinstall org.mozilla.Thunderbird]
Branch=stable
# GNOME Core Applications
[Flatpak Preinstall org.gnome.Calculator]
Branch=stable
[Flatpak Preinstall org.gnome.Calendar]
Branch=stable
[Flatpak Preinstall org.gnome.Characters]
Branch=stable
[Flatpak Preinstall org.gnome.Connections]
Branch=stable
[Flatpak Preinstall org.gnome.Contacts]
Branch=stable
[Flatpak Preinstall org.gnome.DejaDup]
Branch=stable
[Flatpak Preinstall org.gnome.FileRoller]
Branch=stable
[Flatpak Preinstall org.gnome.Firmware]
Branch=stable
[Flatpak Preinstall org.gnome.Logs]
Branch=stable
[Flatpak Preinstall org.gnome.Loupe]
Branch=stable
[Flatpak Preinstall org.gnome.Maps]
Branch=stable
[Flatpak Preinstall org.gnome.NautilusPreviewer]
Branch=stable
[Flatpak Preinstall org.gnome.Papers]
Branch=stable
[Flatpak Preinstall org.gnome.SimpleScan]
Branch=stable
[Flatpak Preinstall org.gnome.TextEditor]
Branch=stable
[Flatpak Preinstall org.gnome.Weather]
Branch=stable
[Flatpak Preinstall org.gnome.baobab]
Branch=stable
[Flatpak Preinstall org.gnome.clocks]
Branch=stable
[Flatpak Preinstall org.gnome.font-viewer]
Branch=stable
# Graphics & Media
[Flatpak Preinstall com.github.PintaProject.Pinta]
Branch=stable
[Flatpak Preinstall com.github.rafostar.Clapper]
Branch=stable
# System Utilities
[Flatpak Preinstall com.github.tchx84.Flatseal]
Branch=stable
[Flatpak Preinstall com.mattjakeman.ExtensionManager]
Branch=stable
[Flatpak Preinstall io.github.flattool.Warehouse]
Branch=stable
[Flatpak Preinstall io.github.flattool.Ignition]
Branch=stable
[Flatpak Preinstall io.missioncenter.MissionCenter]
Branch=stable
[Flatpak Preinstall io.gitlab.adhami3310.Impression]
Branch=stable
# Universal Blue Specific
[Flatpak Preinstall com.ranfdev.DistroShelf]
Branch=stable
[Flatpak Preinstall io.github.kolunmi.Bazaar]
Branch=stable
[Flatpak Preinstall page.tesk.Refine]
Branch=stable
# GTK Themes (runtimes)
[Flatpak Preinstall org.gtk.Gtk3theme.adw-gtk3]
Branch=3.22
IsRuntime=true
[Flatpak Preinstall org.gtk.Gtk3theme.adw-gtk3-dark]
Branch=3.22
IsRuntime=true
+240
View File
@@ -0,0 +1,240 @@
# ujust - User-facing Just Commands
This directory contains Just recipe files that will be installed into your custom image and made available to end users via the `ujust` command.
## What is ujust?
`ujust` is a command that allows users to run predefined tasks on their system. It's built on top of [just](https://github.com/casey/just), a command runner similar to `make` but designed for commands rather than builds.
## How It Works
1. **During Build**: All `.just` files in this directory are consolidated and copied to `/usr/share/ublue-os/just/60-custom.just` in the image
2. **After Installation**: Users run `ujust` to see available commands
3. **User Experience**: Simple command interface for system tasks
## File Structure
Create `.just` files in this directory with your custom commands:
```
custom/ujust/
├── README.md # This file
├── custom-apps.just # Application installation commands
└── custom-system.just # System configuration commands
```
**Example Files in this directory:**
- [`custom-apps.just`](custom-apps.just) - Application installation commands (Brewfiles, Flatpaks, JetBrains Toolbox)
- [`custom-system.just`](custom-system.just) - System configuration commands (benchmarks, dev groups, maintenance)
## Example Commands
### Basic Command
```just
# Run a system maintenance task
run-maintenance:
echo "Running maintenance..."
sudo systemctl restart some-service
```
### Interactive Command with gum
```just
# Configure system setting
configure-thing:
#!/usr/bin/bash
source /usr/lib/ujust/ujust.sh
echo "Configure thing?"
OPTION=$(Choose "Enable" "Disable")
if [[ "${OPTION,,}" =~ ^enable ]]; then
echo "Enabling..."
# your enable logic
else
echo "Disabling..."
# your disable logic
fi
```
### Command with Group
```just
# Groups organize commands in ujust help
[group('Apps')]
install-brewfile:
brew bundle --file /usr/share/ublue-os/homebrew/development.Brewfile
```
## Best Practices
### Naming Conventions
- Use lowercase with hyphens: `install-something`
- Use verb prefixes for clarity:
- `install-` - Install something
- `configure-` - Configure something pre-installed
- `setup-` - Install + configure
- `toggle-` - Enable/disable a feature
- `fix-` - Apply a fix or workaround
### Command Structure
```just
# Brief description of what the command does
[group('Category')]
command-name:
#!/usr/bin/bash
# Use bash shebang for multi-line scripts
# Commands go here
```
### Error Handling
```just
install-something:
#!/usr/bin/bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Your commands
```
### User Prompts
Use `gum` for interactive prompts (included in Universal Blue images):
```just
interactive-command:
#!/usr/bin/bash
source /usr/lib/ujust/ujust.sh # Provides Choose() and other helpers
OPTION=$(Choose "Option 1" "Option 2" "Cancel")
echo "You chose: $OPTION"
```
## Common Use Cases
### 1. Installing Software via Brewfiles
```just
[group('Apps')]
install-dev-tools:
brew bundle --file /usr/share/ublue-os/homebrew/development.Brewfile
```
**See examples in [`custom-apps.just`](custom-apps.just)** for Brewfile shortcuts.
### 2. System Configuration
```just
[group('System')]
configure-firewall:
#!/usr/bin/bash
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
```
**See examples in [`custom-system.just`](custom-system.just)** for system configuration.
### 3. Development Environment Setup
```just
[group('Development')]
setup-nodejs:
#!/usr/bin/bash
curl -fsSL https://fnm.vercel.app/install | bash
source ~/.bashrc
fnm install --lts
```
### 4. Maintenance Tasks
```just
[group('Maintenance')]
clean-containers:
podman system prune -af
podman volume prune -f
```
**See examples in [`custom-system.just`](custom-system.just)** for maintenance tasks.
## Important: Package Installation
**Do not install packages via dnf5/rpm in ujust commands.** Bootc images are immutable and package installation should happen at build time in [`build/10-build.sh`](../../build/10-build.sh).
For runtime package installation, use:
- **Brewfiles** - Create shortcuts to Brewfiles in [`custom/brew/`](../brew/)
- **Flatpak** - Install Flatpaks for GUI applications
- **Containers** - Use toolbox/distrobox for development environments
Example Brewfile shortcut (from [`custom-apps.just`](custom-apps.just)):
```just
[group('Apps')]
install-fonts:
brew bundle --file /usr/share/ublue-os/homebrew/fonts.Brewfile
```
## Available Helpers
Universal Blue images include helpers in `/usr/lib/ujust/ujust.sh`:
- `Choose()` - Present multiple choice menu
- `Confirm()` - Yes/no prompt
- Color variables: `${bold}`, `${normal}`, etc.
## Testing Your Commands
Test locally before committing:
1. Build your image: `just build` (see [`Justfile`](../../Justfile))
2. If on a bootc system: `sudo bootc switch --target localhost/finpilot:stable`
3. Reboot and test: `ujust your-command`
Or test the just files directly:
```bash
just --justfile custom/ujust/custom-apps.just --list
just --justfile custom/ujust/custom-apps.just install-something
```
## Customization
**Start by editing the example files:**
- **[`custom-apps.just`](custom-apps.just)** - Add your application installation commands
- **[`custom-system.just`](custom-system.just)** - Add your system configuration commands
**Create new files** for different categories:
- `custom-gaming.just` - Gaming-related commands
- `custom-media.just` - Media editing workflows
- `custom-dev.just` - Development environment setups
All `.just` files in this directory are automatically included. See [`build/10-build.sh`](../../build/10-build.sh) for the consolidation logic.
## Groups for Organization
Use groups to categorize commands:
```just
[group('Apps')]
install-app:
echo "Installing app..."
[group('System')]
configure-system:
echo "Configuring system..."
[group('Development')]
setup-dev:
echo "Setting up dev environment..."
```
## Examples from Bluefin
The included files provide starting examples:
- **[`custom-apps.just`](custom-apps.just)** - Application installation commands
- **[`custom-system.just`](custom-system.just)** - System configuration commands
These files show how to:
- Create shortcuts to Brewfiles in [`custom/brew/`](../brew/)
- Install Flatpaks interactively
- Configure system settings
- Run maintenance tasks
## Resources
- [Just Manual](https://just.systems/man/en/)
- [Universal Blue Just Documentation](https://universal-blue.org/guide/just/)
- [Bluefin ujust Commands](https://docs.projectbluefin.io/administration)
- [gum Documentation](https://github.com/charmbracelet/gum)
## Notes
- Commands run with user privileges by default
- Use `sudo` or `pkexec` when root access needed
- Consider providing both install and uninstall options
- Test on a clean system before distributing
- Document any prerequisites or dependencies
+80
View File
@@ -0,0 +1,80 @@
# vim: set ft=make :
####################
### custom-apps.just
####################
## Example application installation commands
## These are simplified examples adapted from Bluefin
# Install default applications via Homebrew
[group('Apps')]
install-default-apps:
#!/usr/bin/env bash
echo "Installing default applications via Homebrew..."
brew bundle --file /usr/share/ublue-os/homebrew/default.Brewfile
# Install development tools via Homebrew
[group('Apps')]
install-dev-tools:
#!/usr/bin/env bash
echo "Installing development tools via Homebrew..."
brew bundle --file /usr/share/ublue-os/homebrew/development.Brewfile
# Install fonts via Homebrew
[group('Apps')]
install-fonts:
#!/usr/bin/env bash
echo "Installing fonts via Homebrew..."
brew bundle --file /usr/share/ublue-os/homebrew/fonts.Brewfile
# Install all Brewfiles at once
[group('Apps')]
install-all-brew:
#!/usr/bin/env bash
echo "Installing all applications from Brewfiles..."
brew bundle --file /usr/share/ublue-os/homebrew/default.Brewfile
brew bundle --file /usr/share/ublue-os/homebrew/development.Brewfile
brew bundle --file /usr/share/ublue-os/homebrew/fonts.Brewfile
# Install JetBrains Toolbox for managing JetBrains IDEs
[group('Apps')]
install-jetbrains-toolbox:
#!/usr/bin/env bash
echo "Installing JetBrains Toolbox..."
pushd "$(mktemp -d)"
echo "Fetching latest version..."
curl -sSfL -o releases.json "https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release"
BUILD_VERSION=$(jq -r '.TBA[0].build' ./releases.json)
DOWNLOAD_LINK=$(jq -r '.TBA[0].downloads.linux.link' ./releases.json)
CHECKSUM_LINK=$(jq -r '.TBA[0].downloads.linux.checksumLink' ./releases.json)
echo "Installing JetBrains Toolbox ${BUILD_VERSION}"
curl -sSfL -O "${DOWNLOAD_LINK}"
curl -sSfL "${CHECKSUM_LINK}" | sha256sum -c
tar zxf jetbrains-toolbox-"${BUILD_VERSION}".tar.gz
mkdir -p $HOME/.local/share/JetBrains/ToolboxApp/
mv jetbrains-toolbox-"${BUILD_VERSION}"/* $HOME/.local/share/JetBrains/ToolboxApp/
popd
echo "Launching JetBrains Toolbox..."
$HOME/.local/share/JetBrains/ToolboxApp/bin/jetbrains-toolbox
# Shortcut for install-jetbrains-toolbox
[group('Apps')]
jetbrains-toolbox:
@ujust install-jetbrains-toolbox
# Install a Flatpak application from Flathub
[group('Apps')]
install-flatpak APP_ID:
#!/usr/bin/bash
echo "Installing {{ APP_ID }} from Flathub..."
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install -y flathub {{ APP_ID }}
# Example: Install VSCode via Flatpak
[group('Apps')]
install-vscode:
ujust install-flatpak com.visualstudio.code
# Example: Install GIMP via Flatpak
[group('Apps')]
install-gimp:
ujust install-flatpak org.gimp.GIMP
+92
View File
@@ -0,0 +1,92 @@
# vim: set ft=make :
#######################
### custom-system.just
#######################
## Example system configuration commands
## These are simplified examples adapted from Bluefin
# Run a system benchmark (requires stress-ng from Homebrew)
[group('System')]
benchmark:
#!/usr/bin/env bash
source /usr/lib/ujust/ujust.sh
if ! type -P "stress-ng" &>/dev/null ; then
echo "stress-ng is not installed."
if command -v brew &>/dev/null; then
if gum confirm "Install stress-ng via Homebrew?" ; then
brew install stress-ng
brew link stress-ng
else
exit 0
fi
else
echo "Please install stress-ng to run benchmarks."
exit 1
fi
fi
echo 'Running a 1 minute benchmark...'
pushd $(mktemp -d) > /dev/null
stress-ng --matrix 0 -t 1m --times
popd > /dev/null
# Configure docker and libvirt groups for development
[group('System')]
configure-dev-groups:
#!/usr/bin/pkexec bash
CURRENT_USER="{{ `id -un` }}"
echo "Adding $CURRENT_USER to docker and libvirt groups..."
# Ensure groups exist in /etc/group
for group in docker libvirt; do
if ! grep -q "^$group:" /etc/group; then
echo "Adding $group to /etc/group"
grep "^$group:" /usr/lib/group | tee -a /etc/group > /dev/null
fi
usermod -aG $group $CURRENT_USER
done
echo "Groups configured. Log out and back in for changes to take effect."
# Example toggle command with user choice
[group('System')]
toggle-example-feature:
#!/usr/bin/bash
source /usr/lib/ujust/ujust.sh
echo "This is an example toggle command."
echo "Current status: [check your status here]"
OPTION=$(Choose "Enable" "Disable" "Cancel")
case "$OPTION" in
"Enable")
echo "Enabling feature..."
# Add your enable logic here
;;
"Disable")
echo "Disabling feature..."
# Add your disable logic here
;;
"Cancel")
echo "No changes made."
;;
esac
# Clean up container images and volumes
[group('Maintenance')]
clean-containers:
#!/usr/bin/bash
echo "Cleaning up Podman containers, images, and volumes..."
podman system prune -af
podman volume prune -f
echo "Cleanup complete!"
# Update system and reboot if needed
[group('Maintenance')]
update-and-reboot:
#!/usr/bin/bash
source /usr/lib/ujust/ujust.sh
echo "Updating system..."
sudo bootc upgrade
if gum confirm "Reboot now to apply updates?"; then
systemctl reboot
else
echo "Reboot later to apply updates."
fi
+3
View File
@@ -0,0 +1,3 @@
[[customizations.filesystem]]
mountpoint = "/"
minsize = "20 GiB"
+31
View File
@@ -0,0 +1,31 @@
# Anaconda ISO Installer Configuration
# This configuration creates a bootable ISO image for installing your custom bootc image.
#
# The installer will guide users through disk partitioning and basic system setup,
# then automatically switch to your custom image on first boot.
[customizations.installer.kickstart]
contents = """
%post
# After installation, switch to the custom image from this repository
# IMPORTANT: Update this URL to match your repository (ghcr.io/USERNAME/REPO:stable)
bootc switch --mutate-in-place --transport registry ghcr.io/USERNAME/REPO:stable
%end
"""
[customizations.installer.modules]
# Enable standard Anaconda installer modules for a guided installation experience
enable = [
"org.fedoraproject.Anaconda.Modules.Storage", # Disk partitioning and formatting
"org.fedoraproject.Anaconda.Modules.Runtime", # Basic runtime configuration
"org.fedoraproject.Anaconda.Modules.Network", # Network configuration
"org.fedoraproject.Anaconda.Modules.Security", # Security settings
"org.fedoraproject.Anaconda.Modules.Services", # System services
"org.fedoraproject.Anaconda.Modules.Users", # User account creation
"org.fedoraproject.Anaconda.Modules.Timezone" # Timezone selection
]
# Disable subscription management (not needed for community images)
disable = [
"org.fedoraproject.Anaconda.Modules.Subscription",
]
+128
View File
@@ -0,0 +1,128 @@
# Rclone Configuration Directory
This directory contains example rclone configuration files for uploading ISO images to various cloud storage providers.
## Available Configurations
- **cloudflare-r2.conf** - Cloudflare R2 (S3-compatible, zero egress fees)
- **aws-s3.conf** - Amazon S3 (highly reliable, standard pricing)
- **backblaze-b2.conf** - Backblaze B2 (affordable, low egress fees)
- **sftp.conf** - SFTP/SSH upload to any server
- **scp.conf** - SCP upload to any server
## How to Use
### 1. Choose Your Storage Provider
Select the configuration file that matches your preferred storage provider. Each file contains:
- Setup instructions
- Required GitHub secrets
- Provider-specific configuration options
### 2. Set Up GitHub Secrets
For the ISO build workflow to upload files, you need to configure GitHub secrets:
1. Go to your repository on GitHub
2. Navigate to **Settings****Secrets and variables****Actions**
3. Click **New repository secret**
4. Add the secrets required by your chosen provider (listed in the config file)
### 3. Choose Your Upload Method
The build-disk.yml workflow supports two upload methods:
#### Method A: Using rclone configs (Recommended)
This method uses the configuration files in this directory:
```yaml
# In .github/workflows/build-disk.yml, the workflow will:
# 1. Read the config from this directory
# 2. Substitute secrets automatically
# 3. Upload using rclone
```
To use this method, specify which config to use when triggering the workflow.
#### Method B: Direct environment variables (Legacy)
The workflow also supports direct environment variable configuration for backward compatibility.
### 4. Triggering the Workflow
The `build-disk.yml` workflow is triggered manually:
1. Go to **Actions** tab in your repository
2. Select **Build disk images** workflow
3. Click **Run workflow**
4. Select the platform (amd64 or arm64)
5. Enable **Upload to cloud storage** if you want to upload the ISO
## Configuration File Format
All configuration files use the rclone INI format with placeholders for secrets:
```ini
[remote-name]
type = provider_type
access_key_id = ${SECRET_NAME}
secret_access_key = ${ANOTHER_SECRET}
```
The workflow automatically replaces `${SECRET_NAME}` with the corresponding GitHub secret value.
## Customizing Configurations
You can modify these configuration files to suit your needs:
1. Edit the configuration file for your provider
2. Update the secret names in `${...}` placeholders
3. Add the corresponding secrets to your GitHub repository
4. Commit the changes
**Important:** Never commit actual credentials or secrets to the repository. Always use `${SECRET_NAME}` placeholders and GitHub secrets.
## Provider Comparison
| Provider | Setup Complexity | Cost | Egress Fees | Notes |
|----------|------------------|------|-------------|-------|
| Cloudflare R2 | Medium | $ | Free | Best for frequent downloads |
| AWS S3 | Medium | $$ | $$ | Most reliable, global reach |
| Backblaze B2 | Easy | $ | $ | Good balance of price/features |
| SFTP/SCP | Medium | Free* | Free | Requires your own server |
*Requires existing server infrastructure
## Troubleshooting
### "Permission denied" errors
- Check that your access keys are correct
- Verify IAM permissions (for AWS)
- Ensure the bucket exists and is accessible
### "Endpoint not found" errors
- Verify the endpoint URL is correct
- Check region settings
- For Cloudflare R2, ensure you're using the correct Account ID
### Upload fails silently
- Enable workflow debug logging in GitHub Actions
- Check that secrets are properly set
- Verify the rclone config syntax
## Additional Resources
- [Rclone Documentation](https://rclone.org/docs/)
- [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets)
- [Cloudflare R2 Documentation](https://developers.cloudflare.com/r2/)
- [AWS S3 Documentation](https://docs.aws.amazon.com/s3/)
- [Backblaze B2 Documentation](https://www.backblaze.com/b2/docs/)
## Need Help?
If you encounter issues:
1. Check the workflow logs in the Actions tab
2. Review the rclone documentation for your provider
3. Ask in the [Universal Blue Discord](https://discord.gg/WEu6BdFEtp)
4. Post in the [Universal Blue Forums](https://universal-blue.discourse.group/)
+29
View File
@@ -0,0 +1,29 @@
# AWS S3 Configuration Example
#
# Amazon S3 is a highly reliable object storage service.
#
# Setup Instructions:
# 1. Create an AWS account
# 2. Create an S3 bucket in your desired region
# 3. Create an IAM user with S3 access permissions
# 4. Generate access keys for the IAM user
# 5. Set the following GitHub secrets in your repository:
# - AWS_ACCESS_KEY_ID: Your AWS Access Key ID
# - AWS_SECRET_ACCESS_KEY: Your AWS Secret Access Key
# - AWS_REGION: Your bucket region (e.g., us-east-1, eu-west-1)
# - AWS_BUCKET_NAME: Your S3 bucket name
#
# Note: S3 charges for both storage and egress bandwidth
[aws-s3]
type = s3
provider = AWS
# Access credentials - use GitHub secrets
access_key_id = ${AWS_ACCESS_KEY_ID}
secret_access_key = ${AWS_SECRET_ACCESS_KEY}
# Your bucket's region
region = ${AWS_REGION}
# ACL settings
acl = private
# Optional: Enable server-side encryption
# server_side_encryption = AES256
+24
View File
@@ -0,0 +1,24 @@
# Backblaze B2 Configuration Example
#
# Backblaze B2 is affordable cloud storage with low egress fees.
#
# Setup Instructions:
# 1. Create a Backblaze account
# 2. Create a B2 bucket in the Backblaze dashboard
# 3. Create an Application Key with read/write access to your bucket
# 4. Note your Application Key ID and Application Key
# 5. Set the following GitHub secrets in your repository:
# - B2_ACCOUNT_ID: Your Backblaze Account ID (also called Application Key ID)
# - B2_APPLICATION_KEY: Your Backblaze Application Key
# - B2_BUCKET_NAME: Your B2 bucket name
#
# Note: Backblaze charges for storage and egress over the free tier (1GB/day)
[backblaze-b2]
type = b2
# Account credentials - use GitHub secrets
account = ${B2_ACCOUNT_ID}
key = ${B2_APPLICATION_KEY}
# Optional: Specify the bucket directly in the remote name
# This allows using the remote without specifying bucket in commands
# endpoint =
+29
View File
@@ -0,0 +1,29 @@
# Cloudflare R2 Configuration Example
#
# Cloudflare R2 is S3-compatible storage with zero egress fees.
#
# Setup Instructions:
# 1. Create a Cloudflare account and enable R2
# 2. Create an R2 bucket in the Cloudflare dashboard
# 3. Generate API tokens from R2 > Manage R2 API Tokens
# 4. Copy your Account ID from the R2 dashboard
# 5. Set the following GitHub secrets in your repository:
# - R2_ACCOUNT_ID: Your Cloudflare Account ID
# - R2_ACCESS_KEY_ID: Your R2 Access Key ID
# - R2_SECRET_ACCESS_KEY: Your R2 Secret Access Key
# - R2_BUCKET_NAME: Your R2 bucket name
#
# The endpoint format is: https://<account-id>.r2.cloudflarestorage.com
[cloudflare-r2]
type = s3
provider = Cloudflare
# Access credentials - use GitHub secrets
access_key_id = ${R2_ACCESS_KEY_ID}
secret_access_key = ${R2_SECRET_ACCESS_KEY}
# Endpoint format
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
# Region is not used for R2 but some tools require it
region = auto
# ACL settings
acl = private
+29
View File
@@ -0,0 +1,29 @@
# SCP Configuration Example
#
# Upload ISO files using SCP (SSH Copy Protocol).
# This is similar to SFTP but uses the SCP protocol directly.
#
# Setup Instructions:
# 1. Ensure you have SSH access to your target server
# 2. Generate an SSH key pair if you haven't already
# 3. Add the public key to your server's ~/.ssh/authorized_keys
# 4. Set the following GitHub secrets in your repository:
# - SCP_HOST: Your server hostname or IP address
# - SCP_USER: Your SSH username
# - SCP_SSH_KEY: Your private SSH key (entire contents)
# - SCP_PORT: SSH port (default: 22)
# - SCP_PATH: Remote path to upload files (e.g., /var/www/isos)
#
# Note: SCP is simpler but less feature-rich than SFTP
[scp]
type = sftp
# Server details - use GitHub secrets
host = ${SCP_HOST}
user = ${SCP_USER}
port = ${SCP_PORT:-22}
# Authentication - use key-based auth
key_pem = ${SCP_SSH_KEY}
# SCP-specific settings
# Use SCP protocol instead of SFTP
use_scp = true
+30
View File
@@ -0,0 +1,30 @@
# SFTP Configuration Example
#
# Upload ISO files to any server with SSH/SFTP access.
#
# Setup Instructions:
# 1. Ensure you have SSH access to your target server
# 2. Generate an SSH key pair if you haven't already
# 3. Add the public key to your server's ~/.ssh/authorized_keys
# 4. Set the following GitHub secrets in your repository:
# - SFTP_HOST: Your server hostname or IP address
# - SFTP_USER: Your SSH username
# - SFTP_SSH_KEY: Your private SSH key (entire contents)
# - SFTP_PORT: SSH port (default: 22)
# - SFTP_PATH: Remote path to upload files (e.g., /var/www/isos)
#
# Note: For password authentication, use SFTP_PASSWORD instead of SFTP_SSH_KEY
[sftp]
type = sftp
# Server details - use GitHub secrets
host = ${SFTP_HOST}
user = ${SFTP_USER}
port = ${SFTP_PORT:-22}
# Authentication - use key-based auth (recommended)
key_pem = ${SFTP_SSH_KEY}
# Or use password authentication (less secure)
# pass = ${SFTP_PASSWORD}
# Optional: Skip host key verification (not recommended for production)
# skip_links = false
# set_modtime = true