Files
punktfunk/docs-site/content/docs/ci.md
T
enricobuehler 57e7f9fe25 feat(release): production Apple builds — notarized macOS dmg + iOS TestFlight
release.yml (v* tags / dispatch, macos-arm64 runner): universal mac +
iOS xcframework -> xcodebuild archive -> Developer ID export ->
notarytool + staple -> dmg on the Gitea release; iOS archive uploads
to TestFlight (app-store-connect/upload). Per-run throwaway keychain;
ASC API key authenticates notarization, upload, and automatic-signing
profile fetch. macOS App Store lane deferred (needs App Sandbox);
tvOS deferred (tier-3 Rust targets).

All app targets now share bundle ID io.unom.punktfunk — ONE App Store
listing with universal purchase (decided pre-submission; effectively
unchangeable after). ITSAppUsesNonExemptEncryption=false declared
(standard-algorithm AES-GCM, exempt).

build-xcframework.sh resolves Apple toolchains itself: cargo's HOST
artifacts (proc-macros, build scripts) are loaded by the running OS,
and a newer-than-OS beta Xcode ld emits LINKEDIT layouts dyld rejects
("mis-aligned LINKEDIT string pool" -> misleading E0463) — so prefer
a non-beta Xcode for everything, fall back to CLT for mac-only slices
(env untouched: an explicit DEVELOPER_DIR=<CLT> trips xcrun's license
check), refuse iOS/tvOS without a real Xcode (CLT has no iOS SDK).
The runner plist no longer injects DEVELOPER_DIR for the same reason.

punktfunk_Logo.icon: dropped the Xcode-27-beta-only Icon Composer
features (refractivity, specular-location) — 26.5's actool crashes on
them, and store builds must use release Xcode. Visual delta is the
refraction/specular nuance only; re-author when 27 ships.

Validated on home-mac-mini-1 with Xcode 26.5: mac+iOS xcframework
slices, unified bundle IDs, signing-free app build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:34:45 +00:00

6.8 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.xcframeworkswift build + swift test in clients/apple
release.yml v* tags, manual macos-arm64 Production Apple builds: Developer-ID-signed, notarized, stapled macOS .dmg attached to the Gitea release + iOS archive 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-arm64home-mac-mini-1 (M-series, macOS 26), 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 in ~/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.

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). Secrets: DEVID_CERT_P12_B64/DEVID_CERT_PASSWORD (Developer ID Application certificate, only creatable by the account holder) and ASC_API_KEY_P8/ASC_API_KEY_ID/ASC_API_ISSUER_ID (App Store Connect API key — notarization, TestFlight upload, automatic-signing profile fetch). Signing uses a per-run throwaway keychain; nothing persists on the runner. Per-platform state:

  • macOS — Developer ID export → notarytool → stapled .dmg on the Gitea release. The Mac App Store lane is deferred: it requires App Sandbox entitlements (network client + Bonjour) the app doesn't declare yet.
  • 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 — not built: the Rust core needs tier-3 targets (nightly -Zbuild-std).

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 offlinessh <mac> tail -50 '~/ci/act-runner/runner.log'; 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.