Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Tags: gershwin-desktop/gershwin-desktop

Tags

nextbsd-rc

Toggle nextbsd-rc's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Defer plymouth splash setup to install-plymouth.sh in gershwin-system (

…#42)

plymouth theme setup in the arch/debian/devuan image build steps is now
handled by install-plymouth.sh (invoked from SystemPrepare.sh), which
detects the distribution and installs/configures plymouth automatically.
Package lists are left untouched.

nextbsd-dev

Toggle nextbsd-dev's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

freebsd-rc

Toggle freebsd-rc's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

freebsd-dev

Toggle freebsd-dev's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

devuan-rc

Toggle devuan-rc's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

devuan-dev

Toggle devuan-dev's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

debian-rc

Toggle debian-rc's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

debian-dev

Toggle debian-dev's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

archlinux-rc

Toggle archlinux-rc's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

archlinux-dev

Toggle archlinux-dev's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Build all five Gershwin live ISOs from one tree: rc/dev channels, sha…

…red boot+login+screenshot gate, rolling releases (#32)

* monorepo: add nextbsd as targets/nextbsd/ (reference flavor)

Import the gershwin-on-nextbsd build sources unchanged — build.sh,
pkglist.txt, launchd overlays, and the boot/login/screenshot test
scripts — under the per-flavor targets/<flavor>/ layout from the
consolidation plan. build.sh self-resolves pkglist.txt/overlays via
$(dirname $0), so the move needs no edits to the script.

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

* ci: monorepo skeleton — shared actions + continuous-nextbsd caller

Shared composite actions (.github/actions/):
  - free-disk: reclaim host disk for a ~2 GiB ISO (every flavor's build)
  - publish-continuous: gh release delete --cleanup-tag + create --prerelease
    + attach ISO/sha256/screenshot. The --cleanup-tag is the fix for the
    uploadtool tag-leak: the rolling tag holds exactly one build.

continuous-nextbsd.yml wires the reference flavor through the shared tail:
changes -> build (FreeBSD VM, inline) -> boot/login/screenshot gate -> publish.

Behavior changes vs the standalone gershwin-on-nextbsd pipeline:
  - rolling tag continuous -> continuous-nextbsd (one tag per flavor; five
    flavors will share this repo's releases)
  - desktop screenshot published as the release asset Gershwin-nextbsd-desktop.png
    instead of committed to docs/desktop.png (no binary churn in git, plan §8)

The screenshot gate stays inline (launchd/greeter-specific); it factors into a
shared screenshot-gate action once a second flavor needs it.

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

* ci: make the screenshot gate shared + init-agnostic; fixups

The boot -> greeter -> admin login -> desktop-render gate never depended on
launchd: it observes the Gershwin greeter/desktop from OUTSIDE the guest via
the QEMU monitor, and that UI (LoginWindow greeter, admin/no-password login,
GNUstep Workspace) is identical on every flavor. launchd only lives in the
overlay (how services start at boot), not in the gate. So factor it now:

  - .github/actions/screenshot-gate/ owns the whole QEMU flow (install, launch,
    boot/login/desktop scripts, shutdown, artifacts). Keyed off the greeter
    alone; works for any init system. continuous-nextbsd's test job collapses
    to a single .

Also in this commit:
  - fix build: gershwin-developer renamed its scripts to lowercase
    (Bootstrap.sh/Checkout.sh -> bootstrap.sh/checkout.sh). The old names broke
    the build with 'cannot open Library/Scripts/Bootstrap.sh'. This also breaks
    the standalone gershwin-on-nextbsd repo on its next run.
  - screenshot asset name -> gershwin-on-<flavor>.png (e.g. gershwin-on-nextbsd.png)

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

* gate: require BOTH 'System Disk' icon and 'Workspace' menu via OCR

The desktop-render check now passes only when a single frame OCRs both desktop
landmarks — the 'System Disk' icon label and the 'Workspace' global menu — so a
green build proves the session reached a real, usable desktop. Colour count
demoted to a log-only progress signal: a login that bounces back to the greeter
is also high-colour, so it was never a trustworthy gate on its own. Frames are
upscaled 2x before OCR to read the small menu-bar text reliably; on failure the
last OCR text and which landmarks were ever seen are logged for diagnosis.

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

* gate: full keyboard-driven desktop sequence ending on About This Computer

Replace the desktop-render check with the interactive sequence, all driven from
outside the guest over the QEMU monitor (screendump + sendkey, no mouse, nothing
in-guest). Every step is a hard gate that blocks publish on failure:

  1. desktop renders  — OCR 'System Disk' AND 'Workspace'
  2. discover Command — probe sendkey modifiers until Cmd+R opens the Run dialog
                        ('Type the command to execute:')
  3. close everything — Cmd+W x5
  4. open About       — Cmd+R, type 'uitest about', Enter
  5. About is up      — OCR 'About This Computer'
  6. capture          — that frame becomes docs/desktop.png, the ONLY published
                        screenshot (About This Computer over the desktop)

The Command modifier and whether uitest needs Workspace -d can only be confirmed
on a real boot, so step 2 auto-probes the modifier (GATE_CMD_MODS overrides the
candidate list) and every failure prints a precise reason: a modifier that never
opens Run (keymap issue) vs. Run opening but no About window (uitest/-d issue).
All probe frames upload with boot-artifacts for inspection.

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

* gate: name the screenshot gershwin-on-<flavor>.png from the caller's flavor

The screenshot was written to a scratch docs/desktop.png (a leftover from the
standalone repo's committed-README image) and renamed at publish time. Instead,
thread the flavor from the per-flavor caller workflow into the shared gate and
name the capture gershwin-on-<flavor>.png from the start:

  - screenshot-gate gains a required 'flavor' input; the caller passes it
    (continuous-nextbsd.yml -> flavor: nextbsd; a future continuous-devuan.yml ->
    devuan). The gate writes screenshot/gershwin-on-<flavor>.png.
  - publish-continuous drops its rename + now-unused 'flavor' input and attaches
    the already-named file by basename.

Same name end-to-end, sourced once from the workflow; no more docs/desktop.png.

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

* nextbsd: resync build.sh/pkglist/overlay from upstream (stale 07-04 import)

The imported nextbsd sources came from a local clone stuck at 2026-07-04, so they
missed two-plus weeks of upstream fixes. Most visibly this made the ISO ~3.0 GiB
and the build ~15 min faster than upstream's 1.6 GiB — because our stale build.sh
ran mkuzip's DEFAULT zlib instead of the tuned zstd upstream added. Resync to
gershwin-on-nextbsd@main:

  build.sh:
    - mkuzip -A zstd -C 19 -s 1044480 -d -S  (was: mkuzip default zlib)
      -> ~1.6 GiB not ~3.0 GiB; back under GitHub's 2 GiB release-asset cap
    - pkg clean -ay before makefs (drop cached tarballs from the image)
    - clone gershwin-developer to /Developer and KEEP it in the rootfs
  pkglist.txt:
    - + mesa-dri (without it: 'GLX: no usable GL providers' — no desktop GL)
    - + sudo
  overlays/.../org.freedesktop.dbus-system.plist:
    - wrap dbus-daemon so it recreates /var/run/dbus + machine-id at launch
      (/var is repopulated fresh each live boot; build-time seed doesn't survive)

build.sh already had the lowercase bootstrap.sh/checkout.sh upstream carries, so
the resync is a clean whole-file match (no local delta to preserve).

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

* gate: fix System Disk OCR (psm 3, not 11); add local ocr-check validator

The e9317dc run reached a fully-rendered desktop — Workspace menu read fine, but
'System Disk' (the top-right icon label) was never detected, failing stage 1.
Root cause: ocr_corner used tesseract --psm 11 (sparse), which mangles that
label. Verified against the real captured frame that the default psm 3 reads
'System Disk' cleanly off the same 42% corner crop. Drop --psm 11.

Also add ocr-check.sh, a local dev validator that runs all four gate OCR
detections (System Disk, Workspace, About This Computer, Run dialog) against a
downloaded frame, so detection can be tuned offline without a ~22-min CI build.
Confirmed on the failed run's frame: System Disk + Workspace now detect; About /
Run correctly absent (not on a plain desktop frame).

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

* gate: Run… is Cmd+SHIFT+R, not Cmd+R — add Shift to the modifier probe

Stage 1 (System Disk + Workspace) now passes on the runner — the psm-3 fix
worked. Stage 2 (modifier probe) failed because Workspace.m sets the Run… menu
item's mask to NSCommandKeyMask|NSShiftKeyMask + keyEquivalent "R": the chord
is Cmd+SHIFT+R. The probe was sending mod+r with no Shift (confirmed by the
run-meta_l.ppm frame: plain desktop, no dialog). Send mod+shift+r, remember the
winning chord as RUN_KEYS, and reuse it to open Run… in stage 4. Also probe
altgr, and log each attempt's OCR so a missed dialog is visible.

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

* screenshot-gate: run 'uitest aboutcomputer' instead of 'uitest about'

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

* screenshot-gate: park pointer in corner before capturing the About screenshot

The published About-This-Computer frame is used as the release tag's hero
image; the PS/2 pointer was landing over the centred window. Slam it into
the bottom-right corner over the QEMU monitor before the capture loop.

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

* publish-continuous: drop live ${{ secrets }} expr from input description

A composite action manifest evaluates every ${{ }} it finds, including
inside an input's description text, and the 'secrets' context does not
exist there — so the placeholder in the token input's description made the
whole action fail to load (Unrecognized named-value: 'secrets'). Reword to
plain text; the token is still passed via inputs.token by the caller.

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

* screenshot-gate: fix System Disk corner OCR (crop below menu bar, psm 6)

The desktop gate's step-1 corner OCR read the menu bar's CPU/RAM/clock text
and missed the tiny 'System Disk' volume label, failing every frame even
though the desktop rendered correctly. The crop started at the top edge, so
tesseract's default page segmentation locked onto the big menu-bar text.

Push the crop below the menu bar (+0+28), shrink its height (14%), and use
--psm 6 (single uniform block). Verified against the real captured frames
from run 29783111264: reads 'System Disk' on every frame.

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

* release: canonical ISO naming + preserve-tag rotation, drop sha256

Adopt the cross-flavor artifact convention (matches the debian flavor):
  gershwin-on-<flavor>-<UTC YYYYMMDDhhmmss>-<arch>.iso
- arch token is x86_64 / aarch64 (never amd64 / arm64) — build.sh maps the
  FreeBSD $ARCH to a release label without disturbing the pkg ABI / repo URLs.
- compact 14-digit UTC timestamp, no separators.
- no sha256 sidecars; GitHub shows each asset's digest natively.

publish-continuous now PRESERVES the tag: create-once (no --cleanup-tag),
upload the new ISO first, replace the stable-named screenshot, then delete the
previous ISO(s) and any legacy sidecars — the release is never left without a
downloadable image. Release body embeds the screenshot inline + provenance.

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

* publish-continuous: name screenshot to match the ISO; trim release body

The release screenshot now shares the ISO's stem (same name, .png extension:
gershwin-on-<flavor>-<stamp>-<arch>.png) and rotates the same way — new ISO +
screenshot uploaded first, previous ISO + screenshot pruned after. Drop the
ISO/Commit lines from the body; the ISO name is carried by the image itself.

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

* release: per-flavor title 'Gershwin on NextBSD (continuous)'

The caller passes a proper-cased, flavor-unique release title via the
publish-continuous title: input, and the action now applies it on every
publish (gh release edit --title), not only on first create — so the
releases list reads 'Gershwin on NextBSD (continuous)' instead of the
generic 'Continuous build', and each flavor is distinct at a glance.

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

* ci: always build on any non-README change; drop tests-only fast path

Trigger contract is now: a change to README.md alone never builds; a change
anywhere else does (paths-ignore: README.md). Removed the 'changes' paths-
filter job and the 'download the already-published ISO' fast path — the build
always runs and the gate boots the ISO this run just built, so it can never
try to test an image that doesn't exist (the bootstrap 'release not found'
failure is gone). A plain push to main now builds → gates → publishes; no
separate workflow_dispatch needed.

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

* ci: shorten release title to just the distro name (NextBSD)

GitHub ellipsis-truncates release names in its narrow Releases sidebar and
tag dropdown; a one-word distro name never clips. 'Continuous' is already
conveyed by the continuous-<flavor> tag, so it isn't repeated in the title.
Stays a --prerelease (project is early-stage; all releases are prereleases).

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

* flavor: add freebsd target + continuous-freebsd workflow

Port gershwin-on-freebsd into targets/freebsd/ (build.sh + resources) and wire
the shared free-disk / screenshot-gate / publish-continuous layer, matching the
nextbsd reference. build.sh already followed the naming convention (gershwin-on-
freebsd-<stamp>-<arch>.iso, amd64->x86_64); only the .sha256 sidecar was dropped.
Title 'FreeBSD', tag continuous-freebsd, --prerelease.

Also scope each flavor's triggers to targets/<flavor>/** + .github/** (per the
plan) so a flavor rebuilds only on its own target or shared-infra change, never
on another flavor's edit or a README change.

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

* freebsd: run build.sh from its own dir; tighten per-flavor trigger scoping

build.sh uses CWD=$(pwd) for resources/, so running it from the repo root
looked for resources/ at the root (missing) — run it from targets/freebsd via
a subshell; the ISO output dir is absolute so the copy still works.

Scope each flavor to targets/<flavor>/** + .github/actions/** + its own
workflow file, so editing one flavor's workflow doesn't rebuild the others
(shared-action changes still rebuild all, by design).

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

* flavor: add devuan target + continuous-devuan workflow (amd64)

First Linux flavor. Ports gershwin-on-devuan (debootstrap + squashfs + grub live
ISO) into targets/devuan/ and runs it directly on ubuntu-latest — no GHCR
container stage — then wires the shared free-disk / screenshot-gate /
publish-continuous layer. Compact UTC ISO stamp; debootstrap runs with the sid
script aliased to excalibur + --no-check-gpg on the non-Devuan host. amd64 only
for now (arm64 later, needs dual-arch publish). Title 'Devuan', --prerelease.

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

* devuan: install devuan-keyring into rootfs during debootstrap

The bootstrap ran with --no-check-gpg but the rootfs then had no Devuan
keyring, so apt-get update inside the chroot failed signature verification
(NO_PUBKEY B3982868D104092C) and every downstream install cascaded to failure.
--include=devuan-keyring installs it during bootstrap so the rootfs trusts
Devuan's repos.

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

* devuan: build inside the Devuan container (the Linux-flavor pattern)

Switch from ubuntu-native debootstrap (which needed cross-distro hacks) to
building the ISO INSIDE the flavor's own Devuan container
(targets/devuan/ci/containers/Dockerfile). build.sh reverts to the plain
upstream debootstrap — the container provides Devuan's debootstrap (excalibur
script) + devuan-keyring natively. This is the pattern every Linux flavor
follows: devuan->devuan, debian->debian, arch->arch.

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

* flavor: add debian target + continuous-debian workflow (amd64)

Ports gershwin-on-debian (live-build) into targets/debian/ and builds it INSIDE
a Debian container (debian:latest, --privileged) — the per-distro container
pattern. lb config + lb build; ISO renamed to the canonical
gershwin-on-debian-<stamp>-<arch>.iso. Shared gate + publish. amd64 only for now.
Title 'Debian', --prerelease.

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

* flavor: add archlinux target + continuous-archlinux workflow (x86_64)

Ports gershwin-on-arch (mkarchiso profile) into targets/archlinux/ and builds it
INSIDE an Arch container (archlinux:latest, --privileged) — the per-distro
container pattern. Output renamed to the canonical
gershwin-on-archlinux-<stamp>-x86_64.iso. Uses the 'archlinux' flavor token
(not 'arch', which collides with the CPU arch). Shared gate + publish.
Title 'Arch Linux', --prerelease.

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

* gate: re-attempt greeter login if it's still up; debian: add xz-utils + --zsync false

Gate (shared): archlinux booted all the way to the Gershwin greeter but the
one-shot login fired before the greeter was interactive (boot-test passed at 66
colors / frame 1), so 'admin' half-landed and never submitted. desktop-test now
re-submits the login — clear field, retype admin, Enter and Tab->Enter — but
ONLY while the greeter's own labels (Log In / Username / Shut Down) are still
OCR-visible, so it can never disturb a desktop that's merely still rendering.

debian: lb build died at binary_zsync (xz not found, exit 127) though the ISO
was already built — add xz-utils and --zsync false (we don't ship .zsync).

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

* chore: remove stray arch-art/ screenshot accidentally committed during debugging

A diagnostic screenshot download landed inside the repo clone and got swept
into a commit by git add -A. Not a project file — remove it. (Top-level path,
so no flavor build is triggered.)

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

* rc channel: rename continuous-* workflows to rc-*; plumb GERSHWIN_REF/BRANCH

- Rename continuous-<flavor>.yml -> rc-<flavor>.yml; tag continuous-<flavor> ->
  <flavor>-rc; title '<Display>' -> '<Display> (rc)'.
- Each flavor's build now honours GERSHWIN_REF (gershwin-developer clone ref,
  default main) and GERSHWIN_BRANCH (source-repo branch passed to checkout.sh,
  default empty). Unset = byte-for-byte the previous behaviour, so rc is
  unchanged; the dev workflows (next) set these to build the dev channel.

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

* dev channel: add dev-<flavor> workflows building the dev branches

Each dev-<flavor>.yml resolves gershwin-developer's ref (dev if the branch
exists, else main) and passes GERSHWIN_REF + GERSHWIN_BRANCH=dev into the build,
so both the tooling repo and the source repos prefer dev with per-repo fallback
(via checkout.sh). Publishes to tag <flavor>-dev, title '<Display> (dev)',
--prerelease. Injection per flavor: env exports (nextbsd/freebsd vmactions),
docker -e (devuan), or prepend to the chroot install script (debian/arch).
Until dev branches exist, everything falls back to default = same as rc.

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

* artifacts: end-of-run purge + 1-day backstop; ISO/PNG names carry the channel

Storage: each workflow now has a cleanup job that deletes the run's ISO +
screenshot artifacts once publish has consumed them (needs [build,test,publish],
if: always, actions: write) — near-zero steady-state. retention-days: 1 on the
ISO upload as a backstop, and boot-artifacts upload only on failure (retention 7d).
The ISO/PNG's permanent home is the release; the artifacts are pure job-handoff.

Naming: ISO name now infixes the release channel — gershwin-on-<flavor>-<rc|dev>-
<stamp>-<arch>.iso — via a CHANNEL env (build.sh for nextbsd/freebsd/devuan, the
rename step for debian/arch). The screenshot follows automatically (publish names
it after the ISO stem). CHANNEL unset = no infix (backward compatible).

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

* docs: add flavor-contributor guide + Claude skill; link from README

- docs/ADDING-A-FLAVOR.md: manual, no-tooling guide for adding a new base-OS
  flavor (per-distro container pattern, target layout, rc/dev workflows, the
  gate contract, naming conventions, artifact hygiene, a worked Artix example).
- .claude/skills/add-iso-flavor/SKILL.md: Claude Code skill that points at the
  guide and adds the operational checklist + verify loop (re-run flakes incl.
  gdomap race #98, confirm channel-named release + artifact cleanup).
- README.md: new 'Building the live ISOs' section linking the guide.

Docs/skill/README aren't in any workflow's paths, so this triggers no builds.

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

* gate: re-verify the menu bar is present at capture time (not just at step 1)

The menu ('Workspace') was only checked in step 1; it can crash/vanish between
then and the final screenshot, so a menu-less desktop could pass the gate and
get published (observed on freebsd-dev and nextbsd-dev — About This Computer up
but no menu bar at all). Step 5 now requires BOTH 'About This Computer' and
'Workspace' in the same captured frame, with a distinct FAIL(5) when the menu
disappeared after login. Ref gershwin-desktop/gershwin-components#98.

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

* gate: soft-fail mode for the dev channel — publish anyway, annotate failures in red

dev builds should ship while we investigate flaky desktop problems (menu race,
gdomap #98), not be blocked by them. Add a 'soft-fail' input to screenshot-gate
(dev workflows set it; rc stays hard-gated). In soft mode boot-test and
desktop-test RECORD failures instead of exiting non-zero, always reach the
capture step, and stamp the failed checks onto the published screenshot in red
under the About This Computer window ('GATE FAILED (dev, not blocking): …'),
then exit 0 so the dev ISO still publishes. rc is unchanged: the first failure
still blocks the release. Adds fonts-dejavu-core for the annotation.

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

* publish: fix asset accumulation + put gate failures in the release body (not the image)

Accumulation: the ISO upload occasionally 422'd (already_exists after re-runs);
under set -e that aborted publish BEFORE the prune, so old assets never got
deleted and each failed run left an orphan PNG (observed on freebsd-dev: 1 ISO,
3 PNGs). Now: clear the target names first, upload with a retry (failure-tolerant),
then prune by reconciling against what ACTUALLY landed — only pruning others once
the new ISO is confirmed up; if the ISO never lands, drop the orphan screenshot,
keep the previous release, and fail loudly.

Failure display: stop baking the red banner into the screenshot. The gate writes
screenshot/gate-failures.txt (bundled into the desktop-screenshot artifact); the
publish step renders it as a red diff block UNDER the inline screenshot, ABOVE
the build-log link. Dropped the now-unused fonts-dejavu-core.

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

* targets/{freebsd,nextbsd}: log exact per-package + per-dir size report before mkuzip

The pkg install logs only show aggregate download/install sizes, so we can't
attribute ISO inflation to a specific package from them. Print an exact report
on the final rootfs just before makefs: every installed package by flatsize
(pkg query %sb) plus the biggest shipped directories (du), so a build log tells
us precisely what crosses the 2 GiB GitHub asset cap.

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

* targets/{freebsd,nextbsd}: break size report down inside /usr/local/share and /usr/local/lib

Pin down exactly which data files dominate /usr/local/share (printer DB,
speech/locale data, docs) vs code in /usr/local/lib (python stdlib, numpy,
libLLVM, boost), so we know what is safe to trim.

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

* add iso-diff: mount an old published rc ISO in a FreeBSD VM and du it

Diagnostic-only (workflow_dispatch). The old, smaller published rc ISOs are still
attached to the *-rc tags (failed publishes never pruned them), so we can mount
their compressed rootfs and run the same du report the build emits — diffing
old-vs-new at directory granularity to see exactly what inflated past 2 GiB.

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

* artifacts: keep run ISOs — drop the cleanup job and the 1-day retention

Reverts the artifact-suppression half of c3db992. The ISO artifact is how you
retrieve a build that exceeds the 2 GB release-asset limit and how you diff two
builds byte-for-byte, so it has to survive the run:

- remove the `cleanup` job from all 10 rc-*/dev-* workflows (it deleted every
  non-boot-artifacts artifact via gh api -X DELETE, if: always(), so artifacts
  were purged even when publish failed on an oversized ISO),
- drop `retention-days: 1` from each ISO upload — inherit the repo default,
- restore `boot-artifacts` to `if: always()` with no retention override.

The channel-naming half of c3db992 (the CHANNEL env var and the rc/dev infix in
the ISO name) is deliberately kept — iso-diff and the release tags depend on it.

Docs updated to match: ADDING-A-FLAVOR.md and the add-iso-flavor skill both told
contributors to copy the cleanup job verbatim into every new flavor.

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

* freebsd,nextbsd: drop /var/db/pkg/repos before makefs (-3.3 GiB, -446 MiB ISO)

The rc-freebsd ISO went 1.84 GiB (Jul 22) -> 2.3 GiB (Jul 25) with an identical
package set: both images ship the same 675 packages, and total flatsize moved
only 5669.3 -> 5694.6 MiB. Nothing in targets/freebsd changed in that window
either. The growth is entirely /var:

    var/db/pkg/repos/FreeBSD_pkg/db      71.1 MiB -> 1659.5 MiB
    var/db/pkg/repos/FreeBSD-ports/db    71.1 MiB -> 1659.5 MiB
    /var total                          189   MiB -> 3367   MiB

These are the remote repository catalogues fetched by pkg. REPO_BRANCH=latest
is a rolling repo, so the upstream catalogue grew ~23x on its own and we baked
it in. 3.2 GiB of SQLite metadata compresses ~7:1, which is exactly the +446 MiB
seen in the uzip (1600 -> 2046 MiB) and what pushed the ISO past the 2 GB
release-asset limit.

`pkg clean -a` (already run in both targets) does not help: pkg-clean(8) is
scoped to PKG_CACHEDIR (/var/cache/pkg -- 4 MiB in both images), while the
catalogues live under PKG_DBDIR. There is no pkg subcommand for this, and
PKG_DBDIR cannot be relocated because local.sqlite (the installed-package
registry, which must ship) lives there too. So: remove repos/ explicitly.

Safe: build.sh only ever mkdir's /var/db/pkg, never repos/ -- pkg creates that
subtree itself, and rebuilds it on the installed system on the first update /
install / search (REPO_AUTOUPDATE defaults to YES).

Diagnosed by decoding both ISOs' rootfs.uzip (mkuzip #z4.0 zstd, -d dedup) and
walking the UFS images offline.

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

* artifacts: restore the end-of-run purge; drop the iso-diff workflow

Reverts 40993ce now that the size regression is diagnosed and fixed (af3d61a):
the rc-freebsd ISO is back to 1.82 GiB and publishing again, so run artifacts
are once more pure job-handoff and the release is the ISO's permanent home.
Restores the cleanup job in all 10 rc-*/dev-* workflows, retention-days: 1 on
the ISO upload, and boot-artifacts on failure only -- plus the contributor docs
that describe them.

iso-diff was a one-off used to size-compare a published rc ISO inside a FreeBSD
VM; the per-package/per-dir size report that every build now prints covers it,
so the workflow goes rather than linger in the Actions list.

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

* ci: add rc-all / dev-all fan-out workflows

Two workflow_dispatch-only workflows that trigger every flavor build in one
click: rc-all fires all rc-<flavor>, dev-all fires all dev-<flavor>.

They dispatch the existing flavor workflows rather than duplicating their jobs,
so each flavor still reports as its own run with its own artifacts, gate and
release; the fan-out only fires the starting gun. Every flavor workflow already
declares `workflow_dispatch:` with no inputs, so nothing there had to change.

The flavor list is discovered from the Actions API (active workflows whose file
name starts with rc-/dev-, minus the fan-out itself) rather than hardcoded, so a
new flavor joins automatically and there is no list to keep in sync.

GITHUB_TOKEN suffices: events raised with it do not normally start new workflow
runs, but workflow_dispatch and repository_dispatch are the documented
exceptions. Needs `actions: write`, which is declared.

A failed dispatch warns and continues rather than stranding the remaining
flavors; the step exits non-zero if any failed, and writes a per-workflow
result table to the job summary.

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

* ci: pass --repo to gh workflow run in the rc-all/dev-all fan-out

The first dev-all run dispatched nothing: `gh workflow run` resolves the target
repo from the local git remote, and the fan-out job deliberately has no
actions/checkout, so every call died with "fatal: not a git repository" and the
step exited 1 after five warnings.

Discovery was fine -- `gh api` takes a fully qualified path -- so only the
dispatch call needed $GITHUB_REPOSITORY.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>