# Apple client CI — runs on the self-hosted macOS runner (home-mac-mini-1, host mode; # see scripts/ci/setup-macos-runner.sh). Builds the Rust core into # PunktfunkCore.xcframework, then builds + tests the Swift package. Network-dependent # tests (RemoteFirstLightTests) self-skip without PUNKTFUNK_REMOTE_HOST. # # A second job (`screenshots`) captures the App Store Connect screenshots of the REAL UI # (mac window + iOS/iPad/tvOS Simulators, see clients/apple/tools/screenshots.sh) and attaches # them to the run as a single zip artifact (`punktfunk-appstore-screenshots`). It is isolated # from the build/test job and best-effort, so a capture gap never reds the core signal. name: apple # One pending run per workflow+ref: a newer push supersedes the queued/running one and cancels # it (a canary only needs the latest commit; each release tag is its own ref so tag runs never # cancel each other). Keeps a busy push cadence from piling ~10 queued runs per commit onto the # runner fleet. Gitea honors this for push triggers (PR triggers: see gitea#35933). concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: push: branches: [main] # Scope canary builds to what this artifact is built FROM — a docs-only or # web-only push should not light up the whole fleet. Applies to branch pushes; # tag runs are matched by `tags:` (proven by flatpak/windows-msix releases). paths: - 'crates/**' - 'clients/apple/**' - 'scripts/build-xcframework.sh' - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' - 'scripts/ci/**' - '.gitea/workflows/apple.yml' pull_request: paths: - 'crates/**' - 'clients/apple/**' - 'scripts/build-xcframework.sh' - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' - 'scripts/ci/**' - '.gitea/workflows/apple.yml' workflow_dispatch: # Shared compile cache: sccache -> RustFS S3 (storage.unom.io — the mini resolves it via # the router, i.e. the hairpin path whose TLS always validated). Covers every cargo/rustc # invocation build-xcframework.sh makes, incl. the tvOS -Zbuild-std std builds; the Swift # side stays on DerivedData (sccache doesn't cache swiftc). env: RUSTC_WRAPPER: sccache SCCACHE_BUCKET: unom-ci-sccache SCCACHE_ENDPOINT: https://storage.unom.io SCCACHE_REGION: home-central AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_SECRET_ACCESS_KEY }} # sccache and incremental compilation are mutually exclusive; the shared cache makes the # runner's persistent target/ disposable instead of precious. CARGO_INCREMENTAL: "0" jobs: # SECURITY: builds/tests PULL-REQUEST code on the host-mode, persistent `macos-arm64` runner shared # with the release-signing job (release.yml, which loads the App Store Connect key). Untrusted PR # code could persist on it or harvest signing material. Definitive fix is server-side: enable Gitea's # "require approval for PRs from outside collaborators/forks", and/or isolate PR CI on ephemeral # runners. The `if:` is a fail-open backstop — it skips fork PRs where Gitea reports the fork flag and # still runs same-repo PRs (and where the flag is absent), so it never blocks internal PR CI. swift: runs-on: macos-arm64 if: >- gitea.event_name != 'pull_request' || gitea.event.pull_request.head.repo.fork != true timeout-minutes: 60 steps: - uses: actions/checkout@v4 - name: Rust toolchain (self-healing on a fresh runner) run: | if ! command -v rustup >/dev/null && [ ! -x "$HOME/.cargo/bin/rustup" ]; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --no-modify-path --profile minimal fi RUSTUP="$(command -v rustup || echo "$HOME/.cargo/bin/rustup")" dirname "$RUSTUP" >> "$GITHUB_PATH" "$RUSTUP" target add aarch64-apple-darwin x86_64-apple-darwin # Shared compile cache. ~/.local/bin is on the runner daemon's PATH; GITHUB_PATH is # belt-and-braces. bsdtar (macOS) globs by default — no --wildcards. - name: sccache (self-healing install) run: | if ! command -v sccache >/dev/null; then mkdir -p "$HOME/.local/bin" curl -fsSL https://github.com/mozilla/sccache/releases/download/v0.10.0/sccache-v0.10.0-aarch64-apple-darwin.tar.gz \ | tar -xz --strip-components=1 -C "$HOME/.local/bin" '*/sccache' fi echo "$HOME/.local/bin" >> "$GITHUB_PATH" sccache --version # `punktfunk-core` now decodes Opus in-core for the Apple client (surround), pulling # `audiopus_sys`, which builds a vendored static libopus via CMake when pkg-config can't find a # system Opus — so the xcframework is self-contained (no runtime libopus.dylib on end-user Macs). # CMake must be on PATH; install it self-healing on a fresh runner. - name: CMake (for the vendored libopus audiopus_sys builds) run: | # Runner steps run with `bash --noprofile --norc`, so Homebrew's bin dir isn't on PATH — # locate brew explicitly, install cmake if missing, and export its bin dir to GITHUB_PATH so # the xcframework build step (audiopus_sys → vendored libopus) finds `cmake`. for B in /opt/homebrew/bin/brew /usr/local/bin/brew; do [ -x "$B" ] && BREW="$B" && break; done if [ -z "$BREW" ]; then echo "::error::Homebrew not found on the runner"; exit 1; fi BREW_BIN="$(dirname "$BREW")"; export PATH="$BREW_BIN:$PATH" command -v cmake >/dev/null || "$BREW" install cmake echo "$BREW_BIN" >> "$GITHUB_PATH" # Homebrew's CMake 4 dropped compatibility with the vendored libopus's pre-3.5 # `cmake_minimum_required`; treat 3.5 as the policy minimum (the cmake crate's child cmake # inherits this from the env during the xcframework build). echo "CMAKE_POLICY_VERSION_MINIMUM=3.5" >> "$GITHUB_ENV" - name: Build PunktfunkCore.xcframework run: bash scripts/build-xcframework.sh - name: Build (PunktfunkKit + PunktfunkClient app shell) working-directory: clients/apple run: swift build - name: Test (unit + real-codec round trip; remote tests self-skip) working-directory: clients/apple run: swift test # App Store screenshots of the real UI, zipped and attached to the run as a build artifact. # Skipped on PRs (cost); runs on main pushes + manual dispatch. Needs the build/test job green # first, and is a separate job so a capture hiccup can never red the core signal. # # Scope = the two REQUIRED iOS sizes (iPhone 6.9" + iPad 13"), captured on the Simulator # (`simctl io screenshot`, no Screen Recording grant needed). macOS and tvOS are deliberately # NOT in CI: the self-hosted runner is headless (no window-server session), so the mac window # capture can't run there; tvOS needs the Tier-3 build-std slice. Generate those two locally on # a GUI Mac with `clients/apple/tools/screenshots.sh macos tvos`. screenshots: needs: swift if: gitea.event_name != 'pull_request' runs-on: macos-arm64 timeout-minutes: 75 steps: - uses: actions/checkout@v4 - name: Rust toolchain + iOS Simulator targets run: | if ! command -v rustup >/dev/null && [ ! -x "$HOME/.cargo/bin/rustup" ]; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --no-modify-path --profile minimal fi RUSTUP="$(command -v rustup || echo "$HOME/.cargo/bin/rustup")" dirname "$RUSTUP" >> "$GITHUB_PATH" "$RUSTUP" target add aarch64-apple-darwin x86_64-apple-darwin \ aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios # Shared compile cache. ~/.local/bin is on the runner daemon's PATH; GITHUB_PATH is # belt-and-braces. bsdtar (macOS) globs by default — no --wildcards. - name: sccache (self-healing install) run: | if ! command -v sccache >/dev/null; then mkdir -p "$HOME/.local/bin" curl -fsSL https://github.com/mozilla/sccache/releases/download/v0.10.0/sccache-v0.10.0-aarch64-apple-darwin.tar.gz \ | tar -xz --strip-components=1 -C "$HOME/.local/bin" '*/sccache' fi echo "$HOME/.local/bin" >> "$GITHUB_PATH" sccache --version # See the swift job: audiopus_sys (via the in-core Opus decode) builds vendored libopus with CMake. - name: CMake (for the vendored libopus audiopus_sys builds) run: | # Runner steps run with `bash --noprofile --norc`, so Homebrew's bin dir isn't on PATH — # locate brew explicitly, install cmake if missing, and export its bin dir to GITHUB_PATH so # the xcframework build step (audiopus_sys → vendored libopus) finds `cmake`. for B in /opt/homebrew/bin/brew /usr/local/bin/brew; do [ -x "$B" ] && BREW="$B" && break; done if [ -z "$BREW" ]; then echo "::error::Homebrew not found on the runner"; exit 1; fi BREW_BIN="$(dirname "$BREW")"; export PATH="$BREW_BIN:$PATH" command -v cmake >/dev/null || "$BREW" install cmake echo "$BREW_BIN" >> "$GITHUB_PATH" # Homebrew's CMake 4 dropped compatibility with the vendored libopus's pre-3.5 # `cmake_minimum_required`; treat 3.5 as the policy minimum (the cmake crate's child cmake # inherits this from the env during the xcframework build). echo "CMAKE_POLICY_VERSION_MINIMUM=3.5" >> "$GITHUB_ENV" - name: Pin + prune DerivedData (same disease release.yml already cures) # screenshots.sh builds into a throwaway mktemp DerivedData per invocation — two # fresh ~1 GB trees per run, zero reuse. Pin one stable root (PF_SHOT_DERIVED_DATA, # honored by the script) so repeat runs are incremental, and GC anything a week old # in the default DerivedData root that no pin owns. run: | DD="$HOME/ci/derived-data/screenshots" mkdir -p "$DD" echo "PF_SHOT_DERIVED_DATA=$DD" >> "$GITHUB_ENV" if [ -d "$HOME/Library/Developer/Xcode/DerivedData" ]; then find "$HOME/Library/Developer/Xcode/DerivedData" -mindepth 1 -maxdepth 1 \ -mtime +7 -exec rm -rf {} + 2>/dev/null || true fi - name: Build PunktfunkCore.xcframework (mac + iOS slices) run: BUILD_IOS=1 bash scripts/build-xcframework.sh - name: Capture screenshots (iPhone 6.9" + iPad 13"; auto-creates the Simulators) working-directory: clients/apple env: SETTLE: "8" # Simulators settle slower than a local run run: | # Independent invocations: one platform failing skips it, not the other. bash tools/screenshots.sh ios || echo "::warning::iOS (iPhone 6.9\") screenshots skipped" bash tools/screenshots.sh ipad || echo "::warning::iPad 13\" screenshots skipped" echo "Produced:"; ls -la screenshots || true - name: Shut the Simulators down (leaked booted sims once piled up 846 deep) if: always() run: xcrun simctl shutdown all || true - name: Upload screenshots (zip artifact) if: always() # v3, not v4: Gitea's artifact backend identifies as GHES, which @actions/artifact v2+ # (upload-artifact@v4) refuses. v3 uses the older API Gitea supports; download is still a zip. uses: actions/upload-artifact@v3 with: name: punktfunk-appstore-screenshots path: clients/apple/screenshots if-no-files-found: warn retention-days: 30