diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml new file mode 100644 index 0000000..4010a27 --- /dev/null +++ b/.github/workflows/release-image.yml @@ -0,0 +1,104 @@ +# Build burnable Raspberry Pi OS images for the BetterFrame kiosk on tag push. +# +# Output: betterframe-client--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 diff --git a/deploy/pi-gen/stage-betterframe-client/00-install-packages/00-packages b/deploy/pi-gen/stage-betterframe-client/00-install-packages/00-packages new file mode 100644 index 0000000..08c2f34 --- /dev/null +++ b/deploy/pi-gen/stage-betterframe-client/00-install-packages/00-packages @@ -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 diff --git a/deploy/pi-gen/stage-betterframe-client/01-install-kiosk/00-run-chroot.sh b/deploy/pi-gen/stage-betterframe-client/01-install-kiosk/00-run-chroot.sh new file mode 100755 index 0000000..af9ad84 --- /dev/null +++ b/deploy/pi-gen/stage-betterframe-client/01-install-kiosk/00-run-chroot.sh @@ -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."