Release acceptance on a Raspberry Pi¶
The release checklist: what a release has to show beyond the checks every
PR already passes, the order it is cut in, what it has to show on real
Raspberry Pi hardware before it is tagged, and how the result is recorded. The release workflow proves the
package on Ubuntu VMs and Debian containers (Packaging → Supported
hosts); none of that is a Pi. The kernel, the
Wi-Fi driver, the first hop, boot ordering against docker.service, and
whether the measurements are still right are only shown here.
Until there is a second Pi registered as a self-hosted runner, this is a manual gate on the reference Pi — which is also the production monitor. That is a known compromise: the roadmap asks for the candidate to be isolated from the reference monitor's data, and this procedure gets there only through the backup and the rollback path below. Do not skip them.
What a release proves that a PR does not¶
Every PR already runs every check that can run on a PR, and they are required: lint, shell syntax, the module and CLI tests, the three editions' Compose, Grafana provisioning, the docs build, the apt repository and the Homebrew formula, all nine images built for arm64 and amd64, and CodeQL (CONTRIBUTING). Nothing is moved from a PR to the release to save time. A release adds only what a release alone can show:
| Only a release shows | Where |
|---|---|
| The nine images published to GHCR as one multi-arch manifest each, from the tagged commit | release.yml: build, merge |
The .deb built from the tagged tree installs with each supported host's own apt |
release.yml: package, debian (Debian 12/13 containers) |
| It installs, starts Basic with those images, and upgrades from the previous release keeping its secrets | release.yml: host (Ubuntu VMs) |
Its data survives: backup, purge --config and restore onto a card with no edition recorded bring back the same secrets, config and data; apt purge and a reinstall bring back the same stack |
release.yml: host (check-package.sh, steps 6b and 8) |
| It comes back by itself when Docker is restarted, or stopped and started (a Docker package upgrade), under the enabled unit | release.yml: host (check-package.sh, step 6c) |
| It works on a Raspberry Pi: the kernel, the Wi-Fi driver, the first hop, boot order, the measurements | this checklist, on the reference Pi |
| It stays up: 24 hours with no restart and no unexplained gap | this checklist, Stability |
latest points at it — only after every install test passed |
release.yml: promote |
| What was shipped, tied to the commit: digests, package checksum, the run | the evidence file attach puts on the release |
Releasing, step by step¶
A release is cut from a candidate: a vX.Y.Z-rc.N tag runs the
whole release workflow and produces exactly the artifacts the release will
ship, so the Pi is tested on those, not on a clone. The final tag goes on
the same commit.
- Prepare. A
release/vX.Y.Zbranch converts[Unreleased]into the dated section (with its intro), bumpsversionanddate-releasedinCITATION.cff, andversionand the image tag inpackages[0].identifierinserver.json(CI fails the PR if either disagrees withCITATION.cff). PR, CI green, review, merge. The date is the day the candidate is cut: the release is that same commit, so it cannot change later. - Tag the candidate on that merge commit:
git tag -a vX.Y.Z-rc.1 -m "vX.Y.Z candidate 1" && git push origin vX.Y.Z-rc.1, thengh release create vX.Y.Z-rc.1 --prerelease --verify-tag --title "vX.Y.Z-rc.1" --notes "Candidate for vX.Y.Z.". It must be a pre-release: the apt repository and the upgrade test skip pre-releases, andattachrefuses a candidate on anything else. - Watch the Release run. Every
hostanddebianjob green;attachputs the package andsmoking-pi_X.Y.Z-rc.1_evidence.mdon the pre-release. The package's version isX.Y.Z~rc.1(so apt sorts it beforeX.Y.Z), but GitHub replaces~in an asset name with.: the file to download issmoking-pi_X.Y.Z.rc.1_all.deb.latestdoes not move and the site is not redeployed for a candidate. - Accept it on the Pi, below, on the candidate's own artifacts. A
package install:
sudo apt install ./smoking-pi_X.Y.Z.rc.1_all.deb(apt reads the version from inside the file, not its name, so it still installsX.Y.Z~rc.1), thensudo smoking-pi upgrade. A clone (the reference Pi):git checkout vX.Y.Z-rc.1, thensmoking-pi upgrade, which pulls the published images instead of building: a clone on a release tag runs that tag's images (smoking-pi pathsshows which), with no variable to keep set. Start the 24-hour stability clock. - Anything found is fixed through a normal PR; tag
vX.Y.Z-rc.2on the new merge and start again from step 3. Candidates are never deleted: they are the record of what was tried. - Tag the release on the accepted candidate's commit:
git tag -a vX.Y.Z <sha of vX.Y.Z-rc.N> -m "vX.Y.Z", push,gh release create vX.Y.Z --verify-tagwith the notes and the Validation section below. The run rebuilds from the same commit, runs every install test again, moveslatest, and attaches the.deband the evidence; the docs site and/aptfollow (docs.yml). - After: the Homebrew bump (
packaging/homebrew/bump.sh vX.Y.Z, a PR), the roadmap's release row, the posts.
Before¶
- [ ] The candidate is a
vX.Y.Z-rc.Ntag on a mergedmain(every PR reviewed, CI green) whose Release run is green, with[Unreleased]converted into the dated section andCITATION.cffandserver.jsonbumped. - [ ]
smoking-pi backupcompleted and the directory is somewhere other than the Pi's SD card. Note its path in the record. - [ ]
shared/scripts/acceptance-record.sh --tag X.Y.Z-rc.Non the Pi, from the live checkout (SMOKING_PI_VERSIONor an exact git tag also name the candidate), or/opt/smoking-pi/shared/scripts/acceptance-record.shon a package install, where the package's version names it and the commit is the one in the release's evidence file. It prints the Validation block below with the tag, commit, Pi model, OS, Debian release, kernel and architecture filled in, plus each container's image, restart count and uptime and the doctor's summary. It also names any container that is not on the candidate's image tag. The rest stays<fill>. Run it again at the end of the 24 hours for the Stability line. - [ ] The rollback is one command away: the previous tag checked out and
smoking-pi upgrade, or the backup restored. Write down which.
Clean install and upgrade¶
The reference Pi is always an upgrade; a clean install is proven by the
release workflow's host jobs (Ubuntu VMs) and, on Raspberry Pi OS, by the
debian containers at the package level only. Once a spare Pi exists, a
clean install there is the missing half — until then the record says
"clean install on Raspberry Pi OS: untested".
- [ ] The candidate's own artifacts (Releasing, step 4) and
smoking-pi upgrade— with a version set, that iscompose pullthenup -d --remove-orphans, the doctor--liveat the end. Check every container runs a:X.Y.Z-rc.Nimage (docker compose images): a:devone was built, not the candidate. Note the wall time and whether any container restarted more than once (docker compose ps,docker compose logs --since 10m | grep -i error). - [ ] Configuration and credentials survived:
smoking-pi passwords --show-secretsshows the same values as before, the target list in the web admin is intact, Grafana's dashboards still show history older than the upgrade.
Startup and recovery¶
- [ ]
sudo reboot. Within five minutes:systemctl status smoking-pi(a package) ordocker compose ps— every service that was up before is up, none restarting.docker compose logs --since 10mhas no repeated errors. - [ ] Docker's own start order held: the stack came up after
docker.serviceand the network (journalctl -b -u smoking-pi, or the first lines of each container's log).
Service health and data¶
- [ ]
doctor --livereports0 fail(and says why for every warn): every deployed.pymatches the tree, the panels' measurements are being written, the alerter's defaults match. - [ ] Data is arriving: in Grafana, the latency panels have points in the last five minutes for every probe the edition enables (ICMP, DNS, HTTP/1.1–3, TCP, the CPE hop, the Wi-Fi link — Measuring).
- [ ] The web admin (Standard/Pro) lists the targets and can toggle one;
the change appears in
smokepingwithin a probe cycle and is toggled back. - [ ] Wi-Fi stats (Wi-Fi uplink stats): the panel shows signal and rate for the uplink, and the radio has not hung since boot (When the radio hangs).
Detections and integrations¶
- [ ] Downtime and microcut detection:
get_loss_eventsandget_microcut_statsover the last 24 h return without error and their classifications agree with the alerter's rules (Detection reliability). If the day had no event, say so — do not fabricate one. - [ ] Every profile the Pi runs answers: the MCP server (
get_chart, one question through OpenClaw), the alerter (NOTIFY_MODEas configured, a dry-run notification if the mode allows), AI insights if enabled. - [ ]
smoking-pi openclaw(withsudoon a package install) completes all five steps against the OpenClaw version the host really runs, andopenclaw mcp probe smokepinglists the tools. The tests stubopenclaw, so only a real one catches a change in its configuration schema (2.15.0 shipped a registration OpenClaw 2026.8 refused). Record the OpenClaw version. - [ ] The DNS observer (
dnsprofile): send a few hundred queries through it from another machine. After the next wizard pass (10 minutes), the DNS Wizard's Queries (last hour) includes them although AdGuard, which writes its log every 1,000 queries, still holds them in memory. Then send more than 1,000, which makes AdGuard write them, and check that thecountfield ofdns_resolutionincludes them. Names SmokePing measures and the Pi's own traffic are left out by design, so count what should remain. One data point is not a check.
Editions and backends¶
The reference Pi runs one combination (Pro, InfluxDB, alerts,mcp
profiles). Everything else is untested on a Pi unless the record says
otherwise: Basic and Standard, ClickHouse, the ai profile. The release
workflow starts Basic on Ubuntu; Standard and Pro are rendered on every
host but started only here. Write the combinations not exercised into
the record — the reader should never infer coverage.
Stability¶
- [ ] The candidate stays on the reference Pi for at least 24 hours
before the tag, through at least one full day-night cycle of Wi-Fi
conditions. Acceptance: no container restarted on its own (
docker inspect --format '{{.RestartCount}}'is 0 for all), the doctor still passes, the panels have no gaps that the loss events do not explain. - [ ] Anything found is fixed on a branch and the clock restarts; the record lists what was found.
Rollback¶
If any item fails and is not a documentation error: git checkout
<previous tag> && smoking-pi upgrade (a clone), or apt install of the
previous .deb, then smoking-pi restore <backup> only if the data was
touched. Record what failed, and the release waits.
The record¶
Every release gets a Validation section in its GitHub release notes (and a row in the roadmap's release table), filled from the checklist:
## Validation
- Tag / commit: vX.Y.Z / <sha> — accepted as vX.Y.Z-rc.N (same commit)
- Artifacts: images ghcr.io/estcarisimo/smoking-pi/*:X.Y.Z, smoking-pi_X.Y.Z_all.deb; digests and checksum in smoking-pi_X.Y.Z_evidence.md (release assets)
- Release workflow: <run URL> — host 5/5, debian 4/4, upgrade from vX.Y.(Z-1): pass | none yet; candidate run: <run URL>
- Reference Pi: <model>, Raspberry Pi OS <version> (Debian N), kernel <uname -r>, arm64
- Upgrade on the Pi from vX.Y.(Z-1): pass — <minutes>, restarts 0; credentials and targets intact
- Reboot recovery: pass — up in <minutes>
- doctor --live: <n> ok, <n> warn (why), 0 fail
- Data: all enabled probes writing; Wi-Fi panel live; no radio hang since boot
- Detections: get_loss_events / get_microcut_stats agree with the alerter; events that day: <what>
- Stability: <hours> on the Pi, restarts 0
- Untested this release: clean install on Raspberry Pi OS; Basic/Standard on a Pi; ClickHouse; ai profile
- Known limitations: <anything found and deferred, with the issue link>
- Backup used: <path>; rollback path: <which>
"Untested" is a valid entry. A release with an honest untested list is acceptable; one with an implied coverage is not.