Skip to main content

branding and distribution

Status: SPEC + phase-1 implementation (2026-08-22). Owner intent (maintainer, verbatim requirements): automated GitHub releases; an un-branded wootc build plus branded builds for TunaOS, the Bluefin family (bluefin / dakota / bluefin-lts), Aurora, and Bazzite; branding via a per-brand config + assets override; branded installers do not use the wootc name (wootc is the generic project name only); branded distributions pre-bundle or pre-download the bootc image on Windows, because the deployer stage must never rely on the internet โ€” laptops on Wi-Fi have no networking in the initramfs, and the E2E VM's virtio ethernet has been masking that.


1. Automated releasesโ€‹

Trigger. Two channels, one pipeline (release.yml):

  • Tagged (v*): unchanged โ€” the human-blessed channel, E2E-gated.
  • Auto (workflow_run after a green E2E nightly on main, or workflow_dispatch): tags auto-vYYYYMMDD-<sha> and publishes a pre-release. The E2E gate is satisfied by the run that triggered it โ€” the release is cut from the exact SHA the nightly proved. No green nightly, no auto release.

Assets. Every release ships, for each brand (see ยง2):

AssetSource
<Product>-Setup.exe (e.g. wootc.exe, TunaOS-Installer.exe)per-brand app build
deployer-vmlinuz, deployer-initramfs.imgdeployer image build (payload/deployer/Containerfile)
shimx64.efi, grubx64.efisigned Fedora chain (same extraction the E2E uses)
wubildr.efiwubildr build
SHA256SUMSsha256sum over every asset above

