Initial commit: Talos CM5 builder with Gitea CI/CD

Custom Talos Linux image builder for Raspberry Pi CM5 on Compute Blade
hardware. Uses RPi downstream kernel (via talos-rpi5/talos-builder patches)
since the mainline kernel lacks CM5 device trees and RP1 driver support.

- Makefile: build orchestration targeting docker.io/svrnty registry
- Build pipeline: tag-triggered Gitea Actions workflow
- Update checker: weekly cron for Talos + RPi kernel releases
- CM5 overclock config: 2.6GHz (arm_freq=2600)
- Extensions: iscsi-tools, util-linux-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mathias Beaulieu-Duncan 2026-02-09 17:58:17 -05:00
commit e31cd9add8
8 changed files with 540 additions and 0 deletions

103
.gitea/workflows/build.yaml Normal file
View File

@ -0,0 +1,103 @@
# Build and release custom Talos CM5 image
#
# Triggered by pushing a version tag (e.g. v1.11.5-1)
# Runs on ARM64 self-hosted runner (ASUS GX10)
#
# Produces:
# - Installer container image → Docker Hub (svrnty/installer:<tag>)
# - Raw disk image → Gitea release (metal-arm64.raw.zst)
name: Build Talos CM5 Image
on:
push:
tags:
- 'v*.*.*'
jobs:
build:
runs-on: [self-hosted, linux, arm64]
timeout-minutes: 180
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version tag
id: version
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Clone upstream sources
run: make checkouts
- name: Apply patches
run: make patches
- name: Build kernel
run: make kernel
- name: Build SBC overlay
run: make overlay
- name: Build installer and disk image
run: make installer
- name: Tag release images
run: make release TAG=${{ steps.version.outputs.tag }}
- name: Compress disk image
run: |
# The imager outputs to checkouts/talos/_out/
DISK_IMAGE=$(find checkouts/talos/_out -name 'metal-arm64*.raw*' | head -1)
if [ -z "$DISK_IMAGE" ]; then
echo "Error: disk image not found in checkouts/talos/_out/"
find checkouts/talos/_out -type f
exit 1
fi
# Copy to workspace root for release upload
cp "$DISK_IMAGE" metal-arm64.raw.zst
ls -lh metal-arm64.raw.zst
- name: Create Gitea release
uses: actions/forgejo-release@v2
with:
direction: upload
tag: ${{ steps.version.outputs.tag }}
title: "Talos CM5 ${{ steps.version.outputs.tag }}"
release-notes: |
Custom Talos Linux image for Raspberry Pi CM5 (Compute Blade)
**Talos version**: check Makefile
**Kernel**: RPi downstream (CM5/RP1 support)
**Extensions**: iscsi-tools, util-linux-tools
**Overclock**: 2.6GHz (arm_freq=2600)
## Artifacts
- `metal-arm64.raw.zst` — Raw disk image for eMMC flashing
- `docker.io/svrnty/installer:${{ steps.version.outputs.tag }}` — Installer image for `talosctl upgrade`
## Usage
```bash
# Flash to eMMC
./scripts/flash-emmc.sh metal-arm64.raw.zst
# Upgrade existing node
talosctl upgrade --image docker.io/svrnty/installer:${{ steps.version.outputs.tag }}
```
release-dir: .
release-notes-assistant: none
env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Clean up
if: always()
run: make clean

View File

