Files
punktfunk/design/ci.md
T
enricobuehler d01a8fd17a
ci / web (push) Failing after 22s
windows-host / package (push) Failing after 4m16s
ci / rust (push) Failing after 4m56s
ci / docs-site (push) Successful in 1m7s
android / android (push) Successful in 9m19s
ci / bench (push) Successful in 4m47s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Failing after 3s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
docker / deploy-docs (push) Has been skipped
deb / build-publish (push) Failing after 6m29s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Failing after 7m4s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m17s
apple / swift (push) Successful in 1m13s
apple / screenshots (push) Successful in 5m27s
feat(host): HDR Vulkan layer so Vulkan games get HDR on the virtual display
NVIDIA/AMD Vulkan ICDs refuse to *advertise* an HDR color space for a surface on an
IddCx indirect/virtual display, so Vulkan games (Doom: The Dark Ages, id Tech, Indiana
Jones, …) report "device does not support HDR" — even though Windows HDR, DWM compose,
and the client PQ stream all work, and the ICD happily *accepts + presents* a forced HDR
swapchain there. The whole gap is enumeration; the community (Apollo/Sunshine/VDD) wrote
this off as kernel-side / unfixable.

Add VK_LAYER_PUNKTFUNK_hdr_inject (packaging/windows/pf-vkhdr-layer/): a standalone
cdylib Vulkan implicit layer that appends {A2B10G10R10, HDR10_ST2084} + {RGBA16F, scRGB}
to vkGetPhysicalDeviceSurfaceFormats[2]KHR (no need to hook vkCreateSwapchainKHR — the
ICD doesn't validate the color space there). Self-gated on the surface monitor's actual
advanced-color state (DisplayConfig GET_ADVANCED_COLOR_INFO), so it is a complete no-op
on SDR sessions and real monitors (dedup). Always-on (registry-discovered) so it works
regardless of how a game is launched — env-scoping silently fails for already-running
Steam. Escape hatches: DISABLE_PF_VKHDR, PF_VKHDR_EXCLUDE, and a built-in kernel-anti-
cheat denylist.

The installer builds/signs/stages it and registers it under
HKLM64\SOFTWARE\Khronos\Vulkan\ImplicitLayers (opt-out "Install the HDR Vulkan layer"
task); windows-host CI fmt+clippy-gates it (msvc-only FFI).

Live-validated on the RTX box: Doom: The Dark Ages enables HDR over the pf-vdisplay
virtual display.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:33:20 +00:00

9.3 KiB

title, description
title description
CI & Docker Gitea Actions setup — workflows, the dockerized pieces, and the runners.

CI runs on Gitea Actions (git.unom.io, org unom). The workflows live in .gitea/workflows/; they run across Linux and macOS runners and push a few images to the Gitea container registry.

Release model

Two tracks (full guide: Release Channels). A push to main publishes canary builds to the canary channels; a single vX.Y.Z tag is THE release for every platform — built at that version, published to the stable channels, and every artifact attached to one Gitea Release via the shared scripts/ci/gitea-release.{sh,ps1} helper (idempotent create-or-fetch + delete-before-upload, so concurrent cross-runner attaches don't collide). The old host-v* / win-v* / host-win-v* tag namespaces are retired — v* is the only release tag.

Workflows

Workflow Trigger Runner What it does
ci.yml push main, PRs Linux Rust workspace (fmt · clippy -D warnings · build · test · C-ABI harness · header drift) in punktfunk-rust-ci; web/ + docs-site/ build + typecheck in oven/bun:1
apple.yml push main, PRs, manual macOS Rust core → PunktfunkCore.xcframeworkswift build/swift test (CI gate, no publish)
windows.yml push main (paths), PRs, manual Windows client build · clippy · fmt · test for x86_64/aarch64 (CI gate, no publish)
deb.yml push main → canary, v* → stable, manual Linux host/client/web .deb → apt (canary/stable distribution); v* attaches to the release
rpm.yml push main → canary, v* → stable, manual Linux host .rpm (bazzite + fedora-44 bases) → rpm (*-canary/base groups); v* attaches
windows-msix.yml push main (paths) → canary, v* → stable, manual Windows client MSIX x64+arm64 → generic registry (canary//latest/); v* attaches
windows-host.yml push main (paths) → canary, v* → stable, manual Windows host Inno installer → generic registry (canary//latest/); v* attaches
android.yml push main → Play internal, v* → Play alpha, PRs, manual Linux signed AAB+APK → Play + generic registry; v* attaches
release.yml push main (paths) → TestFlight, v* → DMG + TestFlight, manual macOS Apple mac/iOS(/tvOS on stable); v* notarized .dmg attaches
flatpak.yml push main (paths) → canary branch, v* → stable, manual Linux client flatpak (OSTree repo + bundle, branch per channel); v* attaches
decky.yml push main → canary, v* → stable, manual Linux Decky plugin zip → generic registry (canary//latest/); v* attaches
docker.yml push main, v*, manual Linux web/docs/CI images (latest + sha-<short>; v* adds a vX.Y.Z tag)

Dockerized pieces

The host and the native clients are intentionally not containerized (the host needs the GPU/compositor stack of the box it runs on). What is:

Image Source Notes
git.unom.io/unom/punktfunk-web web/Dockerfile (repo-root context — orval needs docs/api/openapi.json) Nitro bun bundle; PORT (3000) and PUNKTFUNK_MGMT_URL env at runtime
git.unom.io/unom/punktfunk-docs docs-site/Dockerfile This site; PORT (3000)
git.unom.io/unom/punktfunk-rust-ci ci/rust-ci.Dockerfile Ubuntu 26.04 + FFmpeg 8/PipeWire/GL/GBM dev libs + a libcuda link stub (driver userspace, no kernel module) + pinned rustup — the container ci.yml's Rust job runs in

Registry pushes authenticate with a repo Actions secret holding a registry token (a PAT with write:package; the login username in docker.yml is the token owner, not the push actor).

Runners

  • Linux runner — runs the Rust/web/docs jobs (as docker containers) and the image build+push jobs.
  • macOS runner — an Apple-silicon Mac running macOS, a host-mode act_runner (upstream now ships it as gitea-runner) provisioned by scripts/ci/setup-macos-runner.sh: rustup (+ both darwin targets for the universal xcframework), Node.js (host-mode runners execute JS actions via node from PATH — nothing auto-provisions it), the runner binary in ~/.local/bin, state under ~/ci/act-runner/ (config, .runner registration, runner.log), kept alive by the io.gitea.act_runner root LaunchDaemon — it cannot be a user LaunchAgent: macOS Local Network privacy silently blocks LAN dials ("no route to host") from unbundled CLI binaries in gui/user launchd domains, while system daemons are exempt. Needs full Xcode for xcodebuild -create-xcframework (CLT alone only covers swift build/test); if xcode-select still points at CLT, the script auto-detects /Applications/Xcode*.app and bakes a DEVELOPER_DIR override into the daemon environment — no xcode-select -s required.
  • Windows runner — builds and packages the native Windows client (MSIX) for the release matrix.

Re-provisioning is idempotent — re-running scripts/ci/setup-macos-runner.sh on the macOS runner with a fresh GITEA_RUNNER_TOKEN (org unom → Settings → Actions → Runners → Create new runner) re-registers it without manual cleanup.

Apple releases

release.yml produces the production client builds on the Mac runner. All three app targets share the bundle ID io.unom.punktfunk (one App Store listing, universal purchase — effectively unchangeable after first submission). Signing is not secret-based: the runner uses its login keychain directly, so install the Developer ID Application, Apple Distribution, and (for the Mac App Store .pkg) 3rd Party Mac Developer Installer identities once via Xcode, with the WWDR intermediate present so they show as valid. The only secrets are ASC_API_KEY_P8/ASC_API_KEY_ID/ASC_API_ISSUER_ID (App Store Connect API key — notarization + TestFlight upload). Per-platform state:

  • macOS (Developer ID) — sandboxed app (Config/Punktfunk-macOS.entitlements) → export → notarytool → stapled .dmg on the Gitea release.
  • macOS (App Store) — manual-signed archive (Apple Distribution + the Punktfunk macOS App Store Distribution profile) → upload to TestFlight. App Sandbox is mandatory here and is now declared (app-sandbox + network client/server + audio-input + bluetooth/usb). Prereqs (one-time, Apple portal): add the macOS platform to the App Store Connect app record (universal purchase), install the Mac App Store distribution profile + the installer cert above. continue-on-error until those exist.
  • iOS — archive + upload to TestFlight (method: app-store-connect, destination: upload). Crypto is declared exempt (ITSAppUsesNonExemptEncryption, Config/Info.plist) so builds don't stall on the compliance question.
  • tvOS — archive + upload to TestFlight (Rust core built from tier-3 targets, nightly -Zbuild-std via build-xcframework.sh).

Each macOS target uses its own entitlements: Config/Punktfunk-macOS.entitlements (App Sandbox is macOS-only) for the macOS app, and the shared Config/Punktfunk.entitlements (keychain-access-groups only) for iOS/tvOS — com.apple.security.app-sandbox is invalid on iOS/tvOS and would fail upload validation.

The runner needs a release (non-beta) Xcode — App Store processing rejects beta-SDK builds, and a beta is unusable for the Rust side too: a newer-than-OS ld emits dylibs the running dyld rejects ("mis-aligned LINKEDIT string pool"), killing every proc-macro build with a misleading E0463 can't find crate. build-xcframework.sh therefore resolves toolchains itself: non-beta Xcode for everything; with only CLT + a beta present it builds macOS slices against CLT (packaging via any Xcode — -create-xcframework does no linking) and refuses iOS/tvOS slices (CLT has no iOS SDK).

Deployment

docker.yml's deploy-docs job ships this docs site after every image push: it syncs compose.production.yml to the docs server and runs docker compose pull && up -d there over SSH, driven by a small set of deploy secrets (DEPLOY_HOST / DEPLOY_USER / DEPLOY_PORT / DEPLOY_SSH_KEY). A reverse proxy in front of that server serves the container as https://docs.punktfunk.unom.io. The host and the web console are NOT deployed — the console fronts a punktfunk host's management API on whatever box runs the host.

Troubleshooting

  • macOS runner offline — check ~/ci/act-runner/runner.log on the runner; restart with sudo launchctl kickstart -k system/io.gitea.act_runner. "no route to host" in the log means the daemon is running in a gui/user domain again — see the Local Network note above.
  • apple.yml fails at the xcframework step — Xcode missing or unselected: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer and accept the license (sudo xcodebuild -license accept), then re-run.
  • Rust job can't pull punktfunk-rust-ci — the runner host's docker daemon needs a docker login git.unom.io if the org/registry isn't anonymously readable.
  • Stale builder image after toolchain/dep changesdocker.yml re-pushes it on every main push; a manual workflow_dispatch of docker.yml forces a rebuild.