The Mac App Store requires App Sandbox, which the macOS app didn't declare. App Sandbox is macOS-only (invalid on iOS/tvOS, fails upload validation), so the macOS target now uses a dedicated Config/Punktfunk-macOS.entitlements while iOS/tvOS keep the shared Config/Punktfunk.entitlements (unchanged). The single macOS app is sandboxed for BOTH channels — the Developer ID DMG is codesigned with the same file — so the local build equals what App Store users get. Entitlement set (verified against the code + Apple docs): - app-sandbox, network.client. - network.server: NOT optional despite the client being outbound-only — the sandbox gates the bind() syscall as network-bind, and quinn (quic.rs) + the raw-UDP plane (transport/udp.rs) both bind explicitly, so host->client datagrams never arrive without it (the classic QUIC-under-sandbox trap). - device.audio-input (mic uplink), device.bluetooth + device.usb (Xbox/DualSense controllers over BT/USB via GameController), keychain-access-groups (existing). Omitted: device.hid (undocumented), files.user-selected.* (no pickers), networking.multicast (Bonjour browse is exempt; requesting it breaks signing). CI (release.yml): add a macOS App Store archive+upload-to-TestFlight step mirroring the iOS lane (manual Apple Distribution signing + the 'Punktfunk macOS App Store Distribution' profile, app-store-connect/upload, installer-signed pkg), continue-on-error until the portal prereqs exist; point the Developer ID DMG codesign at the sandboxed entitlements. Docs (ci.md) + clients/apple README updated; the runner additionally needs the macOS platform on the App Store Connect record + the '3rd Party Mac Developer Installer' cert. Verified: signed Debug build embeds exactly the intended entitlements (codesign -d --entitlements), swift build green against the rebuilt xcframework. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.7 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). Three workflows in
.gitea/workflows/, two runners, three images in the Gitea container registry.
Workflows
| Workflow | Trigger | Runner | What it does |
|---|---|---|---|
ci.yml |
push to main, PRs |
ubuntu-24.04 |
Rust workspace (fmt · clippy -D warnings · build · test · C-ABI harness · generated-header drift) inside the punktfunk-rust-ci image; web/ and docs-site/ build + typecheck in oven/bun:1 |
docker.yml |
push to main, v* tags, manual |
ubuntu-24.04 |
Builds + pushes the three images below (latest + sha-<short> tags) |
apple.yml |
push to main, PRs, manual |
macos-arm64 |
Rust core → PunktfunkCore.xcframework → swift build + swift test in clients/apple |
release.yml |
v* tags, manual |
macos-arm64 |
Production Apple builds: sandboxed macOS .dmg (Developer ID, notarized, stapled) attached to the Gitea release + macOS/iOS/tvOS archives uploaded to TestFlight |
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 the repo Actions secret REGISTRY_TOKEN (a PAT
with write:package; the login username in docker.yml is the token owner, not the
push actor).
Runners
ubuntu-24.04— the pre-existing Linux runner; runs the Rust/web/docs jobs (as docker containers) and the image build+push jobs.macos-arm64—home-mac-mini-1(M-series, macOS 26), a host-modeact_runner(upstream now ships it asgitea-runner) provisioned byscripts/ci/setup-macos-runner.sh: rustup (+ both darwin targets for the universal xcframework), Node.js (host-mode runners execute JS actions vianodefrom PATH — nothing auto-provisions it), the runner binary in~/.local/bin, state in~/ci/act-runner/(config,.runnerregistration,runner.log), kept alive by theio.gitea.act_runnerroot 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 forxcodebuild -create-xcframework(CLT alone only coversswift build/test); ifxcode-selectstill points at CLT, the script auto-detects/Applications/Xcode*.appand bakes aDEVELOPER_DIRoverride into the daemon environment — noxcode-select -srequired.
Re-provisioning (idempotent) or first-time registration from a dev box:
# token: org unom → Settings → Actions → Runners → Create new runner
ssh enricobuehler@192.168.1.135 GITEA_RUNNER_TOKEN=<token> bash -s \
< scripts/ci/setup-macos-runner.sh
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.dmgon 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-erroruntil 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-stdviabuild-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 ~/punktfunk-docs on unom-1 (the DMZ services VM
website and cms deploy to) and runs docker compose pull && up -d there over SSH (same
pattern and secret set as unom/website: DEPLOY_HOST / DEPLOY_USER / DEPLOY_PORT /
DEPLOY_SSH_KEY, the unom-ci-deploy key). The container binds host port 3220;
Caddy on home-reverse-proxy-1 serves it as https://docs.punktfunk.unom.io (vhost in
unom/reverse-proxy, UniFi firewall allowlist Caddy→unom-1:3220 in unom/infra
proxmox/unom-1). 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
- Mac runner offline —
ssh <mac> tail -50 '~/ci/act-runner/runner.log'; restart withsudo 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.ymlfails at the xcframework step — Xcode missing or unselected:sudo xcode-select -s /Applications/Xcode.app/Contents/Developerand 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 adocker login git.unom.ioif the org/registry isn't anonymously readable. - Stale builder image after toolchain/dep changes —
docker.ymlre-pushes it on everymainpush; a manualworkflow_dispatchofdocker.ymlforces a rebuild.