Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
68 changes: 68 additions & 0 deletions .claude/MEMORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Anthias — seed memory

This is a curated, PII-scrubbed seed of hard-won project knowledge, distilled
from a working agent's memory before its VM was retired. A fresh clone starts
here. It is **not** an auto-memory index — it is a static handoff. Everything
below is also captured, in depth, in the committed skills under
`.claude/skills/`; this file is the map.

> `.claude/` is mostly gitignored (worktrees + unfiltered local memory), but
> `.gitignore` explicitly un-ignores `.claude/MEMORY.md` and `.claude/skills/`
> so this curated handoff tracks normally (no `git add -f` needed) — alongside
> the pre-existing `commit`/`create-pr` skills. Keep them scrubbed: **this is a
> public repo — never add IPs, usernames, emails, device UUIDs, tokens, ngrok
> URLs, or forum handles.**

## Skills (read the one that matches your task)

- **[testbed-qa](skills/testbed-qa/SKILL.md)** — validating changes on the
physical hardware fleet: the `/tmp/used-by` lockfile protocol, docker-compose
(not balena) deploy, the pure-Python overlay method, force-display on headless
eglfs, and per-board memory/hardware quirks.
- **[anthias-hardware](skills/anthias-hardware/SKILL.md)** — per-board display
stack (linuxfb/eglfs/wayland-cage), video HW-vs-SW decode per chip, the Qt6
presentation bottleneck, rotation, WebGL, splash, audio, the Pi 2 armhf SIGBUS.
- **[anthias-viewer](skills/anthias-viewer/SKILL.md)** — viewer/server internals:
server-rendered Django/ASGI + Redis pub/sub, streaming-under-ASGI, WAL playlist
reload, viewer OOM behavior, upload codec gate, Sentry triage, webview C++,
PulseAudio, content-import framework.
- **[anthias-release](skills/anthias-release/SKILL.md)** — balena cloud API +
OS track topology, CalVer stamping + release/OTA sequencing, image builder +
toolchain, CI behavior, Sentry conventions, telemetry (the reference facts).
- **[cut-release](skills/cut-release/SKILL.md)** — the actionable step-by-step
runbook for cutting a tagged release: bump the version across all manifests +
lockfiles, merge, wait for the master Docker build, tag, and publish (auto
notes first, then a curated summary).

## Highest-value cross-cutting facts (the things easiest to get wrong)

- **The repo is server-rendered Django, not React.** UI = templates +
Alpine.js/htmx under `src/anthias_server/app/`; thin TS bundles built by bun.
Everything moved under `src/` (`anthias_common`, `anthias_server`,
`anthias_viewer`, `anthias_webview`, `anthias_host_agent`). See CLAUDE.md.
- **Board → rendering stack is chosen by userspace arch** (`dpkg
--print-architecture`), not `uname -m` — a 32-bit Pi OS ships a 64-bit kernel.
- **Only Qt6 boards (Pi 3-64/4/5, x86, Rock Pi 4) do HW video + WebGL.** Qt5
linuxfb boards (Pi 1/2/3-32) have no GL and use GStreamer→fbdevsink.
- **Rotation is clockwise on every stack** as of 2026.07.3; the mechanism differs
per QPA (linuxfb hand-rotates, eglfs negates, wayland uses wlr transforms).
- **1 GB boards OOM-wedge the latest viewer even idle; 512 MB is unusable** for
webpages. Read "wedge/unresponsive" on a small board as memory pressure first.
- **CI unit tests run on the ephemeral merge SHA**, so head-SHA check-runs look
empty — find them via `gh run list`.
- **CalVer resets MICRO each month**; balena needs no-leading-zero semver in
`balena.yml`; cut the GH release only AFTER the master Docker build is green.

## Working conventions

The durable working conventions (fix-root-cause, no `network_mode: host`, no
`#NNN` in PR bodies, no `noqa`, run `ruff format --check`, the Copilot review
loop, never-break-API, forum-reply tone, US-English website, etc.) live in
**CLAUDE.md → "Working conventions"** so they load every session.

## Rebuilding live auto-memory on a fresh VM

If you use the file-based auto-memory system, treat this file and the skills
under `.claude/skills/` as the source of truth to re-seed from. Do not copy any environment-specific
state (testbed IP↔board mapping, SSH user, tunnel URLs) into the repo — that is
local operator state, kept only on the operator's machine.
121 changes: 121 additions & 0 deletions .claude/skills/anthias-hardware/SKILL.md

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions .claude/skills/anthias-release/SKILL.md

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions .claude/skills/anthias-viewer/SKILL.md

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions .claude/skills/cut-release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
name: cut-release
description: Step-by-step runbook for cutting a tagged Anthias release — bump the CalVer version across all relevant manifests/lockfiles, merge, wait for the master Docker build, tag, and publish a GitHub release (auto-generated notes first, then layered with a curated summary). Use when the task is to ship a new versioned release. For the underlying "why" (balena topology, OTA-only deploys, CI internals) see the anthias-release skill.
---

# Cut an Anthias release

Anthias versions are **CalVer `YYYY.0M.MICRO`** (zero-padded month). This is the
actionable release runbook; the reference facts behind each step live in the
**anthias-release** skill.