This closes the today-gap where deployerBaseURL() points at release assets that have never existed (documented on #196): after the first automated release, a plain online install works with zero pre-staging.

Branded downloads. Each branded exe embeds its own deployerBaseURL-compatible layout expectation; boot artifacts are shared across brands (the deployer is brand-neutral), so one artifact set + one SHA256SUMS serves all brand exes on the same release.

2. Brandingโ€‹

One binary per brand, chosen at build time. The existing runtime overlay (C:\wootc\brand.json, GetBranding/mergeBranding) stays for enterprise re-skins; brand builds embed their config so the exe is branded from the first pixel with no sidecar file.

Layout (app/branding/ โ€” inside the module so go:embed reaches it):

branding/
wootc/brand.json # generic (default; current strings)
tunaos/brand.json
bluefin/brand.json # covers bluefin, dakota, bluefin-lts via catalog
aurora/brand.json
bazzite/brand.json
<brand>/logo.svg # the REAL mark (โ†’ LogoDataURI); emoji only
# where emoji IS the branding (generic TunaOS)
<brand>/font.woff2 # the brand's typeface, embedded (โ†’ FontDataURI);
# never fetched at run time
<brand>/theme.css # deep restyle injected after style.css: tokens,
# button treatment, radii (โ†’ ThemeCSS)

Deep branding, not a palette swap. Assets are taken from each project's own published branding (see app/branding/README.md for provenance): Bazzite's gradient "B" mark + DM Sans + the bazzite.gg cobaltโ†’violet button gradient; Bluefin's indigo tile + Inter on the projectbluefin.io navy; Aurora's gradient mark + Geist + getaurora.dev's zinc ground and pill buttons. The injected theme wins over both light- and dark-mode token blocks, so a branded build commits to its brand's look; the window pre-paint colour follows the brand too (#173). Every brand's four-screen journey is captured as an automated screenshot walkthrough โ€” see branded-walkthroughs.md, regenerated by tests/gui/branded-walkthrough.spec.js on every Playwright run.

brand.json extends the existing Branding struct with distribution fields:

{
"name": "Bazzite",
"productName": "Bazzite Installer",
"exeName": "Bazzite-Installer",
"tagline": "Play on Linux โ€” keep your Windows files.",
"logoEmoji": "๐ŸŽฎ",
"accent": "#7b5cff", "accentText": "#ffffff",
"installVerb": "Install",
"catalog": ["bazzite", "bazzite-deck"],
"defaultImage": "bazzite",
"hideCustomImage": true,
"preloadImage": true
}
  • catalog: image ids from app/data/images.json this brand offers. Empty = full catalog (wootc generic). The Bluefin brand lists ["bluefin", "dakota", "bluefin-lts"].
  • defaultImage: pre-selected card.
  • hideCustomImage: branded builds hide the custom-OCI field regardless of support policy.
  • preloadImage: brand wants the image fetched on Windows (ยง3) โ€” on for every branded build, off only for generic wootc when the user opts out.
  • productName/exeName: no "wootc" in branded user-facing surfaces โ€” window title, screens, Add/Remove entry, shutdown message, docs links. Internal paths (C:\wootc, service names) stay: they are part of the on-disk contract shared by the deployer and migration payload, invisible in normal use, and renaming them per brand would fork the E2E surface. (Revisit if a partner requires path-level rebranding.)

Build selection. app/branding/embed.go embeds branding/*/brand.json via go:embed; -ldflags "-X main.brandID=bazzite" picks one (default wootc). GetBranding() resolution order: embedded brand โ†’ overlay C:\wootc\brand.json โ†’ hardcoded defaults. The frontend already consumes GetBranding + installVerb(); remaining hardcoded "TunaOS"/"wootc" strings in screens move to state.brand.

Upstream blessings. A branded build wears somebody else's mark, so each brand carries a recorded decision about whether that is allowed โ€” app/branding/<brand>/blessing.json, summarised in the table in app/branding/README.md and asked for via upstream-blessings.md (#227). Four separate questions per project (mark, name, tagline, distributing a branded exe), plus who owns the winget namespace the identifier would live in. The status falls out of the answers: any no โ†’ declined, all four yes โ†’ blessed, anything else โ†’ pending. A mark this project does not own cannot be marked blessed without a link to the yes.

Release matrix. The publish job asks packaging/brands.sh which brands may ship, builds each surviving exe with its ldflag, names it exeName.exe, and adds all to the release + SHA256SUMS. The decision is load-bearing there: a declined project's exe drops out of the matrix, a brand with no record at all is a hard error rather than a default yes, and a pending one still builds (shipping predates the ask) but is named as unblessed in the release log. WOOTC_REQUIRE_BLESSING=1 drops pending brands too.

3. Offline-first deploysโ€‹

Principle: Windows does all networking; the deployer does none. A laptop's Wi-Fi does not exist in the initramfs and never will (firmware, supplicant, credentials). Every network dependency in the deploy stage is a bug. Today's dependencies and their fates:

Deployer network useFate
bootc install pulling the OCI imageremoved โ€” image comes from the Windows-staged bundle (below); fisherman already supports --source-imgref oci:<path>
backend/btrfs probes (podman run $IMAGE)probe the bundle's image config (skopeo oci: transport, local)
ntfs-3g/qemu-ga dnf injectionskipped offline โ€” the early-cpio closure fallback (now @-protected and exec-verified) is the offline answer
EPEL reach, registry mirrornot applicable offline
dracut rd.neednet=1 + initqueue/online hooksee "starting without a network" below

The bundle: an OCI layout on NTFS. C:\wootc\bundle\oci\ โ€” index.json

  • blobs/sha256/*. Chosen over a containers-storage store because it is plain files (FAT/NTFS-safe: no xattrs, no whiteouts, no overlay driver mismatch โ€” exactly the #196 items 1-2 trap), digest-addressed (verification is inherent), and already a fisherman-supported source.

Two ways it gets there:

  1. Pre-download on Windows (default for every brand with preloadImage). A new pipeline step after "Downloading Linux": "Downloading your Linux system" โ€” the app pulls the selected image to the bundle dir over the user's working Windows network, with real progress (bytes, not a stalled bar). Implementation: containers/image Go module (docker:// โ†’ oci: copy) โ€” runs fine on Windows, verifies digests as it writes.
  2. Pre-made bundle beside the exe (payload/bundle/make-release-bundle.sh output, USB-stick / media distribution): the app stages bundle/ found beside the executable into C:\wootc\bundle during "Setting things up" (closing #196 item 3).

vault.json gains bundleRef + bundleDigest so the deployer can verify the staged bundle matches the user's selection (superseding tag-only pinning, #196 item on BundleInfo.Digest).

Deployer consumption. Early in deploy.sh, after the NTFS mount: if /mnt/ntfs/wootc/bundle/oci/index.json exists and its ref matches bundleRef, set WOOTC_BUNDLE=/mnt/ntfs/wootc/bundle/oci and

  • fisherman recipe uses --source-imgref oci:$WOOTC_BUNDLE;
  • backend + btrfs probes read the image config from the layout (no podman pull, no network);
  • the ntfs-3g injection is skipped with a [WARN] offline: relying on the image's NTFS support / staged closure;
  • registry preflight becomes "Verifying your downloaded system..." on the splash.

Starting without a network. The deploy hook currently sits in initqueue/online, so an offline machine never starts deploying. Change: install the same guarded hook at initqueue/settled as well; the settled instance mounts nothing itself โ€” it starts deploy.sh, and deploy.sh owns the decision: bundle found โ†’ proceed with zero network; no bundle โ†’ wait (bounded, with a splash message naming the fix: "Waiting for a wired network connectionโ€ฆ") for the online path before pulling. The existing run-once guard file keeps the two hook instances from double-starting. Observed-mode E2E keeps virtio ethernet and today's timings; a future matrix axis offline=on runs a cell with -nic none to prove the whole path.

Not doing: Wi-Fi in the initramfs (credential + firmware + supplicant complexity, against the "one calm reboot" principle); per-brand deployer images (the deployer stays brand-neutral).

4. Delivery phasesโ€‹

  1. Releases โ€” DELIVERED: release.yml carries both channels (tag v* E2E-gated; workflow_run after a green nightly GUI E2E โ†’ auto-v* pre-release from the proven SHA), builds one exe per app/branding/* via -X main.brandID, and ships the shared boot artifacts (deployer kernel/initramfs, signed shim+GRUB, wubildr) + SHA256SUMS.
  2. Branding โ€” DELIVERED: five brands with real assets (logo.svg, font.woff2, theme.css โ€” see app/branding/README.md), embedded selection, de-hardcoded screens (distro name + product name from state.brand), brand catalogs with defaultImage/hideCustomImage, branded ARP entry and boot-menu title (vault distro_name).
  3. Offline core โ€” DELIVERED: Windows-side pre-download (app/ocipull.go, "Downloading your Linux system" step, fail-closed sha256 on every blob, resumable), deployer OCI-bundle ingest before anything network-shaped (podman pull oci: + tag โ€” probes, injection check, and fisherman's containers-storage export all become local I/O), settled-hook start with a bounded, plain-language network wait, and the dnf injection skipped offline. Contracts: tests/unit/offline-first.bats. (Beside-the-exe bundle staging into C:\wootc\bundle remains the existing store/ path; the OCI form joins it.)
  4. Offline proof (follow-up): offline=on matrix axis with -nic none; promote preloadImage to default-on for generic wootc once proven.