vibetunnel/.github/workflows/mac.yml
2025-08-03 20:03:26 +02:00

435 lines
No EOL
16 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
# Clean workspace but preserve .git directory
find . -maxdepth 1 -name '.*' -not -name '.git' -not -name '.' -not -name '..' -exec rm -rf {} + || true
rm -rf * || true
- name: Checkout code
uses: actions/checkout@v4
- name: Verify Xcode
run: |
# Check available Xcode versions
echo "Available Xcode versions:"
ls -la /Applications/ | grep -i xcode || true
# Check if stable Xcode is available
if [ -d "/Applications/Xcode.app" ]; then
echo "Stable Xcode found, switching to it..."
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
elif [ -d "/Applications/Xcode-15.app" ]; then
echo "Xcode 15 found, switching to it..."
sudo xcode-select -s /Applications/Xcode-15.app/Contents/Developer
else
echo "Using current Xcode installation with actool workaround..."
fi
xcodebuild -version
swift --version
# Node.js/pnpm not needed - web artifacts are downloaded
- name: Cache Homebrew packages
uses: actions/cache@v4
continue-on-error: true
with:
path: |
/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: actions/cache@v4
continue-on-error: true
with:
path: |
~/.swiftpm
key: ${{ runner.os }}-spm-${{ hashFiles('mac/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
# Xcode derived data cache disabled due to self-hosted runner filesystem issues
# - name: Cache Xcode derived data
- name: Install all tools
shell: bash
run: |
# Skip Homebrew update for faster CI
export HOMEBREW_NO_AUTO_UPDATE=1
# 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
# Install tools without updating Homebrew
# brew install automatically upgrades if already installed
if 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, continuing without them"
# Don't exit 1, continue with build (tools may already be installed)
break
fi
echo "Command failed, waiting ${WAIT_TIME}s before retry..."
sleep $WAIT_TIME
WAIT_TIME=$((WAIT_TIME * 2)) # Exponential backoff
fi
done
# Verify which tools are available
echo "Tool availability check:"
which swiftlint || echo "swiftlint not available (linting will be skipped)"
which swiftformat || echo "swiftformat not available (formatting will be skipped)"
which xcbeautify || echo "xcbeautify not available (output formatting will be basic)"
# 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')"
# No web artifact caching - Mac build will handle web build directly
- name: Resolve Dependencies (once)
env:
CI: "true" # Ensure CI environment variable is set
run: |
echo "Resolving Swift package dependencies..."
# Workspace is at root level
xcodebuild -resolvePackageDependencies -workspace VibeTunnel.xcworkspace -scheme VibeTunnel || echo "Dependency resolution completed"
# Debug: List available schemes
echo "=== Available schemes ==="
xcodebuild -list -workspace VibeTunnel.xcworkspace | grep -A 20 "Schemes:" || true
# BUILD PHASE
- name: Build Debug
timeout-minutes: 10
id: build
env:
CI: "true" # Ensure CI environment variable is set for build scripts
# Workaround for Xcode beta actool version info issues
ACTOOL_IGNORE_VERSION_CHECK: "1"
IBTOOL_IGNORE_VERSION_CHECK: "1"
run: |
# Always use Debug for now to match test expectations
BUILD_CONFIG="Debug"
set -o pipefail && xcodebuild build \
-workspace VibeTunnel.xcworkspace \
-scheme VibeTunnel \
-configuration $BUILD_CONFIG \
-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 \
ACTOOL_IGNORE_VERSION_CHECK=1 \
IBTOOL_IGNORE_VERSION_CHECK=1 || {
echo "::error::Build failed"
exit 1
}
# LINT PHASE (after build to avoid conflicts)
- 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: 20 # Increased from 15 for CI stability
env:
RUN_SLOW_TESTS: "false" # Skip slow tests in CI by default
RUN_FLAKY_TESTS: "false" # Skip flaky tests in CI by default
# Workaround for Xcode beta actool version info issues
ACTOOL_IGNORE_VERSION_CHECK: "1"
IBTOOL_IGNORE_VERSION_CHECK: "1"
run: |
# Use xcodebuild test for workspace testing
# Only enable coverage on main branch
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
ENABLE_COVERAGE="YES"
else
ENABLE_COVERAGE="NO"
fi
# Always use Debug for tests
TEST_CONFIG="Debug"
set -o pipefail && \
xcodebuild test \
-workspace VibeTunnel.xcworkspace \
-scheme VibeTunnel \
-configuration $TEST_CONFIG \
-destination "platform=macOS" \
-enableCodeCoverage $ENABLE_COVERAGE \
-resultBundlePath TestResults.xcresult \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
COMPILER_INDEX_STORE_ENABLE=NO \
ACTOOL_IGNORE_VERSION_CHECK=1 \
IBTOOL_IGNORE_VERSION_CHECK=1 || {
echo "::error::Tests failed"
echo "result=1" >> $GITHUB_OUTPUT
# Try to get more detailed error information
echo "=== Attempting to get test failure details ==="
xcrun xcresulttool get --path TestResults.xcresult --format json 2>/dev/null | jq '.issues._values[]? | select(.severity == "error")' 2>/dev/null || true
exit 1
}
echo "result=0" >> $GITHUB_OUTPUT
# COVERAGE EXTRACTION
- name: Debug coverage files
if: always()
run: |
echo "=== Checking TestResults.xcresult ==="
if [ -d 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 [ -d 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"
# Skip build artifact upload for PR builds to save time
- name: Upload build artifacts
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
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
retention-days: 3
- 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-2404-arm
needs: [build-lint-test]
# Only run coverage reporting on main branch where we actually collect coverage
if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Clean workspace
run: |
# Clean workspace for self-hosted runner
# Clean workspace but preserve .git directory
find . -maxdepth 1 -name '.*' -not -name '.git' -not -name '.' -not -name '..' -exec 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 }}