feat(release): pi-gen image build pipeline (flashable .img.xz on tag push)

New workflow .github/workflows/release-image.yml takes a tagged kiosk
release binary, layers it onto Raspberry Pi OS Trixie Lite via a custom
pi-gen stage, and publishes the resulting .img.xz back to the GitHub
Release.

Custom stage deploy/pi-gen/stage-betterframe-client/:
  - 00-install-packages: cage, seatd, plymouth, gtk4 runtime, gstreamer,
    libwebkitgtk-6.0, wlr-randr, ca-certificates
  - 01-install-kiosk: drops the prebuilt kiosk binary, systemd unit,
    cage PAM stack, firmware-rollback hook, plymouth theme. Creates
    bfkiosk user, sets multi-user.target, masks all display managers,
    purges piwiz, edits cmdline/config for the BF splash. Mirrors
    setup-pi-kiosk.sh but baked into the image.

End state: rpi-imager → SD → boot → pairing screen on the HDMI display,
no operator setup steps. Kiosk auto-discovers server via discover_server()
(localhost → mDNS → frame-eu.betterportal.net).

Heavy build (~30-60 min on GH-hosted Ubuntu) so tag-push triggered, not
master. Workflow_dispatch also supports baking an existing release tag's
binary into a fresh image without re-tagging.
This commit is contained in:
Mitchell R 2026-05-19 04:34:21 +02:00
parent 093f4947a1
commit 9699036bb2
No known key found for this signature in database
3 changed files with 199 additions and 0 deletions

104
.github/workflows/release-image.yml vendored Normal file
View file

@ -0,0 +1,104 @@
# Build burnable Raspberry Pi OS images for the BetterFrame kiosk on tag push.
#
# Output: betterframe-client-<version>-aarch64.img.xz attached to the GitHub
# Release. Burn with rpi-imager / dd, boot the Pi, kiosk discovers a BF server
# on the LAN or falls through to the cloud (frame-eu.betterportal.net).
#
# Image source: official Raspberry Pi OS Trixie (Lite) base with a custom
# pi-gen stage (`deploy/pi-gen/stage-betterframe-client/`) layered on top.
#
# Heavy build (~30-60 min). Tag-push only — too slow for every master commit.
name: release-image
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
kiosk_artifact_tag:
description: "Existing release tag whose kiosk binary to bake in (e.g. v0.4.2). Empty = same tag as this run."
required: false
default: ""
permissions:
contents: write
jobs:
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Resolve kiosk binary source
id: src
shell: bash
run: |
if [[ "${{ github.ref_type }}" == "tag" ]]; then
tag="${GITHUB_REF#refs/tags/}"
else
tag="${{ inputs.kiosk_artifact_tag }}"
[ -z "$tag" ] && { echo "kiosk_artifact_tag input required for workflow_dispatch"; exit 1; }
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
- name: Download kiosk aarch64 binary from release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.src.outputs.tag }}
run: |
mkdir -p staging
gh release download "$TAG" \
--pattern "betterframe-kiosk-*-aarch64-unknown-linux-gnu" \
--output staging/betterframe-kiosk \
--repo "$GITHUB_REPOSITORY"
chmod +x staging/betterframe-kiosk
# Render BF logo for plymouth (rsvg-convert is in the runner).
sudo apt-get -y update
sudo apt-get -y install --no-install-recommends librsvg2-bin
rsvg-convert -w 480 server/src/web-static/betterframe-logo.svg -o staging/logo.png
- name: Stage files for pi-gen
run: |
mkdir -p deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files
cp staging/betterframe-kiosk \
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
cp staging/logo.png \
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
cp deploy/systemd/betterframe-kiosk.service \
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
cp deploy/systemd/betterframe-firmware-rollback.sh \
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
cp deploy/pam.d/cage \
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/cage.pam
cp deploy/plymouth/betterframe/betterframe.plymouth \
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
cp deploy/plymouth/betterframe/betterframe.script \
deploy/pi-gen/stage-betterframe-client/01-install-kiosk/files/
chmod +x deploy/pi-gen/stage-betterframe-client/01-install-kiosk/00-run-chroot.sh
- name: Build Pi image (pi-gen)
uses: usimd/pi-gen-action@v1
with:
image-name: betterframe-client-${{ steps.src.outputs.version }}
# Lite base, no desktop. Plus our custom stage.
stage-list: stage0 stage1 stage2 ./deploy/pi-gen/stage-betterframe-client
release: trixie
enable-ssh: 1
# Bake a default user — operator can change later. Pi-imager-style
# first-run wizard is purged inside our stage anyway.
username: bfadmin
password: betterframe
locale: en_US.UTF-8
timezone: Etc/UTC
hostname: betterframe-kiosk
compression: xz
- name: Upload image to GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: |
deploy/image-betterframe-client-${{ steps.src.outputs.version }}-lite.img.xz

