From e31cd9add88bc080ecc0f98e80aac6c11e6173dd Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Mon, 9 Feb 2026 17:58:17 -0500 Subject: [PATCH] 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 --- .gitea/workflows/build.yaml | 103 +++++++++++++++++++ .gitea/workflows/check-updates.yaml | 121 ++++++++++++++++++++++ .gitignore | 4 + Makefile | 150 ++++++++++++++++++++++++++++ README.md | 89 +++++++++++++++++ config/config.txt.append | 4 + config/extensions.yaml | 15 +++ scripts/check-upstream.sh | 54 ++++++++++ 8 files changed, 540 insertions(+) create mode 100644 .gitea/workflows/build.yaml create mode 100644 .gitea/workflows/check-updates.yaml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 config/config.txt.append create mode 100644 config/extensions.yaml create mode 100755 scripts/check-upstream.sh diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..3cd69b1 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -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:) +# - 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 diff --git a/.gitea/workflows/check-updates.yaml b/.gitea/workflows/check-updates.yaml new file mode 100644 index 0000000..ef41cc4 --- /dev/null +++ b/.gitea/workflows/check-updates.yaml @@ -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'], + }); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cccd6f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +checkouts/ +_out/ +*.raw +*.raw.zst diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..54a6280 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..56ca283 --- /dev/null +++ b/README.md @@ -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:` (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 --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 +``` diff --git a/config/config.txt.append b/config/config.txt.append new file mode 100644 index 0000000..d00e4e3 --- /dev/null +++ b/config/config.txt.append @@ -0,0 +1,4 @@ +# CM5 Overclock — 2.6GHz stable on Compute Blade with heatsink +arm_freq=2600 +over_voltage_delta=50000 +arm_boost=1 diff --git a/config/extensions.yaml b/config/extensions.yaml new file mode 100644 index 0000000..879d7b1 --- /dev/null +++ b/config/extensions.yaml @@ -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 diff --git a/scripts/check-upstream.sh b/scripts/check-upstream.sh new file mode 100755 index 0000000..1f31139 --- /dev/null +++ b/scripts/check-upstream.sh @@ -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