vibetunnel/.github/workflows/mac.yml
Peter Steinberger ac55f9685c ci: simplify Mac workflow by removing Node.js dependency
Remove the dependency on Node.js CI job for Mac and iOS builds, and
always build web artifacts locally in the Mac workflow. This simplifies
the CI pipeline and removes the artifact download step that could fail.

Changes:
- Remove `node` dependency from `mac` and `ios` jobs in ci.yml
- Replace artifact download with direct web build in mac.yml
- Simplify the build process by removing conditional logic

This extracts the CI workflow improvements from PR #318.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 19:20:17 +02:00

446 lines
No EOL
15 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: actions/cache@v4
continue-on-error: true
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: actions/cache@v4
continue-on-error: true
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: actions/cache@v4
continue-on-error: true
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: Build web artifacts
run: |
echo "Building web artifacts locally..."
cd web
# Skip custom Node.js build in CI to avoid timeout
export CI=true
pnpm run build
echo "Web artifacts built successfully"
- 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"
# Debug: List available schemes
echo "=== Available schemes ==="
xcodebuild -list -workspace VibeTunnel.xcworkspace | grep -A 20 "Schemes:" || true
# 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: 15
run: |
# Debug: Check if web build artifacts were downloaded
echo "=== Checking web build artifacts ==="
if [ -d "web/dist" ]; then
echo "web/dist exists"
ls -la web/dist | head -10
else
echo "web/dist does not exist"
fi
# 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
# 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 [ -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-2404-arm
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 }}