@ -0,0 +1,121 @@
# Check for upstream Talos and RPi kernel updates
#
# Runs on a schedule and creates a Gitea issue when new versions are found.
# This is notification-only — builds require manual tag push after verifying
# patches still apply.
name: Check Upstream Updates
on:
schedule:
# Run weekly on Monday at 08:00 UTC
- cron: '0 8 * * 1'
workflow_dispatch:
jobs:
check-updates:
runs-on: [self-hosted, linux, arm64]
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check for upstream updates
id: check
run: |
chmod +x scripts/check-upstream.sh
scripts/check-upstream.sh >> "$GITHUB_OUTPUT"
- name: Create issue for Talos update
if: steps.check.outputs.talos_update == 'true'
uses: actions/github-script@v7
with:
script: |
const currentVersion = '${{ steps.check.outputs.talos_current }}';
const latestVersion = '${{ steps.check.outputs.talos_latest }}';
const title = `Talos update available: ${currentVersion} → ${latestVersion}`;
// Check if an open issue already exists
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'upstream-update',
});
const existing = issues.data.find(i => i.title.includes('Talos update'));
if (existing) {
console.log(`Issue already exists: #${existing.number}`);
return;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: [
`## Talos Update Available`,
``,
`| | Version |`,
`|---|---|`,
`| Current | \`${currentVersion}\` |`,
`| Latest | \`${latestVersion}\` |`,
``,
`### Steps`,
`1. Update \`TALOS_VERSION\` in \`Makefile\``,
`2. Verify patches still apply: \`make checkouts patches\``,
`3. If patches fail, port them to the new version`,
`4. Push a version tag to trigger the build pipeline`,
``,
`### Links`,
`- [Talos Release Notes](https://github.com/siderolabs/talos/releases/tag/${latestVersion})`,
].join('\n'),
labels: ['upstream-update', 'talos'],
});
- name: Create issue for RPi kernel update
if: steps.check.outputs.rpi_update == 'true'
uses: actions/github-script@v7
with:
script: |
const currentVersion = '${{ steps.check.outputs.rpi_current }}';
const latestVersion = '${{ steps.check.outputs.rpi_latest }}';
const title = `RPi kernel update available: ${currentVersion} → ${latestVersion}`;
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'upstream-update',
});
const existing = issues.data.find(i => i.title.includes('RPi kernel update'));
if (existing) {
console.log(`Issue already exists: #${existing.number}`);
return;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: [
`## RPi Kernel Update Available`,
``,
`| | Version |`,
`|---|---|`,
`| Current (in pkgs patch) | \`${currentVersion}\` |`,
`| Latest stable | \`${latestVersion}\` |`,
``,
`### Steps`,
`1. Update the kernel version in the pkgs patch`,
`2. Verify the patch still applies: \`make checkouts patches\``,
`3. Test build: \`make kernel\``,
`4. Push a version tag to trigger the full build pipeline`,
``,
`### Links`,
`- [RPi Linux Releases](https://github.com/raspberrypi/linux/tags)`,
].join('\n'),
labels: ['upstream-update', 'kernel'],
});

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
checkouts/
_out/
*.raw
*.raw.zst

150
Makefile Normal file
View File

