Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8eda774
docs: log Actions artifact cleanup (#18) in action plan
dubpixel Sep 5, 2026
e157c44
fix: widen IP address card on status page (#16)
dubpixel Sep 5, 2026
f4959f4
fix: correct Dashboard kiosk flag from --kiosk to --kiosk-mode (#13)
dubpixel Sep 5, 2026
bd5199c
feat: add Dashboard remote-config link to web UI (#15)
dubpixel Sep 5, 2026
37e23ee
fix: network settings not persisting on NetworkManager boards (#14)
dubpixel Sep 5, 2026
dcd21db
feat: event-driven splash recovery + deterministic boot mode selectio…
dubpixel Sep 5, 2026
7e19e23
ci: auto-delete redundant + expired Actions artifacts (#18)
dubpixel Sep 5, 2026
42d8432
ci: build on native arm64 runners instead of QEMU-emulated x86_64
dubpixel Sep 5, 2026
d398333
fix: keep qemu-user-static install on native arm64 runners
dubpixel Sep 5, 2026
78078cb
fix: switch_mode() now does a real hidraw recovery, not just a nudge …
dubpixel Sep 6, 2026
27d97f8
feat: add Stop button to Mode tab -- stops current service, shows splash
dubpixel Sep 6, 2026
f69925f
feat: wrap revealed SSH password into 4-char lines instead of shrinki…
dubpixel Sep 6, 2026
043e59c
fix: dpx-dashboard.service enabled under the wrong target, never actu…
dubpixel Sep 9, 2026
4479a23
feat: Dashboard status card on the Status tab (#20)
dubpixel Sep 9, 2026
5e20960
feat: make boot-time mode autostart disable-able, default checked (#12)
dubpixel Sep 9, 2026
b58b8c2
Merge remote-tracking branch 'origin/ci/native-arm64-runners' into fi…
dubpixel Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/workflows/armbian-builder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,14 @@ on:

jobs:
build:
runs-on: ubuntu-latest
# Native arm64 runner -- the images we build are arm64, and Packer's
# arm-image plugin skips its QEMU/binfmt chroot steps entirely when
# image_arch (set in dpx-buttonode.pkr.hcl) matches the host's actual
# runtime arch (pkg/builder/builder.go: `!ImageArch.IsNative()`). On
# ubuntu-latest (x86_64), every apt-get/dpkg inside the chroot ran
# through qemu-aarch64-static emulation, which is commonly 5-10x
# slower than native for that kind of CPU-bound work.
runs-on: ubuntu-24.04-arm
permissions:
contents: read
outputs:
Expand All @@ -294,7 +301,15 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

# ARM emulation is required to chroot into the ARM64 Armbian image
# Still needed even on a native arm64 runner: the plugin's Prepare()
# step unconditionally resolves a qemu_binary path via exec.LookPath
# regardless of whether it'll actually be used later -- only its
# separate Run()-time IsNative() check decides whether QEMU is
# actually invoked for the chroot. Without this installed, Prepare()
# falls back to an "embedded qemu" feature that's amd64-only and
# fails outright on arm64 (confirmed: "embedded qemu is not
# available - currently, embedded qemu is only available for linux
# amd64"). This is just a fast package install, not the slow part.
- name: Install QEMU user-static
run: sudo apt-get update -q && sudo apt-get install -y qemu-user-static

Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/artifact-sweep.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Sweep expired Actions artifacts

# Safety net for issue #18: release-action.yaml's own artifacts are
# deleted immediately once they land in a release, but stray builds that
# never go through that job (feature branches, force-rebuilds, manual
# workflow_dispatch runs someone kicked off and forgot about) just sit
# there. GitHub's own retention-days cleanup can lag by weeks in
# practice -- confirmed 2026-09-05, 12.1GiB of artifacts sitting around
# up to three weeks past their own expiry -- so this sweeps anything
# already past expires_at rather than trusting GitHub to do it.
on:
schedule:
- cron: '0 5 * * 0' # weekly, Sunday 05:00 UTC
workflow_dispatch: {}

jobs:
sweep:
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete artifacts past their own expiry
env:
GH_TOKEN: ${{ github.token }}
run: |
NOW=$(date -u +%s)
gh api "repos/${{ github.repository }}/actions/artifacts" --paginate \
--jq '.artifacts[] | [.id, .expires_at] | @tsv' | while IFS=$'\t' read -r id expires_at; do
[[ -z "$expires_at" || "$expires_at" == "null" ]] && continue
expires_epoch=$(date -u -d "$expires_at" +%s 2>/dev/null || echo 0)
if (( expires_epoch > 0 && expires_epoch < NOW )); then
echo "Deleting expired artifact $id (expired $expires_at)"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" || true
fi
done
12 changes: 9 additions & 3 deletions .github/workflows/raspios-builder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ on:

jobs:
build:
runs-on: ubuntu-latest
# See armbian-builder.yaml's build job for why: native arm64 runner
# lets Packer's arm-image plugin skip QEMU/binfmt entirely
# (image_arch in dpx-buttonode.pkr.hcl matches the host arch), instead
# of emulating every chroot apt-get/dpkg call via qemu-aarch64-static
# on an x86_64 runner.
runs-on: ubuntu-24.04-arm
permissions:
contents: read
outputs:
Expand All @@ -52,8 +57,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

# ARM emulation is required to chroot into the ARM64 image, same as
# the Armbian pipeline -- unrelated to which OS built the base image.
# Still needed on a native arm64 runner -- see armbian-builder.yaml's
# build job for why (Prepare()-time qemu_binary resolution is
# unconditional, only actual usage is skipped on native arch).
- name: Install QEMU user-static
run: sudo apt-get update -q && sudo apt-get install -y qemu-user-static

Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/release-action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
actions: write

steps:
- name: Checkout repository
Expand Down Expand Up @@ -180,3 +181,20 @@ jobs:
--notes-file /tmp/release-notes.md \
release-assets/*.img.gz \
/tmp/buttons-version.txt

# Once the images are attached to the release as real assets, the
# raw CI artifacts this job downloaded from `build` serve no purpose
# -- delete them immediately rather than let retention-days expire
# them on GitHub's own timeline (issue #18: found 12.1GiB of already-
# expired-but-uncollected artifacts sitting around, silently
# billing against the account's Actions storage cap).
- name: Delete this run's CI artifacts (now redundant with the release)
if: success()
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
--jq '.artifacts[].id' | while read -r id; do
echo "Deleting artifact $id"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" || true
done
45 changes: 45 additions & 0 deletions ACTION-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ falling back to splash only if none is." Solving that cleanly handles both the
the right thing should win automatically" case (#12) with one mechanism instead
of two bolted-on fixes that could disagree with each other.

**Implemented 2026-09-05**, in `scripts/install-deck-splash.sh`:
- `OnFailure=dpx-deck-splash.service` drop-ins (`/etc/systemd/system/<mode>.service.d/dpx-recovery.conf`) on all three mode units. Drop-ins, not direct edits, since all three ship from vendor `.deb`s, not this repo — survives a package upgrade.
- `dpx-mode-select.service` (new oneshot, `WantedBy=multi-user.target`): reads `/etc/dpx-mode` at boot and starts exactly that one mode service, falling back to `dpx-deck-splash.service` if nothing's persisted or the target refuses to start.
- `dpx-deck-splash.service` no longer auto-enabled — it's only ever started by the fallback above or by an `OnFailure` recovery, never racing the mode service for `multi-user.target` on its own.
- **Not yet live-verified** (no device access this pass) — needs a real boot-cycle test: confirm the persisted mode wins every time, and force a mode service into permanent failure (e.g. `systemctl kill` past its restart burst) to confirm splash actually comes back.
- Also worth re-checking against this fix once live: the reported "device is already in a mode but not started, hitting GO does not start the thing" symptom. `execute_staged()`'s `mode_dead` check in `dpx-deck-splash.py` already looks correct on paper (re-applies if the persisted mode's service isn't actually active) — this may already have been a downstream effect of the same boot race rather than a separate bug. Confirm rather than assume once testable.

Dashboard's own boot-time auto-start (the "and dashboard on/off" half of #12) is
simpler and independent of the above — it's just "should `dpx-dashboard.service`
be enabled or not," already a persisted systemd state via `set_dashboard_enabled()`,
Expand All @@ -128,3 +135,41 @@ new for it.
5. #11 + #12 together (the real design work — biggest single piece here)
6. #17's "doesn't launch" half — verify once a fresh build exists (falls out of #11/#12 work naturally, since that's a rebuild anyway)
7. #10 and #17's "doesn't reflect state" half — both need live device access, batch them into one SSH session once available

---

## #18 — GitHub Actions artifact storage cleanup (housekeeping)

**2026-09-05: found and fixed once.** `gh api repos/.../actions/artifacts` showed
10 artifacts totaling 12.1 GiB, ALL already past their `retention-days: 3`
expiration (the oldest by three weeks) but never garbage-collected by GitHub —
they were still billing against the 2GB storage cap the whole time. Deleted
manually via `gh api -X DELETE .../actions/artifacts/<id>`, storage now at 0.

Not a one-time cleanup — this will silently refill: `release-action.yaml`'s
nightly cron (`0 6 * * *`) builds new images whenever the Buttons mirror has an
unreleased version, `armbian-builder.yaml`/`raspios-builder.yaml` upload
1-1.7GB artifacts per board/variant, and the `release` job downloads them into
a GitHub Release but never deletes the source CI artifacts afterward — nothing
sweeps them once `retention-days` lapses, they just sit there until someone
notices.

**Squash it down properly, don't just re-delete manually next time:**
- Add a step at the end of `release-action.yaml`'s `release` job (after the
release is successfully created) that deletes the just-downloaded build
artifacts immediately via `gh api -X DELETE` — once they're in the
release as `.img.gz` assets, the raw CI artifacts serve no purpose.
- Consider a small separate scheduled workflow (e.g. weekly) that lists and
deletes any artifact past its `expires_at`, as a safety net for stray
manual/debug-branch builds (feature branches, force-rebuilds) that don't
go through the release job at all.
- Low priority relative to #10-#17, but cheap to build once — fold into
the work whenever convenient, or do it standalone.

**Implemented 2026-09-05**: `release-action.yaml`'s `release` job now deletes
its own run's CI artifacts immediately after the release is created
(`actions: write` added to its permissions). New `artifact-sweep.yaml`
workflow runs weekly (Sunday 05:00 UTC) plus `workflow_dispatch`, deleting
any artifact anywhere in the repo already past its own `expires_at` — the
same category of already-expired-but-uncollected artifact found and
manually cleared this session.
1 change: 1 addition & 0 deletions dpx-buttonode.pkr.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ source "arm-image" "base" {
iso_url = var.url
target_image_size = var.variant == "full" ? 8000000000 : 5000000000
output_filename = "output-dpx-buttonode/dpx-buttonode.img"
image_arch = "arm64"
qemu_binary = "qemu-aarch64-static"
image_mounts = var.image_mounts

Expand Down
Binary file removed images/009_deck_splash.png
Binary file not shown.
6 changes: 3 additions & 3 deletions scripts/install-dashboard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ xset s off
xset s noblank
unclutter -idle 0.5 -root &
openbox-session &
exec companion-dashboard --kiosk --no-sandbox
exec companion-dashboard --kiosk-mode --no-sandbox
XINITRC
chmod +x "$DASH_HOME/.xinitrc"

Expand All @@ -113,7 +113,7 @@ chown -R dpx-dashboard:dpx-dashboard "$DASH_HOME"
cat > /etc/systemd/system/dpx-dashboard.service << 'UNIT'
[Unit]
Description=Companion Dashboard Display Service
After=network-online.target graphical.target
After=network-online.target
Wants=network-online.target

[Service]
Expand All @@ -129,7 +129,7 @@ StandardOutput=journal
StandardError=journal

[Install]
WantedBy=graphical.target
WantedBy=multi-user.target
UNIT

systemctl daemon-reload
Expand Down
77 changes: 75 additions & 2 deletions scripts/install-deck-splash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,81 @@ KillMode=process
WantedBy=multi-user.target
UNIT

systemctl enable dpx-deck-splash.service
echo "==> dpx-deck-splash.service: enabled"
# NOT enabled directly. dpx-mode-select.service (below) is now the only
# thing that starts this at boot -- only as the no-persisted-mode
# fallback -- instead of both it and the current mode service racing
# multi-user.target with Conflicts= picking whichever happens to win
# (dpx#12, confirmed nondeterministic on hardware). The [Install] block
# stays so `systemctl enable dpx-deck-splash.service` still works for
# anyone who wants the old always-auto-start behavior back.
echo "==> dpx-deck-splash.service: installed (started via dpx-mode-select.service, not auto-enabled)"

# ── Recovery: bring the splash back if a mode service dies for good ────────
# OnFailure= only fires when a unit's ActiveState actually reaches
# "failed" -- with Restart=on-failure, systemd holds the unit in
# "activating (auto-restart)" between individual retry attempts, and
# only lands in "failed" once StartLimitBurst is exhausted. So this
# fires once per real, permanent outage, not once per transient restart
# (dpx#11 -- "what's not clear is when the splash comes back"). Purely
# event-driven, no polling loop.
#
# Drop-ins, not edits to the vendor unit files themselves -- all three
# mode services ship from their own .deb packages (Buttons/Satellite/
# Companion), not this repo, and a drop-in survives a package upgrade
# that a direct edit wouldn't.
for MODE_UNIT in bitfocus-buttons-usb-relay.service satellite.service companion.service; do
mkdir -p "/etc/systemd/system/${MODE_UNIT}.d"
cat > "/etc/systemd/system/${MODE_UNIT}.d/dpx-recovery.conf" << 'UNIT'
[Unit]
OnFailure=dpx-deck-splash.service
UNIT
done
echo "==> OnFailure=dpx-deck-splash.service drop-ins installed for all 3 mode services"

# ── Boot-time mode selection: exactly one of {persisted mode, splash} ──────
# The other half of dpx#12/dpx#11: decide once, at boot, which single
# thing should run instead of leaving it to a Conflicts= race. Reads
# /etc/dpx-mode (same file switch_mode() in dpx-buttonode-ui.py writes)
# and starts that mode's service; falls back to the splash if nothing's
# persisted or the target service refuses to start. Mirrors
# get_dpx_mode()'s own "buttons" default for consistency.
cat > /usr/local/bin/dpx-mode-select.sh << 'SCRIPT'
#!/usr/bin/env bash
set -u
# Existence-based toggle, same convention as /var/lib/dpx-hostname-set --
# absent (the default on a fresh image) means autostart is ON, matching
# the web UI's Mode tab checkbox defaulting to checked.
if [ -e /var/lib/dpx-mode-autostart-disabled ]; then
systemctl start dpx-deck-splash.service
exit 0
fi
MODE="$(cat /etc/dpx-mode 2>/dev/null || echo "buttons")"
case "$MODE" in
buttons) SVC="bitfocus-buttons-usb-relay.service" ;;
satellite) SVC="satellite.service" ;;
companion) SVC="companion.service" ;;
*) SVC="bitfocus-buttons-usb-relay.service" ;;
esac
systemctl start "$SVC" || systemctl start dpx-deck-splash.service
SCRIPT
chmod +x /usr/local/bin/dpx-mode-select.sh

cat > /etc/systemd/system/dpx-mode-select.service << 'UNIT'
[Unit]
Description=Start the persisted dpx-buttonode mode (fallback: deck splash)
Documentation=https://github.com/dubpixel/dpx_buttonode
After=dpx-set-hostname.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/dpx-mode-select.sh

[Install]
WantedBy=multi-user.target
UNIT

systemctl enable dpx-mode-select.service
echo "==> dpx-mode-select.service: enabled"

# ── sudoers: the ONLY door from dpx-splash (buttons group, nothing else)
# to actually changing system state ─────────────────────────────────────────
Expand Down
Loading
Loading