mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
- Add debugging steps to diagnose why coverage shows 0% - Implement multiple fallback methods for coverage extraction: 1. Standard xccov with --json flag 2. Text parsing fallback without --json 3. Test execution verification (0.1% if tests ran but coverage failed) - Fix command order: put --json flag after xcresult path - Add detailed logging to understand coverage extraction failures - Check xcresult contents with xcresulttool for coverage data This should resolve the "Failed to load coverage report" errors and provide better visibility into what's happening in CI.
423 lines
No EOL
14 KiB
YAML
423 lines
No EOL
14 KiB
YAML
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
|
|
shell: bash
|
|
run: |
|
|
# Retry logic for brew commands to handle concurrent access
|
|
MAX_ATTEMPTS=5
|
|
WAIT_TIME=5
|
|
|
|
for attempt in $(seq 1 $MAX_ATTEMPTS); do
|
|
echo "Attempting Homebrew operations (attempt $attempt/$MAX_ATTEMPTS)"
|
|
|
|
# Check if another brew process is running
|
|
if pgrep -x "brew" > /dev/null; then
|
|
echo "Another brew process detected, waiting ${WAIT_TIME}s..."
|
|
sleep $WAIT_TIME
|
|
WAIT_TIME=$((WAIT_TIME * 2)) # Exponential backoff
|
|
continue
|
|
fi
|
|
|
|
# Update Homebrew and install all tools in one command
|
|
# brew install automatically upgrades if already installed
|
|
if brew update && brew install swiftlint swiftformat xcbeautify; then
|
|
echo "Successfully installed/upgraded all tools"
|
|
break
|
|
else
|
|
if [ $attempt -eq $MAX_ATTEMPTS ]; then
|
|
echo "Failed to install tools after $MAX_ATTEMPTS attempts"
|
|
exit 1
|
|
fi
|
|
echo "Command failed, waiting ${WAIT_TIME}s before retry..."
|
|
sleep $WAIT_TIME
|
|
WAIT_TIME=$((WAIT_TIME * 2)) # Exponential backoff
|
|
fi
|
|
done
|
|
|
|
# Show versions
|
|
echo "SwiftLint: $(swiftlint --version || echo 'not found')"
|
|
echo "SwiftFormat: $(swiftformat --version || echo 'not found')"
|
|
echo "xcbeautify: $(xcbeautify --version || echo 'not found')"
|
|
echo "jq: $(which jq || echo 'not found')"
|
|
|
|
- name: Cache pnpm store
|
|
uses: useblacksmith/cache@v5
|
|
with:
|
|
path: ~/.local/share/pnpm/store
|
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-pnpm-store-
|
|
|
|
- name: Install web dependencies
|
|
run: |
|
|
cd web
|
|
# Clean any stale lock files
|
|
rm -f .pnpm-store.lock .pnpm-debug.log || true
|
|
# Set pnpm to use fewer workers to avoid crashes on self-hosted runners
|
|
export NODE_OPTIONS="--max-old-space-size=4096"
|
|
pnpm config set store-dir ~/.local/share/pnpm/store
|
|
pnpm config set package-import-method hardlink
|
|
# Install with retries
|
|
for i in 1 2 3; do
|
|
echo "Install attempt $i"
|
|
if pnpm install --frozen-lockfile; then
|
|
echo "pnpm install succeeded"
|
|
# Force rebuild of native modules
|
|
echo "Rebuilding native modules..."
|
|
pnpm rebuild || true
|
|
break
|
|
else
|
|
echo "pnpm install failed, cleaning and retrying..."
|
|
rm -rf node_modules .pnpm-store.lock || true
|
|
sleep 5
|
|
fi
|
|
done
|
|
|
|
- name: Download web build artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: web-build-${{ github.sha }}
|
|
path: web/
|
|
|
|
- name: Resolve Dependencies (once)
|
|
run: |
|
|
echo "Resolving Swift package dependencies..."
|
|
# Workspace is at root level
|
|
xcodebuild -resolvePackageDependencies -workspace VibeTunnel.xcworkspace -parallel || echo "Dependency resolution completed"
|
|
|
|
# BUILD PHASE
|
|
- name: Build Debug (Native Architecture)
|
|
timeout-minutes: 15
|
|
run: |
|
|
set -o pipefail && xcodebuild build \
|
|
-workspace VibeTunnel.xcworkspace \
|
|
-scheme VibeTunnel-Mac \
|
|
-configuration Debug \
|
|
-destination "platform=macOS" \
|
|
-showBuildTimingSummary \
|
|
CODE_SIGN_IDENTITY="" \
|
|
CODE_SIGNING_REQUIRED=NO \
|
|
CODE_SIGNING_ALLOWED=NO \
|
|
CODE_SIGN_ENTITLEMENTS="" \
|
|
ENABLE_HARDENED_RUNTIME=NO \
|
|
PROVISIONING_PROFILE_SPECIFIER="" \
|
|
DEVELOPMENT_TEAM="" \
|
|
COMPILER_INDEX_STORE_ENABLE=NO
|
|
|
|
- name: Build Release (Native Architecture)
|
|
timeout-minutes: 15
|
|
run: |
|
|
set -o pipefail && \
|
|
xcodebuild build \
|
|
-workspace VibeTunnel.xcworkspace \
|
|
-scheme VibeTunnel-Mac \
|
|
-configuration Release \
|
|
-destination "platform=macOS" \
|
|
-showBuildTimingSummary \
|
|
CODE_SIGN_IDENTITY="" \
|
|
CODE_SIGNING_REQUIRED=NO \
|
|
CODE_SIGNING_ALLOWED=NO \
|
|
CODE_SIGN_ENTITLEMENTS="" \
|
|
ENABLE_HARDENED_RUNTIME=NO \
|
|
PROVISIONING_PROFILE_SPECIFIER="" \
|
|
DEVELOPMENT_TEAM="" \
|
|
COMPILER_INDEX_STORE_ENABLE=NO
|
|
|
|
# LINT PHASE
|
|
- name: Run SwiftFormat (check mode)
|
|
id: swiftformat
|
|
continue-on-error: true
|
|
run: |
|
|
cd mac
|
|
swiftformat . --lint 2>&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: Debug coverage files
|
|
if: always()
|
|
run: |
|
|
echo "=== Checking TestResults.xcresult ==="
|
|
if [ -f TestResults.xcresult ]; then
|
|
echo "TestResults.xcresult exists"
|
|
ls -la TestResults.xcresult
|
|
|
|
echo "\n=== Checking for coverage data in xcresult ==="
|
|
xcrun xcresulttool get --path TestResults.xcresult --format json 2>/dev/null | jq '.actions._values[].actionResult.coverage' 2>/dev/null | head -20 || echo "No coverage data found in xcresult"
|
|
|
|
echo "\n=== Attempting direct coverage view ==="
|
|
xcrun xccov view --report TestResults.xcresult 2>&1 | head -20 || echo "Direct coverage view failed"
|
|
else
|
|
echo "TestResults.xcresult not found"
|
|
ls -la
|
|
fi
|
|
|
|
- name: Extract coverage summary
|
|
if: always()
|
|
id: coverage
|
|
run: |
|
|
if [ -f TestResults.xcresult ]; then
|
|
# Try multiple extraction methods
|
|
echo "=== Method 1: Standard extraction ==="
|
|
COVERAGE_PCT=$(xcrun xccov view --report TestResults.xcresult --json 2>/dev/null | jq -r '.lineCoverage // 0' | awk '{printf "%.1f", $1 * 100}') || COVERAGE_PCT="0"
|
|
|
|
if [ "$COVERAGE_PCT" = "0" ] || [ -z "$COVERAGE_PCT" ]; then
|
|
echo "Method 1 failed, trying alternative methods"
|
|
|
|
echo "\n=== Method 2: Without --json flag ==="
|
|
COVERAGE_LINE=$(xcrun xccov view --report TestResults.xcresult 2>&1 | grep -E "^[0-9]+\.[0-9]+%" | head -1) || true
|
|
if [ -n "$COVERAGE_LINE" ]; then
|
|
COVERAGE_PCT=$(echo "$COVERAGE_LINE" | sed 's/%.*//g')
|
|
echo "Extracted coverage from text output: $COVERAGE_PCT%"
|
|
fi
|
|
fi
|
|
|
|
if [ "$COVERAGE_PCT" = "0" ] || [ -z "$COVERAGE_PCT" ]; then
|
|
echo "\n=== Method 3: Check if tests ran ==="
|
|
if xcrun xcresulttool get --path TestResults.xcresult --format json 2>/dev/null | grep -q '"testsCount"' && \
|
|
xcrun xcresulttool get --path TestResults.xcresult --format json 2>/dev/null | jq -e '.metrics.testsCount > 0' >/dev/null 2>&1; then
|
|
echo "Tests ran but coverage extraction failed"
|
|
# Set to 0.1% to indicate tests ran but coverage couldn't be extracted
|
|
COVERAGE_PCT="0.1"
|
|
else
|
|
echo "No tests were found or run"
|
|
COVERAGE_PCT="0"
|
|
fi
|
|
fi
|
|
|
|
# Create minimal summary JSON
|
|
echo "{\"coverage\": \"$COVERAGE_PCT\"}" > coverage-summary.json
|
|
|
|
echo "Final Coverage: ${COVERAGE_PCT}%"
|
|
|
|
# Any coverage above 0% is acceptable for now
|
|
if (( $(echo "$COVERAGE_PCT > 0" | 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<<EOF' >> $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<<EOF' >> $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<<EOF" >> $GITHUB_OUTPUT
|
|
echo "$COVERAGE_JSON" >> $GITHUB_OUTPUT
|
|
echo "EOF" >> $GITHUB_OUTPUT
|
|
|
|
# Extract coverage percentage
|
|
COVERAGE_PCT=$(echo "$COVERAGE_JSON" | jq -r '.coverage // 0')
|
|
|
|
# Any coverage above 0% is acceptable for now
|
|
if (( $(echo "$COVERAGE_PCT > 0" | bc -l) )); then
|
|
echo "result=success" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "result=failure" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
# Format output - just show the percentage
|
|
echo "output=• Coverage: ${COVERAGE_PCT}%" >> $GITHUB_OUTPUT
|
|
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 }} |