name: Mac CI on: workflow_call: permissions: contents: read pull-requests: write issues: write # Single job for efficient execution on shared runner jobs: build-lint-test: name: Build, Lint, and Test macOS runs-on: [self-hosted, macOS, ARM64] timeout-minutes: 40 env: GITHUB_REPO_NAME: ${{ github.repository }} steps: - name: Clean workspace run: | # Clean workspace for self-hosted runner rm -rf * || true rm -rf .* || true - name: Checkout code uses: actions/checkout@v4 - name: Verify Xcode run: | xcodebuild -version swift --version - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '24' - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9 dest: ~/pnpm-${{ github.run_id }} - name: Cache Homebrew packages uses: useblacksmith/cache@v5 with: path: | ~/Library/Caches/Homebrew /opt/homebrew/Cellar/swiftlint /opt/homebrew/Cellar/swiftformat /opt/homebrew/Cellar/xcbeautify key: ${{ runner.os }}-brew-${{ hashFiles('.github/workflows/mac.yml') }} restore-keys: | ${{ runner.os }}-brew- - name: Cache Swift packages uses: useblacksmith/cache@v5 with: path: | ~/Library/Developer/Xcode/DerivedData ~/.swiftpm key: ${{ runner.os }}-spm-${{ hashFiles('mac/Package.resolved') }} restore-keys: | ${{ runner.os }}-spm- - name: Install all tools continue-on-error: true shell: bash run: | # Install linting and build tools cat > Brewfile <&1 | tee ../swiftformat-output.txt echo "result=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT - name: Run SwiftLint id: swiftlint continue-on-error: true run: | cd mac swiftlint 2>&1 | tee ../swiftlint-output.txt echo "result=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT # TEST PHASE - name: Run tests with coverage id: test-coverage timeout-minutes: 10 run: | # Use xcodebuild test for workspace testing with coverage enabled set -o pipefail && \ xcodebuild test \ -workspace VibeTunnel.xcworkspace \ -scheme VibeTunnel-Mac \ -configuration Debug \ -destination "platform=macOS" \ -enableCodeCoverage YES \ -resultBundlePath TestResults.xcresult \ CODE_SIGN_IDENTITY="" \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_ALLOWED=NO \ COMPILER_INDEX_STORE_ENABLE=NO || { echo "::error::Tests failed" echo "result=1" >> $GITHUB_OUTPUT exit 1 } echo "result=0" >> $GITHUB_OUTPUT # COVERAGE EXTRACTION - name: Extract coverage summary if: always() id: coverage run: | if [ -f TestResults.xcresult ]; then # Use faster xcrun command to extract coverage percentage COVERAGE_PCT=$(xcrun xccov view --report --json TestResults.xcresult 2>/dev/null | jq -r '.lineCoverage // 0' | awk '{printf "%.1f", $1 * 100}') || { echo "::warning::Failed to extract coverage with xccov" echo '{"error": "Failed to extract coverage data"}' > coverage-summary.json echo "coverage_result=failure" >> $GITHUB_OUTPUT exit 0 } # Create minimal summary JSON echo "{\"coverage\": \"$COVERAGE_PCT\"}" > coverage-summary.json echo "Coverage: ${COVERAGE_PCT}%" # Check if coverage meets threshold (75% for Swift projects) THRESHOLD=75 if (( $(echo "$COVERAGE_PCT >= $THRESHOLD" | bc -l) )); then echo "coverage_result=success" >> $GITHUB_OUTPUT else echo "coverage_result=failure" >> $GITHUB_OUTPUT fi else echo '{"error": "No test results bundle found"}' > coverage-summary.json echo "coverage_result=failure" >> $GITHUB_OUTPUT fi # ARTIFACT UPLOADS - name: List build products if: always() run: | echo "Searching for build products..." find ~/Library/Developer/Xcode/DerivedData -name "VibeTunnel.app" -type d 2>/dev/null || echo "No build products found" - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: mac-build-artifacts path: | ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug/VibeTunnel.app ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Release/VibeTunnel.app - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@v4 with: name: mac-coverage path: | coverage-summary.json TestResults.xcresult # LINT REPORTING - name: Read SwiftFormat Output if: always() id: swiftformat-output run: | if [ -f swiftformat-output.txt ]; then echo 'content<> $GITHUB_OUTPUT cat swiftformat-output.txt >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT else echo "content=No output" >> $GITHUB_OUTPUT fi - name: Read SwiftLint Output if: always() id: swiftlint-output run: | if [ -f swiftlint-output.txt ]; then echo 'content<> $GITHUB_OUTPUT cat swiftlint-output.txt >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT else echo "content=No output" >> $GITHUB_OUTPUT fi - name: Report SwiftFormat Results if: always() uses: ./.github/actions/lint-reporter with: title: 'Mac Formatting (SwiftFormat)' lint-result: ${{ steps.swiftformat.outputs.result == '0' && 'success' || 'failure' }} lint-output: ${{ steps.swiftformat-output.outputs.content }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Report SwiftLint Results if: always() uses: ./.github/actions/lint-reporter with: title: 'Mac Linting (SwiftLint)' lint-result: ${{ steps.swiftlint.outputs.result == '0' && 'success' || 'failure' }} lint-output: ${{ steps.swiftlint-output.outputs.content }} github-token: ${{ secrets.GITHUB_TOKEN }} report-coverage: name: Report Coverage Results runs-on: blacksmith-8vcpu-ubuntu-2204 needs: [build-lint-test] if: always() && github.event_name == 'pull_request' steps: - name: Clean workspace run: | # Clean workspace for self-hosted runner rm -rf * || true rm -rf .* || true - name: Checkout code uses: actions/checkout@v4 - name: Download coverage artifacts uses: actions/download-artifact@v4 with: name: mac-coverage path: mac-coverage-artifacts - name: Read coverage summary id: coverage run: | if [ -f mac-coverage-artifacts/coverage-summary.json ]; then # Read the coverage summary COVERAGE_JSON=$(cat mac-coverage-artifacts/coverage-summary.json) echo "summary<> $GITHUB_OUTPUT echo "$COVERAGE_JSON" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT # Extract coverage percentage COVERAGE_PCT=$(echo "$COVERAGE_JSON" | jq -r '.coverage // 0') # Check if coverage meets threshold (75% for Swift) THRESHOLD=75 if (( $(echo "$COVERAGE_PCT >= $THRESHOLD" | bc -l) )); then echo "result=success" >> $GITHUB_OUTPUT else echo "result=failure" >> $GITHUB_OUTPUT fi # Format output with warning indicator if below threshold if (( $(echo "$COVERAGE_PCT < $THRESHOLD" | bc -l) )); then echo "output=• Coverage: ${COVERAGE_PCT}% ⚠️ (threshold: ${THRESHOLD}%)" >> $GITHUB_OUTPUT else echo "output=• Coverage: ${COVERAGE_PCT}% (threshold: ${THRESHOLD}%)" >> $GITHUB_OUTPUT fi else echo "summary={\"error\": \"No coverage data found\"}" >> $GITHUB_OUTPUT echo "result=failure" >> $GITHUB_OUTPUT echo "output=Coverage data not found" >> $GITHUB_OUTPUT fi - name: Report Coverage Results uses: ./.github/actions/lint-reporter with: title: 'macOS Test Coverage' lint-result: ${{ steps.coverage.outputs.result }} lint-output: ${{ steps.coverage.outputs.output }} github-token: ${{ secrets.GITHUB_TOKEN }}