@ -0,0 +1,150 @@
# Talos CM5 Builder — Custom Talos Linux images for Raspberry Pi CM5
#
# Forked from https://github.com/talos-rpi5/talos-builder
# Builds Talos with the RPi downstream kernel (CM5/RP1 support)
#
# Usage:
# make checkouts patches # Clone and patch upstream sources
# make kernel # Build RPi kernel (~15-30 min on ARM64)
# make overlay # Build U-Boot + firmware + DTBs
# make installer # Build installer image + raw disk image
# make release # Tag images for release
PKG_VERSION = v1.11.0
TALOS_VERSION = v1.11.5
SBCOVERLAY_VERSION = main
REGISTRY ?= docker.io
REGISTRY_USERNAME ?= svrnty
TAG ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
# System extensions baked into the image
EXTENSIONS ?= ghcr.io/siderolabs/iscsi-tools:v0.1.6 ghcr.io/siderolabs/util-linux-tools:2.40.4
# Upstream repositories
PKG_REPOSITORY = https://github.com/siderolabs/pkgs.git
TALOS_REPOSITORY = https://github.com/siderolabs/talos.git
SBCOVERLAY_REPOSITORY = https://github.com/talos-rpi5/sbc-raspberrypi5.git
CHECKOUTS_DIRECTORY := $(PWD)/checkouts
PATCHES_DIRECTORY := $(PWD)/patches
PKGS_TAG = $(shell cd $(CHECKOUTS_DIRECTORY)/pkgs && git describe --tag --always --dirty --match v[0-9]\*)
TALOS_TAG = $(shell cd $(CHECKOUTS_DIRECTORY)/talos && git describe --tag --always --dirty --match v[0-9]\*)
SBCOVERLAY_TAG = $(shell cd $(CHECKOUTS_DIRECTORY)/sbc-raspberrypi5 && git describe --tag --always --dirty)-$(PKGS_TAG)
# Build the --system-extension-image flags from the EXTENSIONS list
EXTENSION_FLAGS = $(foreach ext,$(EXTENSIONS),--system-extension-image=$(ext))
#
# Help
#
.PHONY: help
help:
@echo "Talos CM5 Builder"
@echo ""
@echo "Targets:"
@echo " checkouts — Clone upstream repositories"
@echo " patches — Apply RPi kernel + CM5 patches"
@echo " kernel — Build RPi downstream kernel"
@echo " overlay — Build SBC overlay (U-Boot, firmware, DTBs)"
@echo " installer — Build Talos installer image + raw disk image"
@echo " release — Tag and push release images"
@echo " clean — Remove checkouts and build artifacts"
@echo ""
@echo "Variables:"
@echo " TALOS_VERSION = $(TALOS_VERSION)"
@echo " PKG_VERSION = $(PKG_VERSION)"
@echo " REGISTRY = $(REGISTRY)"
@echo " REGISTRY_USERNAME = $(REGISTRY_USERNAME)"
#
# Checkouts
#
.PHONY: checkouts checkouts-clean
checkouts:
git clone -c advice.detachedHead=false --branch "$(PKG_VERSION)" "$(PKG_REPOSITORY)" "$(CHECKOUTS_DIRECTORY)/pkgs"
git clone -c advice.detachedHead=false --branch "$(TALOS_VERSION)" "$(TALOS_REPOSITORY)" "$(CHECKOUTS_DIRECTORY)/talos"
git clone -c advice.detachedHead=false --branch "$(SBCOVERLAY_VERSION)" "$(SBCOVERLAY_REPOSITORY)" "$(CHECKOUTS_DIRECTORY)/sbc-raspberrypi5"
checkouts-clean:
rm -rf "$(CHECKOUTS_DIRECTORY)/pkgs"
rm -rf "$(CHECKOUTS_DIRECTORY)/talos"
rm -rf "$(CHECKOUTS_DIRECTORY)/sbc-raspberrypi5"
#
# Patches
#
.PHONY: patches-pkgs patches-talos patches
patches-pkgs:
cd "$(CHECKOUTS_DIRECTORY)/pkgs" && \
git am "$(PATCHES_DIRECTORY)/siderolabs/pkgs/"*.patch
patches-talos:
cd "$(CHECKOUTS_DIRECTORY)/talos" && \
git am "$(PATCHES_DIRECTORY)/siderolabs/talos/"*.patch
patches: patches-pkgs patches-talos
#
# Kernel
#
.PHONY: kernel
kernel:
cd "$(CHECKOUTS_DIRECTORY)/pkgs" && \
$(MAKE) \
REGISTRY=$(REGISTRY) USERNAME=$(REGISTRY_USERNAME) PUSH=true \
PLATFORM=linux/arm64 \
kernel
#
# Overlay
#
.PHONY: overlay
overlay:
@echo "SBCOVERLAY_TAG = $(SBCOVERLAY_TAG)"
cd "$(CHECKOUTS_DIRECTORY)/sbc-raspberrypi5" && \
$(MAKE) \
REGISTRY=$(REGISTRY) USERNAME=$(REGISTRY_USERNAME) IMAGE_TAG=$(SBCOVERLAY_TAG) PUSH=true \
PKGS_PREFIX=$(REGISTRY)/$(REGISTRY_USERNAME) PKGS=$(PKGS_TAG) \
INSTALLER_ARCH=arm64 PLATFORM=linux/arm64 \
sbc-raspberrypi5
#
# Installer / Disk Image
#
.PHONY: installer
installer:
cd "$(CHECKOUTS_DIRECTORY)/talos" && \
$(MAKE) \
REGISTRY=$(REGISTRY) USERNAME=$(REGISTRY_USERNAME) PUSH=true \
PKG_KERNEL=$(REGISTRY)/$(REGISTRY_USERNAME)/kernel:$(PKGS_TAG) \
INSTALLER_ARCH=arm64 PLATFORM=linux/arm64 \
IMAGER_ARGS="--overlay-name=rpi5 --overlay-image=$(REGISTRY)/$(REGISTRY_USERNAME)/sbc-raspberrypi5:$(SBCOVERLAY_TAG) $(EXTENSION_FLAGS)" \
kernel initramfs imager installer-base installer && \
docker \
run --rm -t -v ./_out:/out -v /dev:/dev --privileged \
$(REGISTRY)/$(REGISTRY_USERNAME)/imager:$(TALOS_TAG) \
metal --arch arm64 \
--base-installer-image="$(REGISTRY)/$(REGISTRY_USERNAME)/installer:$(TALOS_TAG)" \
--overlay-name="rpi5" \
--overlay-image="$(REGISTRY)/$(REGISTRY_USERNAME)/sbc-raspberrypi5:$(SBCOVERLAY_TAG)" \
--overlay-option="configTxtAppend=$$(cat $(PWD)/config/config.txt.append)" \
$(EXTENSION_FLAGS)
#
# Release — tag images with the Git tag for stable references
#
.PHONY: release
release:
docker pull $(REGISTRY)/$(REGISTRY_USERNAME)/installer:$(TALOS_TAG) && \
docker tag $(REGISTRY)/$(REGISTRY_USERNAME)/installer:$(TALOS_TAG) $(REGISTRY)/$(REGISTRY_USERNAME)/installer:$(TAG) && \
docker push $(REGISTRY)/$(REGISTRY_USERNAME)/installer:$(TAG)
#
# Clean
#
.PHONY: clean
clean: checkouts-clean
rm -rf checkouts/_out