> **Sequencing is load-bearing.** Publishing the GitHub release triggers the
> balena disk-image + OTA pipeline, whose preflight verifies the per-board
> container images already exist in GHCR. Those are built by the master-push
> Docker build. So you must **merge the bump, wait for the full master Docker
> build to go green across every board, and only then publish the release.**
> Publishing early fails preflight (a guard catches it — no harm — but the
> deploy jobs skip).

## 0. Pick the version

- CalVer `YYYY.0M.MICRO`. **Check the current month** — a new month resets MICRO
to `0` (the release after `2026.05.2` is `2026.06.0`, not `2026.05.3`).
- Two spellings of the same number, and you need both:
- **zero-padded** `2026.06.0` — for `package.json`, `pyproject.toml`, the git tag.
- **strict semver, no leading zero** `2026.6.0` — for `uv.lock` (SemVer §9;
balena also requires this form, rendered from pyproject at deploy time).

## 1. Bump version metadata (all relevant files)

The release bump touches exactly **three** files (confirmed against every prior
`chore(release)` commit). `bun.lock` carries no root version — do **not** touch it.

1. `package.json` → `"version": "2026.06.0"` (zero-padded)
2. `pyproject.toml` → `version = "2026.06.0"` (zero-padded)
3. `uv.lock` → under the `name = "anthias"` package block, `version = "2026.6.0"`
(**strict semver, no leading zero**). Bump this line directly, or regenerate
with `uv lock` and confirm only the anthias version line changed.

Sanity check before committing — these must be the only version edits:

```bash
git diff -- package.json pyproject.toml uv.lock
grep -n '"version"' package.json; grep -n '^version' pyproject.toml
grep -n -A1 'name = "anthias"' uv.lock
```

## 2. PR → merge to master

- Branch, commit as `chore(release): bump version metadata to <ver>` (the
established convention), open a PR ready-for-review, let CI pass, merge to
`master`. Admin can merge past `REVIEW_REQUIRED` with `gh pr merge --squash
--admin` (`enforce_admins=false` on master).

## 3. Wait for the master Docker build (ALL boards)

- The merge to `master` triggers `docker-build.yaml`, which builds and pushes
`ghcr.io/screenly/anthias-{server,viewer,redis}:<short-hash>-<board>` for every
board. **The 32-bit armhf (Pi 2 / Pi 3) jobs run under QEMU and finish last** —
wait for the whole matrix.
- Track it: `gh run list --workflow=docker-build.yaml --branch master` → watch
the run for the bump commit to conclude success.
- Optional preflight (what the release pipeline checks): for each board,
`docker buildx imagetools inspect ghcr.io/screenly/anthias-server:<short-hash>-<board>`
should resolve (repeat for viewer/redis; include `arm64` for Rock Pi 4).

## 4. Tag + create the GitHub release — auto-generated notes FIRST

Create the release as a **draft** with GitHub's auto-generated notes. Target the
**full** commit SHA (a short hash is rejected as `target_commitish`); draft
avoids triggering the deploy pipeline while you edit.

```bash
SHA=$(git rev-parse origin/master) # full 40-char SHA of the merged bump
gh release create v2026.06.0 \
--draft --generate-notes \
--title 2026.06.0 \
--target "$SHA"
```

`--generate-notes` populates the body with GitHub's PR-based changelog (the
"first round"). The tag `v2026.06.0` is created by this command.

## 5. Layer our curated notes on top (keep the auto-generated list)

Capture the generated notes, prepend a short human summary of the highlights,
and update — do **not** discard the auto-generated changelog:

```bash
gh release view v2026.06.0 --json body -q .body > /tmp/notes.md
# Prepend a curated "## Highlights" section above the generated list,
# then write the combined file back:
gh release edit v2026.06.0 --notes-file /tmp/notes.md
```

Keep the curated summary short; link notable PRs/releases as markdown links.

## 6. Publish → the deploy pipeline runs

```bash
gh release edit v2026.06.0 --draft=false --latest
```

Flipping to published (`release: published`, `startsWith(tag,'v')`) triggers
`build-balena-disk-image.yaml`:

- **`preflight`** re-verifies every `<short-hash>-<board>` image exists in GHCR
(this is why step 3 must be fully green first).
- **`balena-build-images`** builds bootable disk images for fresh flashes.
- **`balena-cloud-deploy`** is the actual fleet OTA (independent of the disk
images — OTA can be green while a disk-image job fails).

## 7. Verify / recover

- Watch the run: `gh run list --workflow=build-balena-disk-image.yaml`.
- **Transient disk-image failures → rerun, don't debug.** `balena os download
--version latest` intermittently dies with `ERR_STREAM_PREMATURE_CLOSE` (a CDN
blip); `gh run rerun <run-id> --failed` fixes it (also re-runs the skipped
`upload-release-assets` + `build-rpi-imager-json`).
- If preflight failed because you published too early, re-trigger after the
master build is green: `gh workflow run build-balena-disk-image.yaml -f
tag=v2026.06.0`.

## Notes

- **Deploying master to fleets WITHOUT a release** is a different flow —
`bin/balena_ota_deploy.sh <board> <ver> <short-hash>` per board (see the
anthias-release skill). That leaves no git tag / GH release / disk image;
cutting the release later is a separate explicit step.
- CI derives the version from the tag (`${TAG#v}`); local deploy greps
`[project].version` from `pyproject.toml`. They agree because CI checks out at
the tag — so keep the tag and pyproject in lockstep.
Loading