Skip to main content

cut-release

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.

Source facts

Repository
Screenly/Anthias
Last source activity
August 14, 2026 at 15:43
Detected SKILL.md language
English
Stars
3,691
Forks
727

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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"). **A draft release does NOT create the tag.** GitHub defers tag creation until the release is published, so after this command `git ls-remote --tags upstream refs/tags/v2026.06.0` returns nothing and the release URL is a placeholder (`releases/tag/untagged-<hash>`). That is a feature: nothing is outward-facing yet, so if QA turns up a blocker you can delete the draft without burning a version number or deleting a published tag. The tag appears — at the `--target` SHA — at step 6. Also note the board matrix is 7 boards (`pi2`, `pi3`, `pi3-64`, `pi4-64`, `pi5`, `x86`, `arm64`); there is no `pi1` or 32-bit `pi4` image, so a preflight loop that invents those tags will report false gaps. On the release-asset side only 4 Pi boards get an rpi-imager entry and the 32-bit `pi3` has no per-board `.json` (the imager's "Raspberry Pi 3" entry uses the 64-bit image) — expected, not a gap. ## 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. ## 5b. Burn-in on the testbeds — while the release is still a draft The draft is the QA gate: nothing is outward-facing yet, so a blocker found here costs a deleted draft rather than a published tag and a fleet OTA. Run the soak per **testbed-qa §6** (all supported asset types on every board, shipping configuration, playlist verified *rotating*), then report measured numbers. Soak the newest `<short-hash>` that actually contains device code — a website-only commit produces no images (`docker-build.yaml` excludes `website/**`), and the version-bump commit itself only touches `package.json` / `pyproject.toml` / `uv.lock`, so the preceding code commit is code-identical to the release and is the correct target. ## 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.
View on GitHub