Skip to content

feat(balena): unpinner also rolls OS + supervisor updates - #2984

Merged
vpetersson merged 6 commits into
masterfrom
feat/unpinner-os-supervisor-update
Jun 7, 2026
Merged

feat(balena): unpinner also rolls OS + supervisor updates#2984
vpetersson merged 6 commits into
masterfrom
feat/unpinner-os-supervisor-update

Conversation

@vpetersson

Copy link
Copy Markdown
Contributor

Issues Fixed

No tracked issue. Follow-up to #2980 (the hourly unpinner): clearing the app-release pin alone left those devices on whatever old balenaOS / supervisor they were flashed with, so they tracked the latest app but never the OS or supervisor.

Description

Extends bin/balena_unpin_devices.py so the one hourly cloud-API job brings pinned/stale devices fully current, in three independent, dry-run-by-default phases:

  1. unpin (always on, unchanged) — one filtered bulk PATCH per fleet clears is_pinned_on__release.
  2. --os-update — starts a host OS update (resinhup) toward the latest balenaOS for the device type. Replicates the SDK's startOsUpdate: POST https://actions.<deviceUrlsBase>/v2/<uuid>/resinhup (base resolved at runtime from /config, nothing hardcoded). Online-only; skips devices on balenaOS < 2.14.0 (the single-hop HUP floor from balena-hup-action-utils actionsConfig — none of our device types are jetson, so the only constraints that apply are source ≥ 2.14.0 / target ≥ 2.16.0). Bounded to --os-percent (default 5%) of the eligible population per run so the backlog ramps instead of every device downloading + rebooting at once.
  3. --supervisor — points devices at the newest supervisor release for their CPU architecture (PATCH should_be_managed_by__release, resolved via the same query as the SDK's getSupervisorReleasesForCpuArchitecture). Restricted to devices already on the target OS so the supervisor/OS pairing stays within balena's compatibility window — devices still mid-HUP get their matching supervisor from the OS update itself. Tranche-bounded like the OS phase.

Both new phases honour the anthias_keep_pinned opt-out, stay pure cloud-API (no balena CLI — the workflow still runs with just BALENA_TOKEN), and keep the aggregate-only output (no per-device uuids in the world-readable CI logs). The hourly workflow now passes --os-update --supervisor.

bin/balena_fleet_maintenance.py (the CLI roller) does the same OS-update + unpin but can't see the pinned population; this keeps that division of labour and just lets the cloud-API job do the OS + supervisor too.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

Validation: ruff check + ruff format --check clean. Dry-run (no --apply, zero mutations) against the live fleet exercised all three phases end-to-end — e.g. anthias-pi4: os-update -> 2026.1.0: online=857 eligible=230 tranche=12, supervisor -> 17.8.5 (aarch64): behind-and-on-target=627 tranche=32. Verified the /config → actions-host, latest-host-OS, CPU-arch, and supervisor-release queries all return correctly with the production token.

🤖 Generated with Claude Code

vpetersson and others added 3 commits June 2, 2026 16:57
With debug logging removed (#2977), two pre-existing startup warnings
became visible in the viewer logs. Both are fixed here:

- migrate_legacy_paths.sh: `set -euo pipefail` + the bare `${USER}` in
  USER_HOME aborted the script when $USER is unset — which is the case
  in-container (DATA mode, via migrate_in_container_paths.sh). On a
  legacy (screenly→anthias) device that would skip the in-container
  migration entirely. Fall back to `id -un` so it resolves without
  tripping `set -u`.
- AnthiasViewer: the MainWindow ctor called showFullScreen() AND main()
  called window->show(), showing the window twice. Under cage/wayland
  the double surface-commit tripped wlroots' "A configure is scheduled
  for an uninitialized xdg_surface" warning at startup. Show once, in
  main(), after construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
bin/balena_unpin_devices.py cleared the app-release pin but left
unpinned devices on whatever old balenaOS / supervisor they booted.
Add two optional, cloud-API-only phases so the one hourly job brings
the fleet fully current:

- --os-update: start a resinhup toward the latest balenaOS for the
  device type (POST actions/<base>/v2/<uuid>/resinhup, base resolved
  from /config). Online-only, skips OS < 2.14.0 (single-hop HUP floor
  per balena-hup-action-utils), bounded to --os-percent (default 5%)
  of the eligible population per run so the backlog ramps instead of
  stampeding.
- --supervisor: point devices at the newest supervisor release for
  their CPU architecture (PATCH should_be_managed_by__release).
  Restricted to devices already on the target OS so the supervisor/OS
  pairing stays compatible, and tranche-bounded like the OS phase.

Both phases honour the anthias_keep_pinned opt-out, stay dry-run by
default, and keep the aggregate-only public-log output. The hourly
workflow now passes --os-update --supervisor.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vpetersson
vpetersson requested a review from a team as a code owner June 3, 2026 04:24
@vpetersson vpetersson self-assigned this Jun 3, 2026
@vpetersson
vpetersson requested a review from Copilot June 3, 2026 04:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the existing balena “unpinner” job so that pinned/stale devices can be brought fully current via the cloud API: unpin app release (always), optionally trigger balenaOS HUPs in a bounded tranche, and optionally bump supervisors (also tranche-bounded) for devices already on the target OS.

Changes:

  • Added --os-update phase to trigger per-device resinhup actions toward the latest balenaOS for each fleet’s device type (online-only, tranche-limited).
  • Added --supervisor phase to bulk-patch should_be_managed_by__release to the latest supervisor for the device architecture (only when already on the target OS, tranche-limited).
  • Updated the hourly GitHub Actions workflow to run the new OS-update and supervisor phases (still dry-run on manual dispatch by default).

Reviewed changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated no comments.

File Description
bin/balena_unpin_devices.py Adds OS-update and supervisor-refresh phases (cloud API), plus shared version parsing, tranche selection, and updated reporting.
.github/workflows/balena-unpin-devices.yaml Updates the hourly job to invoke the script with --os-update --supervisor (and --apply on schedule).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

vpetersson and others added 3 commits June 3, 2026 04:30
getSupervisorReleasesForCpuArchitecture returns the newest supervisor
for an arch regardless of OS. On a device type frozen on a legacy
balenaOS line that's unsafe: raspberry-pi2 tops out at balenaOS 5.1.x
(devices run supervisor 15.x), and pointing them at 17.x would likely
break them. Only run the supervisor phase when the fleet's target OS is
on the calendar-versioned line (major >= 2025); legacy-OS fleets keep
their OS-matched supervisor.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The device-actions host (actions.balena-devices.com) is behind
Cloudflare, which 403s the default Python-urllib User-Agent as a banned
client signature (error 1010) — so every OS-update POST failed. Send a
descriptive User-Agent on all requests; the resinhup action then
triggers normally (HTTP 202).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The per-fleet line printed the tranche size regardless of how many
resinhup calls actually succeeded. Track started/failed per fleet and
print both, so a few transient busy/offline failures are visible
instead of hidden behind the planned count.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@sonarqubecloud

sonarqubecloud Bot commented Jun 3, 2026

Copy link
Copy Markdown

@vpetersson
vpetersson merged commit fb4770b into master Jun 7, 2026
10 checks passed
@vpetersson vpetersson mentioned this pull request Jun 7, 2026
5 tasks
vpetersson added a commit that referenced this pull request Jun 7, 2026
- CalVer (YYYY.0M.MICRO); still June 2026, micro 1 -> 2
- Ships the Qt 6 video audio fix (#3001) — PulseAudio in the viewer
  container; videos were silent on pi4-64/pi5/x86/arm64 since the
  QtMultimedia migration
- Adds the arm64/Qt6 pi3-64 board and the Rock Pi 4 fleet (#2985)
- Page-load watchdog so a stalled fetch can't freeze the display
  (#3003), Sentry error tracking for the Django services (#3007)
- Redis data persisted to the mounted volume so device identity
  survives recreation (#2983); unpinner also rolls OS + supervisor
  updates (#2984)
- Streamed backup downloads (#3005), 12-hour AM/PM asset times
  (#3002), BuildKit frontend via mirror.gcr.io (#3008)

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants