Skip to content

Build Talos UFS

Build Talos UFS #48

Workflow file for this run

name: Build Talos UFS
on:
workflow_dispatch:
inputs:
talos_version:
description: 'Talos version to build (e.g., v1.12.2)'
required: true
type: string
concurrency:
group: build-${{ github.workflow }}
cancel-in-progress: true
env:
KERNEL_IMAGE: ghcr.io/${{ github.repository_owner }}/talos-ufs-kernel
IMAGER_IMAGE: ghcr.io/${{ github.repository_owner }}/talos-ufs-imager
INSTALLER_IMAGE: ghcr.io/${{ github.repository_owner }}/talos-ufs-installer
jobs:
build-kernel:
runs-on: ubuntu-latest
timeout-minutes: 360
permissions:
contents: read
packages: write
outputs:
pkgs_ref: ${{ steps.resolve-pkgs.outputs.pkgs_ref }}
kernel_image: ${{ steps.meta.outputs.kernel_image }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Clone siderolabs/talos
run: |
git clone --depth 1 --branch "${{ inputs.talos_version }}" \
https://github.com/siderolabs/talos.git /tmp/talos
- name: Resolve pkgs version from Talos Makefile
id: resolve-pkgs
run: |
PKGS_REF=$(grep -E '^PKGS \?=' /tmp/talos/Makefile | sed 's/PKGS ?= //')
echo "pkgs_ref=${PKGS_REF}" >> "$GITHUB_OUTPUT"
echo "Resolved pkgs ref: ${PKGS_REF}"
# Extract git commit hash from describe format (e.g., v1.12.0-32-g4f8efaf -> 4f8efaf)
if [[ "${PKGS_REF}" =~ -g([0-9a-f]+)$ ]]; then
PKGS_COMMIT="${BASH_REMATCH[1]}"
echo "pkgs_commit=${PKGS_COMMIT}" >> "$GITHUB_OUTPUT"
echo "Resolved pkgs commit: ${PKGS_COMMIT}"
else
echo "pkgs_commit=${PKGS_REF}" >> "$GITHUB_OUTPUT"
fi
- name: Clone siderolabs/pkgs
run: |
git clone https://github.com/siderolabs/pkgs.git /tmp/pkgs
cd /tmp/pkgs
git checkout "${{ steps.resolve-pkgs.outputs.pkgs_commit }}"
- name: Apply kernel config patch
run: |
git -C /tmp/pkgs apply --3way "${{ github.workspace }}/patches/kernel-config.patch"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set kernel image tag
id: meta
run: |
TAG="${{ inputs.talos_version }}"
echo "kernel_image=${KERNEL_IMAGE}:${TAG}" >> "$GITHUB_OUTPUT"
- name: Check if kernel image already exists
id: check-kernel
run: |
if skopeo inspect "docker://${{ steps.meta.outputs.kernel_image }}" &>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Kernel image already exists, skipping build."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Kernel image not found, building."
fi
- name: Build and push kernel
if: steps.check-kernel.outputs.exists == 'false'
working-directory: /tmp/pkgs
run: |
docker buildx build \
--no-cache \
--file=Pkgfile \
--platform=linux/amd64 \
--target=kernel \
--tag="${{ steps.meta.outputs.kernel_image }}" \
--push \
.
build-talos:
runs-on: ubuntu-latest
timeout-minutes: 120
needs: build-kernel
permissions:
contents: read
packages: write
outputs:
imager_image: ${{ steps.meta.outputs.imager_image }}
installer_image: ${{ steps.meta.outputs.installer_image }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set image tags
id: meta
run: |
TAG="${{ inputs.talos_version }}"
echo "imager_image=${IMAGER_IMAGE}:${TAG}" >> "$GITHUB_OUTPUT"
echo "installer_image=${INSTALLER_IMAGE}:${TAG}" >> "$GITHUB_OUTPUT"
- name: Check if talos images already exist
id: check-talos
run: |
if skopeo inspect "docker://${IMAGER_IMAGE}:${{ inputs.talos_version }}" &>/dev/null && \
skopeo inspect "docker://${INSTALLER_IMAGE}:${{ inputs.talos_version }}" &>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Imager and installer images already exist, skipping build."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Images not found, building."
fi
- name: Clone siderolabs/talos
if: steps.check-talos.outputs.exists == 'false'
run: |
git clone --depth 1 --branch "${{ inputs.talos_version }}" \
https://github.com/siderolabs/talos.git /tmp/talos
- name: Apply EFI partition size patch
if: steps.check-talos.outputs.exists == 'false'
run: |
git -C /tmp/talos apply --3way "${{ github.workspace }}/patches/efi-partition-size.patch"
- name: Install crane
if: steps.check-talos.outputs.exists == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Authenticate the API call: unauthenticated requests from shared
# Actions runners hit the 60/hr rate limit and return JSON without
# .tag_name, leaving VERSION=null and a 404 download URL.
VERSION=$(curl -fsSL -H "Authorization: Bearer ${GH_TOKEN}" \
https://api.github.com/repos/google/go-containerregistry/releases/latest | jq -r .tag_name)
if [ -z "${VERSION}" ] || [ "${VERSION}" = "null" ]; then
echo "Failed to resolve latest crane version" >&2
exit 1
fi
echo "Installing crane ${VERSION}"
curl -fsSL "https://github.com/google/go-containerregistry/releases/download/${VERSION}/go-containerregistry_Linux_x86_64.tar.gz" | sudo tar -xz -C /usr/local/bin crane
- name: Start local registry
if: steps.check-talos.outputs.exists == 'false'
run: |
docker run -d -p 5000:5000 --name registry registry:2
- name: Set up Docker Buildx (with insecure local registry)
if: steps.check-talos.outputs.exists == 'false'
uses: docker/setup-buildx-action@v4
with:
buildkitd-config-inline: |
[registry."localhost:5000"]
http = true
insecure = true
driver-opts: network=host
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build imager to local registry
if: steps.check-talos.outputs.exists == 'false'
working-directory: /tmp/talos
run: |
mkdir -p _out
make imager \
PKG_KERNEL="${{ needs.build-kernel.outputs.kernel_image }}" \
PLATFORM=linux/amd64 \
REGISTRY=localhost:5000 \
PUSH=true \
INSTALLER_ARCH=amd64
- name: Build installer-base to local registry
if: steps.check-talos.outputs.exists == 'false'
working-directory: /tmp/talos
run: |
mkdir -p _out
make installer-base \
PKG_KERNEL="${{ needs.build-kernel.outputs.kernel_image }}" \
PLATFORM=linux/amd64 \
REGISTRY=localhost:5000 \
PUSH=true \
INSTALLER_ARCH=amd64
- name: Build installer to local registry
if: steps.check-talos.outputs.exists == 'false'
working-directory: /tmp/talos
env:
CRANE_INSECURE: "true"
run: |
# Talos v1.13+ runs imager as host user (--user $(id -u):$(id -g)).
# Pre-create _out so the docker volume mount doesn't auto-create it as root.
mkdir -p _out
make installer \
PKG_KERNEL="${{ needs.build-kernel.outputs.kernel_image }}" \
PLATFORM=linux/amd64 \
REGISTRY=localhost:5000 \
PUSH=true \
INSTALLER_ARCH=amd64
- name: Determine local image tag
if: steps.check-talos.outputs.exists == 'false'
id: local-tag
run: |
# Match Talos's own Makefile tag logic (TAG := git describe --tag
# --always --dirty --match v[0-9]\*). Without --match, describe can
# resolve to a monorepo tag like pkg/machinery/v1.13.5, whose slashes
# are an invalid Docker reference and don't match the pushed image.
# Talos appends -dirty when source is modified.
cd /tmp/talos
TAG=$(git describe --tag --always --dirty --match 'v[0-9]*' 2>/dev/null || echo "${{ inputs.talos_version }}-dirty")
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "Local image tag: ${TAG}"
- name: Push images to GHCR with project naming
if: steps.check-talos.outputs.exists == 'false'
run: |
LOCAL_TAG="${{ steps.local-tag.outputs.tag }}"
VERSION="${{ inputs.talos_version }}"
echo "${{ secrets.GITHUB_TOKEN }}" | skopeo login ghcr.io -u "${{ github.actor }}" --password-stdin
echo "Copying imager: localhost:5000 -> ${IMAGER_IMAGE}:${VERSION}"
skopeo copy --all --src-tls-verify=false \
"docker://localhost:5000/siderolabs/imager:${LOCAL_TAG}" \
"docker://${IMAGER_IMAGE}:${VERSION}"
echo "Copying installer: localhost:5000 -> ${INSTALLER_IMAGE}:${VERSION}"
skopeo copy --all --src-tls-verify=false \
"docker://localhost:5000/siderolabs/installer:${LOCAL_TAG}" \
"docker://${INSTALLER_IMAGE}:${VERSION}"
generate-iso:
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [build-kernel, build-talos]
permissions:
contents: write
packages: read
issues: write
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate ISO
run: |
mkdir -p output
docker run --rm --platform linux/amd64 \
-v "$(pwd)/output:/out" \
--privileged \
"${{ needs.build-talos.outputs.imager_image }}" \
iso --arch amd64
- name: Generate checksum
run: |
cd output
sha256sum metal-amd64.iso > metal-amd64.iso.sha256
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ inputs.talos_version }}-ufs"
OWNER="${{ github.repository_owner }}"
gh release create "${TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--title "Talos Linux ${{ inputs.talos_version }} with UFS Support" \
--notes "$(cat <<'NOTES_EOF'
**Upstream Release Notes:** https://github.com/siderolabs/talos/releases/tag/${{ inputs.talos_version }}
## Downloads
- `metal-amd64.iso` - USB boot installation media
- `metal-amd64.iso.sha256` - SHA256 checksum
## Changes from Upstream
- UFS drivers (ufshcd-core, ufshcd-pci) built into kernel
- EFI partition size increased to 512MiB (4096-byte sector support)
## Container Images
```
ghcr.io/OWNER_PLACEHOLDER/talos-ufs-installer:VERSION_PLACEHOLDER
ghcr.io/OWNER_PLACEHOLDER/talos-ufs-imager:VERSION_PLACEHOLDER
ghcr.io/OWNER_PLACEHOLDER/talos-ufs-kernel:VERSION_PLACEHOLDER
```
## Installation
1. Write ISO to USB drive
2. Disable Secure Boot on device
3. Boot from USB
4. Specify the installer in machine config:
```yaml
machine:
install:
image: ghcr.io/OWNER_PLACEHOLDER/talos-ufs-installer:VERSION_PLACEHOLDER
```
## Custom ISO Generation
```bash
docker run --rm -t -v /dev:/dev --privileged \
ghcr.io/OWNER_PLACEHOLDER/talos-ufs-imager:VERSION_PLACEHOLDER \
metal --system-extension-image <extension-image>
```
## Supported Hardware
All x86_64 devices with PCI-connected UFS controllers.
### Verified
- MINISFORUM S100-WLP (Intel Alder Lake-N, PCI ID: 8086:54ff)
NOTES_EOF
)" \
output/metal-amd64.iso \
output/metal-amd64.iso.sha256
# Replace placeholders in release notes
gh release edit "${TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--notes "$(gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" --json body --jq .body | \
sed "s/OWNER_PLACEHOLDER/${OWNER}/g; s/VERSION_PLACEHOLDER/${{ inputs.talos_version }}/g")"
- name: Auto-close patch failure issues
if: success()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Close any open patch-failure issues
ISSUES=$(gh issue list \
--repo "${GITHUB_REPOSITORY}" \
--label "patch-failure" \
--state open \
--json number \
--jq '.[].number' 2>/dev/null || true)
for ISSUE_NUM in ${ISSUES}; do
gh issue close "${ISSUE_NUM}" \
--repo "${GITHUB_REPOSITORY}" \
--comment "Automatically closed: build succeeded for ${{ inputs.talos_version }}." || true
done
notify-failure:
runs-on: ubuntu-latest
needs: [build-kernel, build-talos, generate-iso]
if: failure()
permissions:
issues: write
steps:
- name: Create failure issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ inputs.talos_version }}"
TITLE="Build failed for Talos ${VERSION}"
# Ensure patch-failure label exists
gh label create "patch-failure" \
--description "Automated build failure" \
--color "d93f0b" \
--repo "${GITHUB_REPOSITORY}" 2>/dev/null || true
# Check if an open issue already exists for this version
EXISTING=$(gh issue list \
--repo "${GITHUB_REPOSITORY}" \
--label "patch-failure" \
--state open \
--search "in:title ${TITLE}" \
--json number \
--jq 'length')
if [ "${EXISTING}" -gt 0 ]; then
echo "Issue already exists for ${VERSION}, skipping creation."
exit 0
fi
gh issue create \
--repo "${GITHUB_REPOSITORY}" \
--title "${TITLE}" \
--label "patch-failure" \
--body "$(cat <<EOF
The automated build for Talos **${VERSION}** has failed.
**Workflow run:** ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}
This usually means one of:
1. A patch failed to apply due to upstream changes
2. The kernel build failed
3. The Talos imager/installer build failed
Please check the workflow logs and update the patches if needed.
EOF
)"