mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-31 10:25:57 +00:00
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>
446 lines
No EOL
15 KiB
YAML
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 }} |