diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f550a24 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,188 @@ +# Reusable build workflow. Invoked by release.yml on every master push (dev +# release) and every v* tag push (stable / beta). Produces: +# - betterframe-kiosk-- (Linux ELF, no extension) +# - betterframe-client--.img.xz (flashable Pi OS image) +# All assets attached to the GitHub Release at ${{ inputs.tag }}. + +name: build + +on: + workflow_call: + inputs: + version: + description: "Semver without leading v (0.4.2 or 0.4.2-dev.abcdef0)" + required: true + type: string + channel: + description: "stable | beta | dev" + required: true + type: string + tag: + description: "Git tag (with leading v)" + required: true + type: string + ref: + description: "Git ref to check out (commit sha or tag name)" + required: true + type: string + build-image: + description: "Also build the flashable .img.xz (slow; skip for fast dev builds)" + required: false + type: boolean + default: true + secrets: + BF_AUTOIMPORT_URL: + required: false + BF_AUTOIMPORT_API_KEY: + required: false + +permissions: + contents: write + +jobs: + # ---- Per-arch kiosk binary ------------------------------------------------ + binary: + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-unknown-linux-gnu + runs-on: blacksmith-2vcpu-ubuntu-2404-arm + - target: x86_64-unknown-linux-gnu + runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ matrix.runs-on }} + # Trixie container matches Pi OS Trixie's glibc + apt packages. + container: + image: debian:trixie-slim + steps: + - name: Bootstrap apt + git (container has none preinstalled) + run: | + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates curl git build-essential pkg-config jq sudo + git config --global --add safe.directory '*' + + - uses: actions/checkout@v4 + with: { ref: ${{ inputs.ref }} } + + - name: Install GTK/GStreamer/WebKit build deps + run: | + apt-get install -y --no-install-recommends \ + libgtk-4-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + libwebkitgtk-6.0-dev libssl-dev + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: cargo build --release + working-directory: kiosk + env: + BF_BUILD_ARCH: ${{ matrix.target }} + run: cargo build --release --target ${{ matrix.target }} + + - name: Strip + rename + working-directory: kiosk + run: | + strip target/${{ matrix.target }}/release/betterframe-kiosk + cp target/${{ matrix.target }}/release/betterframe-kiosk \ + betterframe-kiosk-${{ inputs.version }}-${{ matrix.target }} + + - name: Upload to GitHub Release (binary) + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ inputs.tag }} + files: kiosk/betterframe-kiosk-${{ inputs.version }}-${{ matrix.target }} + + - name: Auto-import into BF server + if: env.BF_AUTOIMPORT_URL != '' && env.BF_AUTOIMPORT_API_KEY != '' + env: + BF_AUTOIMPORT_URL: ${{ secrets.BF_AUTOIMPORT_URL }} + BF_AUTOIMPORT_API_KEY: ${{ secrets.BF_AUTOIMPORT_API_KEY }} + working-directory: kiosk + run: | + bin="betterframe-kiosk-${{ inputs.version }}-${{ matrix.target }}" + content_b64=$(base64 -w 0 "$bin") + curl -sSf -X POST \ + -H "Authorization: Bearer ${BF_AUTOIMPORT_API_KEY}" \ + -H "Content-Type: application/json" \ + -d "$(jq -nc \ + --arg v "${{ inputs.version }}" \ + --arg c "${{ inputs.channel }}" \ + --arg a "${{ matrix.target }}" \ + --arg n "GitHub Actions build of ${{ inputs.tag }} (${{ github.sha }})" \ + --arg b "$content_b64" \ + '{version:$v, channel:$c, arch:$a, release_notes:$n, content_b64:$b}')" \ + "${BF_AUTOIMPORT_URL}/api/admin/firmware/import" + + - name: Upload artifact (always) + uses: actions/upload-artifact@v4 + with: + name: betterframe-kiosk-${{ matrix.target }} + path: kiosk/betterframe-kiosk-${{ inputs.version }}-${{ matrix.target }} + retention-days: 14 + + # ---- Flashable Pi OS Trixie image (aarch64 only) ------------------------- + image: + if: ${{ inputs.build-image }} + needs: binary + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: { ref: ${{ inputs.ref }} } + + - name: Pull kiosk aarch64 binary from this run's artifact + uses: actions/download-artifact@v4 + with: + name: betterframe-kiosk-aarch64-unknown-linux-gnu + path: staging/ + + - name: Render BF logo for plymouth + run: | + sudo apt-get -y update + sudo apt-get -y install --no-install-recommends librsvg2-bin + mv staging/betterframe-kiosk-${{ inputs.version }}-aarch64-unknown-linux-gnu \ + staging/betterframe-kiosk + chmod +x staging/betterframe-kiosk + 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-${{ inputs.version }} + stage-list: stage0 stage1 stage2 ./deploy/pi-gen/stage-betterframe-client + release: trixie + enable-ssh: 1 + username: bfadmin + password: betterframe + locale: en_US.UTF-8 + timezone: Etc/UTC + hostname: betterframe-kiosk + compression: xz + + - name: Upload image to GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ inputs.tag }} + files: | + deploy/image-betterframe-client-${{ inputs.version }}-lite.img.xz diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml deleted file mode 100644 index 4010a27..0000000 --- a/.github/workflows/release-image.yml +++ /dev/null @@ -1,104 +0,0 @@ -# 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/.github/workflows/release-kiosk.yml b/.github/workflows/release-kiosk.yml deleted file mode 100644 index bf409ec..0000000 --- a/.github/workflows/release-kiosk.yml +++ /dev/null @@ -1,149 +0,0 @@ -# Build the kiosk binary for multiple targets on tag push (vX.Y.Z), upload -# each as a GitHub Release asset, and optionally auto-import into a running -# BetterFrame server via /api/admin/firmware/import. -# -# Build environment is debian:trixie-slim (matches Raspberry Pi OS Trixie -# the kiosk deploys to). Trixie ships gtk4 4.14 + libwebkitgtk-6.0 natively, -# no backports needed. glibc matches Pi OS Trixie → no runtime symbol drift. -# Runner host distro doesn't matter; everything runs inside the container. -# -# Required secrets: -# BF_AUTOIMPORT_URL e.g. https://bf.example.com (optional) -# BF_AUTOIMPORT_API_KEY admin-scope API key for the BF server (optional) - -name: release-kiosk - -on: - push: - branches: - - master - tags: - - "v*" - workflow_dispatch: - inputs: - channel: - description: "Release channel" - type: choice - options: ["dev", "beta", "stable"] - default: "dev" - -# Cancel an in-flight run when a newer commit lands on the same branch — -# dev channel only needs the latest. -concurrency: - group: release-kiosk-${{ github.ref }} - cancel-in-progress: ${{ github.ref_type != 'tag' }} - -permissions: - contents: write - -jobs: - build: - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-unknown-linux-gnu - runs-on: blacksmith-2vcpu-ubuntu-2404-arm - arch_label: "aarch64 (Pi5)" - - target: x86_64-unknown-linux-gnu - runs-on: blacksmith-4vcpu-ubuntu-2404 - arch_label: "x86_64" - - runs-on: ${{ matrix.runs-on }} - - # Lock glibc + APT package set to Trixie — matches RPi OS Trixie - # (Debian 13) which kiosks run. Raspbian is Debian; same packages. - container: - image: debian:trixie-slim - - steps: - - name: Bootstrap apt + git (container has none preinstalled) - run: | - apt-get update - apt-get install -y --no-install-recommends \ - ca-certificates curl git build-essential pkg-config \ - jq sudo - # Container UID != workspace owner UID — git's CVE-2022-24765 - # check refuses to operate. Whitelist before checkout runs git. - git config --global --add safe.directory '*' - - - uses: actions/checkout@v4 - - - name: Determine channel + version - id: meta - shell: bash - run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - channel="${{ inputs.channel }}" - version="0.0.0-dev.$(git rev-parse --short HEAD)" - elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then - tag="${GITHUB_REF#refs/tags/v}" - version="$tag" - if [[ "$tag" == *"-beta."* ]]; then channel="beta"; - else channel="stable"; fi - else - # Push to master → auto dev channel build. - channel="dev" - version="0.0.0-dev.$(git rev-parse --short HEAD)" - fi - echo "channel=$channel" >> "$GITHUB_OUTPUT" - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Install GTK/GStreamer/WebKit build deps - # Trixie stock: gtk4 4.14 + libwebkitgtk-6.0 — no backports needed. - run: | - apt-get install -y --no-install-recommends \ - libgtk-4-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ - libwebkitgtk-6.0-dev libssl-dev - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: cargo build --release - working-directory: kiosk - env: - BF_BUILD_ARCH: ${{ matrix.target }} - run: cargo build --release --target ${{ matrix.target }} - - - name: Strip + rename binary - working-directory: kiosk - run: | - strip target/${{ matrix.target }}/release/betterframe-kiosk - cp target/${{ matrix.target }}/release/betterframe-kiosk \ - betterframe-kiosk-${{ steps.meta.outputs.version }}-${{ matrix.target }} - - - name: Upload to GitHub Release - if: startsWith(github.ref, 'refs/tags/v') - uses: softprops/action-gh-release@v2 - with: - files: kiosk/betterframe-kiosk-${{ steps.meta.outputs.version }}-${{ matrix.target }} - - - name: Auto-import into BF server - if: env.BF_AUTOIMPORT_URL != '' && env.BF_AUTOIMPORT_API_KEY != '' - env: - BF_AUTOIMPORT_URL: ${{ secrets.BF_AUTOIMPORT_URL }} - BF_AUTOIMPORT_API_KEY: ${{ secrets.BF_AUTOIMPORT_API_KEY }} - working-directory: kiosk - run: | - bin="betterframe-kiosk-${{ steps.meta.outputs.version }}-${{ matrix.target }}" - content_b64=$(base64 -w 0 "$bin") - curl -sSf -X POST \ - -H "Authorization: Bearer ${BF_AUTOIMPORT_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$(jq -nc \ - --arg v "${{ steps.meta.outputs.version }}" \ - --arg c "${{ steps.meta.outputs.channel }}" \ - --arg a "${{ matrix.target }}" \ - --arg n "Built by GH Actions (${{ github.sha }})" \ - --arg b "$content_b64" \ - '{version:$v, channel:$c, arch:$a, release_notes:$n, content_b64:$b}')" \ - "${BF_AUTOIMPORT_URL}/api/admin/firmware/import" - - - name: Upload artifact (always) - uses: actions/upload-artifact@v4 - with: - name: betterframe-kiosk-${{ matrix.target }} - path: kiosk/betterframe-kiosk-${{ steps.meta.outputs.version }}-${{ matrix.target }} - retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4e6a641 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,131 @@ +# Single entrypoint for release automation. +# +# Triggers: +# - push to master → auto-tag (semver patch bump + -dev.), +# create prerelease, invoke build.yml +# - push of v* tag → use tag as-is, create release (or update), +# invoke build.yml; channel = stable / beta +# +# Build steps live in build.yml (reusable). This file only owns versioning +# + release creation. Skip the heavy image build for dev runs (binary only) +# so master pushes don't take 30 min; tag pushes do the full image. + +name: release + +on: + push: + branches: + - master + tags: + - "v*" + workflow_dispatch: + inputs: + bump: + description: "Force a tagged dev release (one-off)." + type: boolean + default: false + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: ${{ github.ref_type != 'tag' }} + +permissions: + contents: write + +jobs: + # ---- Tag + release record ------------------------------------------------- + meta: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.compute.outputs.version }} + tag: ${{ steps.compute.outputs.tag }} + channel: ${{ steps.compute.outputs.channel }} + ref: ${{ steps.compute.outputs.ref }} + build_image: ${{ steps.compute.outputs.build_image }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # need tags for git describe + + - name: Compute version / channel / tag + id: compute + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + short=$(git rev-parse --short HEAD) + + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + tag="${GITHUB_REF#refs/tags/}" + version="${tag#v}" + if [[ "$version" == *"-beta."* ]]; then + channel=beta + elif [[ "$version" == *"-dev."* ]]; then + channel=dev + else + channel=stable + fi + build_image=true + else + # Master push (or workflow_dispatch). Find latest stable tag, + # bump patch, append -dev.. Prerelease. + latest=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname \ + | grep -v '-' | head -1 || true) + if [ -z "$latest" ]; then latest="v0.0.0"; fi + base="${latest#v}" + IFS=. read -r MAJ MIN PAT <<< "$base" + PAT=$((PAT + 1)) + version="${MAJ}.${MIN}.${PAT}-dev.${short}" + tag="v${version}" + channel=dev + # Skip image build for master pushes — too slow. Image happens on + # tags or when workflow_dispatch.bump=true. + build_image=${{ inputs.bump == true || 'false' }} + fi + + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "channel=$channel" >> "$GITHUB_OUTPUT" + echo "ref=$GITHUB_SHA" >> "$GITHUB_OUTPUT" + echo "build_image=$build_image" >> "$GITHUB_OUTPUT" + + - name: Create / ensure release record (lightweight tag for master pushes) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.compute.outputs.tag }} + CHANNEL: ${{ steps.compute.outputs.channel }} + run: | + set -e + # For master / dispatch we need to create the tag ourselves. + if [[ "$GITHUB_REF" != refs/tags/v* ]]; then + git config user.email "actions@github.com" + git config user.name "github-actions[bot]" + git tag -a "$TAG" -m "Auto-tag $TAG ($CHANNEL)" + git push origin "$TAG" + fi + # Create release if missing. Mark dev/beta as prerelease. + if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + extra="" + [[ "$CHANNEL" != "stable" ]] && extra="--prerelease" + gh release create "$TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$TAG" \ + --target "$GITHUB_SHA" \ + --notes "Channel: ${CHANNEL}. Commit ${GITHUB_SHA}." \ + $extra + fi + + # ---- Build assets --------------------------------------------------------- + build: + needs: meta + uses: ./.github/workflows/build.yml + with: + version: ${{ needs.meta.outputs.version }} + tag: ${{ needs.meta.outputs.tag }} + channel: ${{ needs.meta.outputs.channel }} + ref: ${{ needs.meta.outputs.ref }} + build-image: ${{ needs.meta.outputs.build_image == 'true' }} + secrets: + BF_AUTOIMPORT_URL: ${{ secrets.BF_AUTOIMPORT_URL }} + BF_AUTOIMPORT_API_KEY: ${{ secrets.BF_AUTOIMPORT_API_KEY }}