View file

@ -0,0 +1,16 @@
cage
seatd
plymouth
plymouth-themes
librsvg2-bin
libgtk-4-1
libgstreamer1.0-0
libgstreamer-plugins-base1.0-0
libwebkitgtk-6.0-4
gstreamer1.0-plugins-base
gstreamer1.0-plugins-good
gstreamer1.0-plugins-bad
gstreamer1.0-libav
v4l-utils
wlr-randr
ca-certificates

View file

@ -0,0 +1,79 @@
#!/bin/bash -e
# Runs inside the pi-gen chroot. Installs the BetterFrame kiosk binary +
# systemd unit + cage PAM + plymouth theme. Mirrors setup-pi-kiosk.sh but
# baked into the image so first boot is fully provisioned.
# --- bfkiosk user ---
if ! id -u bfkiosk >/dev/null 2>&1; then
useradd -m -s /usr/sbin/nologin bfkiosk
fi
for grp in video render input audio; do
if getent group "$grp" >/dev/null; then
usermod -a -G "$grp" bfkiosk
fi
done
# --- Binary ---
install -d -m 755 /opt/betterframe/kiosk
install -m 755 /tmp/bf-files/betterframe-kiosk /opt/betterframe/kiosk/betterframe-kiosk
# --- Systemd unit + PAM + rollback hook ---
install -m 644 /tmp/bf-files/betterframe-kiosk.service /etc/systemd/system/betterframe-kiosk.service
install -m 644 /tmp/bf-files/cage.pam /etc/pam.d/cage
install -m 755 /tmp/bf-files/betterframe-firmware-rollback.sh \
/usr/local/sbin/betterframe-firmware-rollback.sh
# Default env file — operator may edit on first boot to point at their server.
cat > /etc/default/betterframe-kiosk <<'EOF'
# Runtime env for betterframe-kiosk. Edit and `systemctl restart betterframe-kiosk`.
# Override the BF server discovery (default tries localhost → betterframe.local
# → frame-eu.betterportal.net):
# BETTERFRAME_SERVER=https://frame.example.com
EOF
# Plymouth boot splash
install -d -m 755 /usr/share/plymouth/themes/betterframe
install -m 644 /tmp/bf-files/betterframe.plymouth /usr/share/plymouth/themes/betterframe/betterframe.plymouth
install -m 644 /tmp/bf-files/betterframe.script /usr/share/plymouth/themes/betterframe/betterframe.script
install -m 644 /tmp/bf-files/logo.png /usr/share/plymouth/themes/betterframe/logo.png
plymouth-set-default-theme betterframe || true
# --- Enable services, disable noise ---
systemctl enable seatd
systemctl enable betterframe-kiosk.service
# Boot to multi-user, no display manager, no welcome wizard, no getty on tty1.
systemctl set-default multi-user.target
for dm in lightdm gdm gdm3 sddm; do
systemctl disable "${dm}.service" 2>/dev/null || true
systemctl mask "${dm}.service" 2>/dev/null || true
done
systemctl disable getty@tty1.service 2>/dev/null || true
# piwiz first-run wizard + userconf-pi → out.
apt-get -y purge piwiz userconf-pi 2>/dev/null || true
rm -f /etc/xdg/autostart/piwiz.desktop
# Suppress console motd / issue.
: > /etc/motd
printf 'BetterFrame Kiosk\n\n' > /etc/issue
rm -f /etc/update-motd.d/* 2>/dev/null || true
# Boot config: quiet splash + no rainbow.
if [ -f /boot/firmware/cmdline.txt ]; then BOOT_DIR=/boot/firmware
else BOOT_DIR=/boot; fi
CMDLINE="${BOOT_DIR}/cmdline.txt"
CONFIG="${BOOT_DIR}/config.txt"
if [ -f "$CMDLINE" ]; then
for flag in quiet splash plymouth.ignore-serial-consoles loglevel=0 vt.global_cursor_default=0 logo.nologo; do
if ! grep -qw -- "$flag" "$CMDLINE"; then
sed -i "s|\$| $flag|" "$CMDLINE"
fi
done
fi
if [ -f "$CONFIG" ] && ! grep -q '^disable_splash=1' "$CONFIG"; then
printf '\n# BetterFrame: disable firmware rainbow splash\ndisable_splash=1\n' >> "$CONFIG"
fi
rm -rf /tmp/bf-files
echo "BetterFrame kiosk stage complete."