89
README.md Normal file
View File

@ -0,0 +1,89 @@
# Talos CM5 Builder
Custom Talos Linux images for Raspberry Pi CM5 on Compute Blade hardware.
The official Talos Image Factory does not support CM5 — the mainline kernel lacks CM5 device trees and RP1 driver support. This builder uses the RPi downstream kernel (via [talos-rpi5/talos-builder](https://github.com/talos-rpi5/talos-builder) patches) to produce working CM5 images with our extensions and overclock config.
## What it builds
- **Installer image**`docker.io/svrnty/installer:<tag>` (for `talosctl upgrade`)
- **Raw disk image** → Gitea release `metal-arm64.raw.zst` (for eMMC flashing)
Baked-in config:
- RPi downstream kernel with CM5/RP1 support
- Overclock: 2.6GHz (`arm_freq=2600`, `over_voltage_delta=50000`, `arm_boost=1`)
- Extensions: `iscsi-tools`, `util-linux-tools`
## Usage
### Building locally (ARM64 host required)
```bash
make checkouts patches # Clone and patch sources
make kernel # Build RPi kernel
make overlay # Build SBC overlay
make installer # Build installer + disk image
```
### CI/CD (Gitea Actions)
Push a version tag to trigger an automated build:
```bash
git tag v1.11.5-1
git push origin v1.11.5-1
```
The pipeline runs on the ARM64 self-hosted runner and:
1. Builds the kernel, overlay, and installer
2. Pushes the installer image to Docker Hub
3. Creates a Gitea release with the raw disk image
### Upstream update checks
A weekly scheduled workflow checks for new Talos and RPi kernel releases and creates Gitea issues when updates are available.
## CI Secrets
| Secret | Description |
|--------|-------------|
| `DOCKERHUB_USERNAME` | Docker Hub username |
| `DOCKERHUB_TOKEN` | Docker Hub access token |
| `GITEA_TOKEN` | Gitea API token (for creating releases and issues) |
## Runner Setup (ASUS GX10)
The ARM64 build runner needs:
- Docker + Docker Buildx
- Gitea `act_runner` registered with labels: `self-hosted`, `linux`, `arm64`
- Sufficient disk space for kernel builds (~20GB)
```bash
# Install act_runner
curl -sL https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-arm64 -o act_runner
chmod +x act_runner
# Register
./act_runner register --instance <gitea-url> --token <runner-token>
# Run as service
./act_runner daemon
```
## Structure
```
.gitea/workflows/
build.yaml # Build pipeline (tag push trigger)
check-updates.yaml # Upstream update checker (weekly cron)
Makefile # Build orchestration
config/
config.txt.append # CM5 overclock settings
extensions.yaml # System extensions list
scripts/
check-upstream.sh # Version comparison script
patches/
siderolabs/
pkgs/0001-*.patch # RPi kernel patch
talos/0001-*.patch # Module list patch
```

4
config/config.txt.append Normal file
View File

@ -0,0 +1,4 @@
# CM5 Overclock — 2.6GHz stable on Compute Blade with heatsink
arm_freq=2600
over_voltage_delta=50000
arm_boost=1

15
config/extensions.yaml Normal file
View File

@ -0,0 +1,15 @@
# System extensions included in the Talos CM5 image
#
# These are passed to the imager via --system-extension-image flags.
# Update the EXTENSIONS variable in Makefile when changing this list.
#
# Available extensions: https://github.com/siderolabs/extensions
extensions:
# iSCSI initiator for shared storage (Longhorn, democratic-csi)
- name: iscsi-tools
image: ghcr.io/siderolabs/iscsi-tools:v0.1.6
# util-linux tools (lsblk, etc.) for storage debugging
- name: util-linux-tools
image: ghcr.io/siderolabs/util-linux-tools:2.40.4

54
scripts/check-upstream.sh Executable file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Check for upstream Talos and RPi kernel updates
#
# Compares current versions in Makefile against the latest GitHub releases.
# Outputs GitHub Actions-compatible variables for use in CI workflows.
#
# Usage:
# ./scripts/check-upstream.sh # Print results
# ./scripts/check-upstream.sh >> "$GITHUB_OUTPUT" # For CI
set -euo pipefail
MAKEFILE="${MAKEFILE:-Makefile}"
# Extract current versions from Makefile
CURRENT_TALOS=$(grep '^TALOS_VERSION' "$MAKEFILE" | head -1 | awk '{print $NF}')
CURRENT_PKG=$(grep '^PKG_VERSION' "$MAKEFILE" | head -1 | awk '{print $NF}')
echo "Current Talos version: $CURRENT_TALOS"
echo "Current PKG version: $CURRENT_PKG"
# Check latest Talos stable release
LATEST_TALOS=$(curl -sf "https://api.github.com/repos/siderolabs/talos/releases/latest" \
| grep '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
echo "Latest Talos release: $LATEST_TALOS"
# Check latest RPi kernel stable tag (format: stable_YYYYMMDD)
LATEST_RPI_KERNEL=$(curl -sf "https://api.github.com/repos/raspberrypi/linux/tags?per_page=10" \
| grep '"name"' | grep 'stable_' | head -1 | sed -E 's/.*"name": *"([^"]+)".*/\1/')
echo "Latest RPi kernel tag: $LATEST_RPI_KERNEL"
# Output for GitHub Actions
echo "talos_current=$CURRENT_TALOS"
echo "talos_latest=$LATEST_TALOS"
if [ "$CURRENT_TALOS" != "$LATEST_TALOS" ]; then
echo "talos_update=true"
echo ">> Talos update available: $CURRENT_TALOS -> $LATEST_TALOS" >&2
else
echo "talos_update=false"
echo ">> Talos is up to date" >&2
fi
# For RPi kernel, we output what we found — the actual version tracking
# depends on the pkgs patch content which references a specific kernel tag
echo "rpi_current=check-patch"
echo "rpi_latest=$LATEST_RPI_KERNEL"
# We always flag RPi kernel for review since we can't easily parse the
# patch to extract the exact pinned version
echo "rpi_update=true"
echo ">> RPi kernel latest stable: $LATEST_RPI_KERNEL (review patch manually)" >&2