One repo, universal build templates

Collapse gershwin-on-{debian,devuan,archlinux,freebsd,nextbsd} into gershwin-desktop/gershwin-desktop so every flavor builds the same way with the same shared code — one container path for all Linux, one vmactions path for all BSD — and the only per-flavor difference is the overlay. No more archiso, no more live-build. One X server everywhere — XLibre, no Xorg. A screenshot test gates every publish, and a documented template turns "add a new distro" into an ~8-line caller plus one target directory.

DECISION DOC MONOREPO SHARED TEMPLATES COMPOSITE ACTIONS SCREENSHOT GATE POC → PR → REPLACE

Built from a live read of all five ISO repos, the shared gershwin-developer checkout.sh, the current release inventory, and GitHub's 2026 limits. Thesis: the five repos use five different build engines and only one of them (NextBSD) tests before publishing. Unify the build into two shared templates — container for Linux, vmactions for BSD — push everything identical into shared actions, and reduce each flavor to its overlay. The result is DRY, uniformly gated, and turns new variants into a fill-in-the-blanks exercise.

dispatch: build-all──▶ matrix (per flavor)──▶ ──▶ screenshot gate──▶ publish continuous-<flavor>

1Today: five repos, five build systems

The five ISO repos are not near-copies. They split into two substrate families, and within the Linux family they use three different engines. Only NextBSD tests before it publishes.

RepoBuild engine (today)Runner / archGate?PublishISO size
gershwin-on-debianlive-build in debian:latestubuntu-24.04 + -arm (amd64+arm64)Nouploadtool (fork)1.44 GiB + RPi .img
gershwin-on-devuanbuild.sh in GHCR containersubuntu-24.04 + -arm (amd64+arm64)Nouploadtool1.26 GiB tag leaking
gershwin-on-archmkarchiso in archlinux:latestubuntu-latest (x86_64)Nouploadtool1.91 GiB near 2 GiB cap
gershwin-on-freebsdbuild.sh in vmactions/freebsd-vmubuntu-latest → FreeBSD VM (amd64)No*gh release1.84 GiB
gershwin-on-nextbsdbuild.sh in vmactions/freebsd-vmubuntu-latest → FreeBSD VM (amd64)Yesgh release1.65 GiB + screenshot

* freebsd only skips the build on PRs; it has no boot/screenshot verification.

2The thesis: one build method per family, only overlays differ

Replace the three Linux engines with one container build, keep the two BSDs on one vmactions build, and reduce every flavor to the parts that genuinely can't be shared. Precisely:

LayerShared how far?Mechanism
Disk-free · screenshot gate · publishAll 5 flavors, identicalComposite actions (§3)
ISO assembly skeletonAll Linux share one; both BSD share oneTwo family templates (§4, §5)
Live-boot initOne per family (hand-rolled)Shared init (§4)
Distro adapter (bootstrap + pkg cmd)Per flavor — but tiny (~3 funcs)targets/<flavor>/adapter.sh
Overlay (init wiring, X drivers, boot bits)Per flavor — irreducibletargets/<flavor>/overlays/ (§6)
X server + input/video driversAll 5, identical — XLibre, no XorgShared requirement; package source in adapter (§7)
The Gershwin desktop itselfAll 5, already sharedgershwin-developer install (§7)

3Shared layer 1 — composite actions every flavor calls

The parts that are identical no matter how the ISO was made are expressed as composite actions (bundles of steps dropped into any job with uses:). Each flavor writes its unique build inline and calls these for the rest. Fix a bug once → all flavors get it.

SHARED ACTIONS — .github/actions/
free-disk/action.yml            # reclaim ~25 GB on ubuntu-latest for a 2 GiB ISO
screenshot-gate/action.yml      # boot ISO in QEMU+OVMF → colour/OCR check → FAIL job if no desktop
publish-continuous/action.yml   # preserve tag: upload new ISO+screenshot → prune old ISO+screenshot (no sha256)

Two maximally-different flavors, Arch and NextBSD, end in the same three lines:

continuous-archlinux.yml (left) vs continuous-nextbsd.yml (right) — build differs, tails match
steps:                                   steps:
  - uses: actions/checkout@v4               - uses: actions/checkout@v4
  - uses: ./.github/actions/free-disk       - uses: ./.github/actions/free-disk
  - run: ./targets/archlinux/build.sh      - uses: vmactions/freebsd-vm@v1  (build.sh)
  - uses: ./.github/actions/screenshot-gate  - uses: ./.github/actions/screenshot-gate
  - uses: ./.github/actions/publish-continuous- uses: ./.github/actions/publish-continuous

4Shared layer 2a — each Linux flavor builds in its own distro container

Every Linux flavor builds its ISO inside a container of its own distribution, using that distro's native live-media tooling — not a reimplementation. The container is the per-flavor build environment; the only things shared across Linux flavors are the Gershwin install and the gate + publish tail.

