name: Build & Test on: push: branches: - main pull_request: jobs: test: name: Run Swift Tests (${{ matrix.name }}) runs-on: macos-latest # A hung test must fail fast, not hold a runner for GitHub's 360-minute # default (observed: intermittent app-suite hangs starving the queue). # The long steps carry tighter individual bounds (5-minute test watchdog, # 6-minute benchmark step, 10-minute floor gate that may re-run the # benchmarks up to twice on a noisy runner); this is the backstop. timeout-minutes: 25 strategy: fail-fast: false # Don't cancel other matrix jobs when one fails matrix: include: - name: app path: . - name: BitLogger path: localPackages/BitLogger - name: BitFoundation path: localPackages/BitFoundation steps: - name: Checkout code uses: actions/checkout@v5 # Use the Xcode-bundled Swift toolchain: it always matches the SDK on # the runner image. A standalone swift.org toolchain (setup-swift) broke # whenever the image's Xcode moved ahead of it ("this SDK is not # supported by the compiler"). - name: Note toolchain version (cache key) id: swift-version run: echo "version=$(swift --version 2>/dev/null | head -1 | shasum | cut -c1-12)" >> "$GITHUB_OUTPUT" - name: Cache build artifacts uses: actions/cache@v4 with: path: ${{ matrix.path }}/.build key: ${{ runner.os }}-${{ steps.swift-version.outputs.version }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/*.swift', matrix.path), format('{0}/**/Package.resolved', matrix.path)) }} restore-keys: | ${{ runner.os }}-${{ steps.swift-version.outputs.version }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/Package.resolved', matrix.path)) }} ${{ runner.os }}-${{ steps.swift-version.outputs.version }}-${{ matrix.name }}- - name: Build tests # Built separately so the hang watchdog below times only test # execution: a cold-cache coverage build on a slow runner can # legitimately take several minutes, and is already bounded by the # 15-minute job timeout. run: swift build --build-tests --enable-code-coverage --package-path ${{ matrix.path }} - name: Run Tests # Perf benchmarks are excluded here and run in their own serial step # below: measuring while parallel test processes contend for cores # produces noisy numbers, and the XCTest measure machinery has hung # intermittently under parallel workers on loaded runners. Excluded # via --skip (not just the env guard): every app run since the # baselines landed timed out at the 15-minute job limit with the # baseline tests dispatched into the parallel phase. # # The watchdog samples any still-running test processes after 5 # minutes (the suite passes in seconds when healthy; the build is # done by this step) and kills the run, so a hang fails fast with # stacks in the log instead of a silent timeout. env: BITCHAT_SKIP_PERF_BASELINES: "1" run: | swift test --skip-build --parallel --quiet --enable-code-coverage \ --skip PerformanceBaselineTests \ --package-path ${{ matrix.path }} & test_pid=$! ( sleep 300 if kill -0 "$test_pid" 2>/dev/null; then echo "::group::Tests still running after 5 minutes — sampling before kill" for pid in $(pgrep -if 'swiftpm-testing|xctest|PackageTests' || true); do echo "--- sample of pid $pid ---" sample "$pid" 5 2>/dev/null || true done echo "::endgroup::" pkill -KILL -P "$test_pid" 2>/dev/null || true kill -KILL "$test_pid" 2>/dev/null || true fi ) & watchdog_pid=$! wait "$test_pid" && status=0 || status=$? kill "$watchdog_pid" 2>/dev/null || true exit "$status" # Benchmarks run serially on an otherwise idle runner for stable # numbers; BITCHAT_PERF_LOG captures the PERF[...] lines for the gate. - name: Run performance benchmarks (serial) if: matrix.name == 'app' timeout-minutes: 6 env: BITCHAT_PERF_LOG: ${{ github.workspace }}/perf-output.log run: swift test --quiet --filter PerformanceBaselineTests # Order-of-magnitude performance regression gate. Floors are deliberately # generous (see bitchatTests/Performance/perf-floors.json) so this # catches algorithmic regressions, never runner variance. If a metric # still lands below floor (a saturated runner can dip one), the script # re-runs the benchmarks — appending to the same log and keeping each # benchmark's best value per metric — so noise clears on retry while a # real regression fails every attempt. Floors are never lowered by this. - name: Performance floor gate if: matrix.name == 'app' timeout-minutes: 10 run: ./scripts/check-perf-floors.sh perf-output.log # Informational only: surfaces per-file and total line coverage in the # job log so coverage trends are visible on every PR. No thresholds — # this must never be the reason a build goes red. - name: Coverage summary run: | BIN_PATH=$(swift build --show-bin-path --package-path ${{ matrix.path }}) PROF="$BIN_PATH/codecov/default.profdata" XCTEST=$(find "$BIN_PATH" -maxdepth 1 -name '*.xctest' | head -1) BINARY="$XCTEST/Contents/MacOS/$(basename "$XCTEST" .xctest)" if [ -f "$PROF" ] && [ -f "$BINARY" ]; then xcrun llvm-cov report "$BINARY" -instr-profile "$PROF" \ -ignore-filename-regex='(Tests|\.build|checkouts|Mocks|_PreviewHelpers)' || true else echo "No coverage data found; skipping summary." fi # SPM tests do not link the shipping app targets. This job covers the # iOS-conditional paths and both universal Release link configurations. ios-build: name: Build Release apps (universal) runs-on: macos-latest timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v5 - name: Build iOS (simulator, no signing) # Build both simulator architectures so CI validates every vendored # Arti simulator slice and the configuration that ships. run: | set -o pipefail xcodebuild -project bitchat.xcodeproj \ -scheme "bitchat (iOS)" \ -configuration Release \ -sdk iphonesimulator \ -destination 'generic/platform=iOS Simulator' \ ARCHS='arm64 x86_64' \ ONLY_ACTIVE_ARCH=NO \ CODE_SIGNING_ALLOWED=NO \ build - name: Build macOS (universal, no signing) run: | set -o pipefail xcodebuild -project bitchat.xcodeproj \ -scheme "bitchat (macOS)" \ -configuration Release \ -destination 'generic/platform=macOS' \ ARCHS='arm64 x86_64' \ ONLY_ACTIVE_ARCH=NO \ CODE_SIGNING_ALLOWED=NO \ build # Advisory only: SwiftLint reports style violations without ever failing the # build. Runs in a pinned container (no Xcode plugin, no pbxproj changes) so # it can never break the documented xcodebuild path or block a merge. lint: name: SwiftLint (advisory) runs-on: ubuntu-latest timeout-minutes: 15 # This job runs a third-party container image, so give it the least # privilege we can: a read-only token, and no credentials left in the # checkout for the container to find. permissions: contents: read container: # Tag for readability, digest for immutability (tags can be repointed). # Bump both together, deliberately — never a floating tag. image: ghcr.io/realm/swiftlint:0.65.0@sha256:a482729f4b58741875af1566f23397f3f6db300372756fc31606d0a4527fab9e continue-on-error: true steps: - uses: actions/checkout@v5 with: persist-credentials: false - name: Run SwiftLint run: swiftlint lint --reporter github-actions-logging