name: Build & Test on: push: branches: - main pull_request: workflow_dispatch: 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). timeout-minutes: 15 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 - name: Compute Swift cache salt id: swift_cache_salt run: | { swift --version xcrun --show-sdk-platform-path xcrun --show-sdk-version xcodebuild -version } | shasum -a 256 | awk '{ print "value=" $1 }' >> "$GITHUB_OUTPUT" - name: Cache build artifacts uses: actions/cache@v4 with: path: ${{ matrix.path }}/.build key: ${{ runner.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ steps.swift_cache_salt.outputs.value }}-${{ hashFiles(format('{0}/**/*.swift', matrix.path), format('{0}/**/Package.resolved', matrix.path)) }} restore-keys: | ${{ runner.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ steps.swift_cache_salt.outputs.value }}-${{ hashFiles(format('{0}/**/Package.resolved', matrix.path)) }} ${{ runner.os }}-${{ runner.arch }}-${{ matrix.name }}-${{ steps.swift_cache_salt.outputs.value }}- - 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. - name: Performance floor gate if: matrix.name == 'app' 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 above only compile the macOS slice; this job covers the # iOS-conditional code paths (UIKit, CoreBluetooth restoration, etc.). ios-build: name: Build iOS app (simulator) runs-on: macos-latest timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v5 - name: Build iOS (simulator, no signing) # arm64 only: the vendored arti.xcframework has no x86_64 simulator slice. run: | set -o pipefail xcodebuild -project bitchat.xcodeproj \ -scheme "bitchat (iOS)" \ -sdk iphonesimulator \ -destination 'generic/platform=iOS Simulator' \ ARCHS=arm64 \ CODE_SIGNING_ALLOWED=NO \ build