FlavorBuild containerNative ISO toolPer-flavor sources under targets/<flavor>/
devuancustomci/containers/Dockerfile (Devuan repo + keyring + Devuan's debootstrap)debootstrap + grub + xorrisobuild.sh, packages.list, grub.cfg, ci/containers/
debianstock debian:latest --privilegedlive-build (lb config + lb build)config/ (hooks, package-lists, includes.chroot), scripts/
archlinuxstock archlinux:latest --privilegedmkarchisoprofiledef.sh, packages.x86_64, airootfs/, efiboot/, syslinux/

5Shared layer 2b — the BSD template (vmactions)

FreeBSD and NextBSD already build the same way; the template just makes it explicit. Both assemble inside vmactions/freebsd-vm with the identical makefsmkuzipmkisoimages.sh toolchain (NextBSD is FreeBSD-derived). FreeBSD gains the screenshot gate for free the moment it calls the template.

6The only real per-flavor difference: overlays fork by init system

"Start LoginWindow / dshelper / gdomap at login" is the same intent everywhere but a different expression per init system. That's why overlays can't be unified — and it's fine, because overlays were never the thing to share.

FlavorInit systemOverlay form (session wiring)Package list
debian, ubuntusystemd.service / .target unitspackages.list
archlinuxsystemd.service unitspackages.list
devuansysvinit / openrc/etc/init.d/ scriptspackages.list
freebsdrc.d/etc/rc.d/ + rc.confresources/packages/
nextbsdlaunchdLaunchDaemons/*.plistpkglist.txt

Each target carries only its own init's overlay under targets/<flavor>/overlays/. Build-method uniformity buys a shared build workflow; it buys nothing on the init wiring, by design.

7What gershwin-developer owns (already shared)

7a · One X server everywhere: XLibre, no Xorg

NextBSD already ships the XLibre server + input/video drivers. Standardizing all five flavors on XLibre — a drop-in X.Org fork — makes the display stack another shared layer: same server, same drivers, same config, on every flavor. XLibre publishes its own binary repos at packages.xlibre.net, so this is a repo-add, not a source build.

the XLibre repo-add, per adapter
# targets/archlinux/adapter.sh — confirmed layout from packages.xlibre.net/arch/
cat >> "$ROOTFS/etc/pacman.conf" <<'EOF'
[xlibre]
Server = https://packages.xlibre.net/arch/stable/$arch
EOF

# targets/debian/adapter.sh — confirm suite/component against packages.xlibre.net/debian/
install -m0644 xlibre-archive-keyring.gpg "$ROOTFS/etc/apt/keyrings/"
echo "deb [signed-by=/etc/apt/keyrings/xlibre-archive-keyring.gpg] \
  https://packages.xlibre.net/debian stable main" \
  > "$ROOTFS/etc/apt/sources.list.d/xlibre.list"

8Tags, names, titles & release presentation — one universal scheme

Today the five are inconsistent (mixed prefixes, arch tokens x86_64/amd64/aarch64/arm64, and timestamp formats). One template fixes all of it.

Tags — one rolling tag per flavor

Dual-arch ISOs share the flavor's tag as separate assets, keeping the tag count at five and grouping a flavor's arches together.

PurposeTagExamples
Continuous (main)continuous-<flavor>continuous-debian, continuous-devuan, continuous-archlinux, continuous-freebsd, continuous-nextbsd
Custom-branch (future, §9)branch-<name>-<flavor>branch-menu-work-nextbsd

ISO / asset names — one template

universal filename
gershwin-on-<flavor>-<YYYYMMDDhhmmss>-<arch>.<ext>

flavor  = debian | devuan | archlinux | freebsd | nextbsd
stamp   = UTC, 14 digits, NO separators (sortable, cache-busting)
arch    = x86_64 | aarch64  (release token, normalized for EVERY OS — decoupled
                            from the build's internal triple: FreeBSD amd64 →
                            x86_64, dpkg arm64 → aarch64. NEVER amd64/arm64 here)
ext     = iso | img.zip     (img.zip = the RPi / raw disk-image variant)

gershwin-on-nextbsd-20260719101500-x86_64.iso
gershwin-on-debian-20260719101500-aarch64.iso
gershwin-on-debian-20260719101500-aarch64.img.zip

Screenshots publish in the tag, not the README

Each continuous-<flavor> release contains

AssetOn which flavors
gershwin-on-<flavor>-<stamp>-x86_64.isoall
gershwin-on-<flavor>-<stamp>-aarch64.isodebian, devuan (dual-arch)
gershwin-on-<flavor>-<stamp>-x86_64.png (matches the ISO)all (gated x86_64 boot)
gershwin-on-debian-<stamp>-aarch64.img.zipdebian only (RPi / disk-image variant)

What a published release looks like

publish-continuous renders each continuous-<flavor> tag as one self-describing release: a proper-cased title, a body that leads with the desktop screenshot inline and nothing else but a build-log link, and exactly two assets whose stems match. Title, body and assets are regenerated on every publish, so the release never shows a stale or half-updated state. For nextbsd it renders as:

releases/tag/continuous-nextbsd — as GitHub renders it
# ── title (releases list + tag page) ─────────────────────────────
NextBSD      Pre-release · tag: continuous-nextbsd

# ── body (Markdown) ──────────────────────────────────────────────
┌───────────────────────────────────────────────┐
│                                               │
│      [ About This Computer + desktop ]        │   inline image, embedded from
│                                               │   the matching .png asset below
└───────────────────────────────────────────────┘
Build log: …/actions/runs/<run-id>

# ── assets — exactly two, matching stems; GitHub shows the digest ─
gershwin-on-nextbsd-20260721000706-x86_64.iso     sha256:…   1.66 GiB
gershwin-on-nextbsd-20260721000706-x86_64.png     sha256:…    248 KiB

9The future custom-branch job — checkout.sh first

gershwin-developer/Library/Scripts/checkout.sh clones the 17 Gershwin/GNUstep source repos, but only on their default branch — its sole ref control is PINNED=1 (hardcoded SHAs) and SKIP_REPOS. No branch support.

current — checkout.sh (lines 61–63)
    else
        git clone "$REPO"          # ← no -b, always default branch
    fi

10Adding a new variant — the contributor guide

Adding a flavor is one per-flavor continuous-<flavor>.yml that builds the ISO in the flavor's own distro container and then calls the shared gate + publish, plus one targets/<flavor>/ dir holding that distro's native build. No forking a repo, no reimplementing live-boot.

continuous-ubuntu.yml — build in the distro container, then the shared tail
on:
  push: { branches: [main], paths: ['targets/ubuntu/**', '.github/actions/**', '.github/workflows/continuous-ubuntu.yml'] }
  workflow_dispatch:
jobs:
  build:                                       # in the flavor's OWN distro container
    runs-on: ubuntu-latest
    container: { image: ubuntu:latest, options: --privileged }
    steps: [ install live tooling, checkout, build ISO → out/, upload-artifact ]
  test:    { uses: ./.github/actions/screenshot-gate,   with: { flavor: ubuntu } }
  publish: { uses: ./.github/actions/publish-continuous, with: { tag: continuous-ubuntu, title: Ubuntu } }

Trigger scoped to targets/ubuntu/** + shared .github/actions/** + the flavor's own workflow file — rebuilds on its own target or a shared-infra change, never on another flavor's edit or a README. Filenames use x86_64/aarch64; the distro's build maps them to whatever its bootstrap needs (dpkg arm64, etc.).

targets/ubuntu/ — the distro's native live build + its config
<native build>    # build.sh (debootstrap) | config/ (live-build) | profiledef.sh (mkarchiso)
ci/containers/    # ONLY if the distro needs a custom image (as Devuan does)
packages…         # the distro package set (incl. XLibre, never Xorg)

The contract a variant must honor

ContractRequirement
Outputwrite the ISO to out/gershwin-on-<flavor>-<stamp>-<arch>.iso (arch = x86_64/aarch64; no sha256 sidecar)
BootISO boots to the Gershwin desktop on x86_64 UEFI so the shared QEMU gate can screenshot it
Desktopinstall Gershwin via gershwin-developer so the gate's colour/OCR markers match
X serverinstall XLibre (never Xorg); the flavor's own build supplies the package source, the config comes from the shared install
Overlaywire the session services via the variant's own init system under targets/<flavor>/overlays/
Gate tuningvalidate the colour threshold + OCR marker for the variant's greeter/wallpaper on first run

Checklist

11GitHub limits that actually bite

All re-verified against current GitHub docs. Only three constrain this — none is caused by consolidating.

LimitValue (Free, public repo)Bites?
Single release-asset file2 GiBYES — Arch at 1.91 GiB. Keep it compressed.
Actions concurrent jobs20 — org-wide, shared across reposNeutral — the 5 repos already share one budget; mono vs split is a wash. A full build-all is ~7 jobs.
ubuntu-latest disk14 GBYES — needs free-disk to stage a 2 GiB ISO.
Actions minutes (public)Free / unlimitedNo
Release assets vs storage quotaSeparate store, no total cap, unbilledNo — 5 rolling tags cost nothing
Actions artifacts storageCounts against quota (free for public)No — keep the build→gate ISO artifact short-retention
paths: filterIgnored by dispatch / scheduleBy design — build-all rebuilds everything; per-flavor callers gate on their paths:

Storage answer, directly: with delete+recreate each continuous-<flavor> holds ~1 ISO ≈ 1.5–2 GiB → the whole live set is ~10–15 GiB, none of it counting against any quota, no total cap. Five tags, or fifty, is fine.

12Migration: issues, history, cut-over

Moving into one home has two migration pieces. Issues are easy (native support); release history is the awkward one.

13Rollout: fork → POC → PR → replace

14Risks & gotchas, consolidated

15Alternative if you want to de-risk

Hub-repo first. If the full monorepo feels big, ship an intermediate: make gershwin-desktop/gershwin-desktop an orchestrator that retrofits the screenshot gate into each existing repo and hosts a release-index / gallery, keeping the five repos in place. It buys uniform gating this week with no migration, and its hub becomes the monorepo's build-all later. The downside is the five build engines still diverge — you don't get "everything builds the same way" until you do the real consolidation. Recommended only as a stepping stone, not the destination.