Optimize CI performance: remove duplicate web builds, parallelize tasks, improve caching (#399)

This commit is contained in:
Peter Steinberger 2025-07-18 08:01:23 +02:00 committed by GitHub
parent 412aa3c035
commit 453d888731
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 175 additions and 160 deletions

View file

@ -52,7 +52,7 @@ jobs:
mac: mac:
name: Mac CI name: Mac CI
needs: [changes] needs: [changes, node]
if: | if: |
always() && always() &&
!contains(needs.*.result, 'failure') && !contains(needs.*.result, 'failure') &&

View file

@ -32,16 +32,7 @@ jobs:
xcodebuild -version xcodebuild -version
swift --version swift --version
- name: Setup Node.js # Node.js/pnpm not needed for iOS builds
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Cache Homebrew packages - name: Cache Homebrew packages
uses: actions/cache@v4 uses: actions/cache@v4
@ -70,6 +61,9 @@ jobs:
- name: Install all tools - name: Install all tools
shell: bash shell: bash
run: | run: |
# Skip Homebrew update for faster CI
export HOMEBREW_NO_AUTO_UPDATE=1
# Retry logic for brew commands to handle concurrent access # Retry logic for brew commands to handle concurrent access
MAX_ATTEMPTS=5 MAX_ATTEMPTS=5
WAIT_TIME=5 WAIT_TIME=5
@ -85,9 +79,9 @@ jobs:
continue continue
fi fi
# Update Homebrew and install all tools in one command # Install tools without updating Homebrew
# brew install automatically upgrades if already installed # brew install automatically upgrades if already installed
if brew update && brew install swiftlint swiftformat xcbeautify; then if brew install swiftlint swiftformat xcbeautify; then
echo "Successfully installed/upgraded all tools" echo "Successfully installed/upgraded all tools"
break break
else else
@ -107,37 +101,8 @@ jobs:
echo "xcbeautify: $(xcbeautify --version || echo 'not found')" echo "xcbeautify: $(xcbeautify --version || echo 'not found')"
echo "PATH: $PATH" echo "PATH: $PATH"
- name: Cache pnpm store # iOS doesn't need web dependencies - skip pnpm entirely
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 copy
pnpm config set node-linker hoisted
# Install with retries
for i in 1 2 3; do
echo "Install attempt $i"
if pnpm install --frozen-lockfile; then
echo "pnpm install succeeded"
break
else
echo "pnpm install failed, cleaning and retrying..."
rm -rf node_modules .pnpm-store.lock || true
sleep 5
fi
done
- name: Resolve Dependencies (once) - name: Resolve Dependencies (once)
run: | run: |
cd ios cd ios
@ -151,6 +116,7 @@ jobs:
# Ensure xcbeautify is in PATH # Ensure xcbeautify is in PATH
export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH" export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
# Use Release config for faster builds
set -o pipefail set -o pipefail
xcodebuild build \ xcodebuild build \
-workspace ../VibeTunnel.xcworkspace \ -workspace ../VibeTunnel.xcworkspace \
@ -158,14 +124,13 @@ jobs:
-destination "generic/platform=iOS" \ -destination "generic/platform=iOS" \
-configuration Release \ -configuration Release \
-showBuildTimingSummary \ -showBuildTimingSummary \
-quiet \
CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_REQUIRED=NO \
ONLY_ACTIVE_ARCH=NO \ ONLY_ACTIVE_ARCH=NO \
-derivedDataPath build/DerivedData \ -derivedDataPath build/DerivedData \
COMPILER_INDEX_STORE_ENABLE=NO \ COMPILER_INDEX_STORE_ENABLE=NO || {
2>&1 | tee build.log || { echo "::error::Build failed"
echo "Build failed. Last 100 lines of output:"
tail -100 build.log
exit 1 exit 1
} }
@ -311,13 +276,20 @@ jobs:
exit 1 exit 1
fi fi
# 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
set -o pipefail set -o pipefail
xcodebuild test \ xcodebuild test \
-workspace ../VibeTunnel.xcworkspace \ -workspace ../VibeTunnel.xcworkspace \
-scheme VibeTunnel-iOS \ -scheme VibeTunnel-iOS \
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \ -destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
-resultBundlePath TestResults.xcresult \ -resultBundlePath TestResults.xcresult \
-enableCodeCoverage YES \ -enableCodeCoverage $ENABLE_COVERAGE \
CODE_SIGN_IDENTITY="" \ CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_ALLOWED=NO \
@ -419,13 +391,14 @@ jobs:
fi fi
# ARTIFACT UPLOADS # ARTIFACT UPLOADS
# Skip build artifact upload for PR builds to save time
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: success() if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with: with:
name: ios-build-artifacts name: ios-build-artifacts
path: ios/build/DerivedData/Build/Products/Release-iphoneos/ path: ios/build/DerivedData/Build/Products/Release-iphoneos/
retention-days: 7 retention-days: 3
- name: Upload coverage artifacts - name: Upload coverage artifacts
if: always() if: always()
@ -484,7 +457,8 @@ jobs:
name: Report iOS Coverage name: Report iOS Coverage
runs-on: blacksmith-8vcpu-ubuntu-2404-arm runs-on: blacksmith-8vcpu-ubuntu-2404-arm
needs: [build-lint-test] needs: [build-lint-test]
if: always() && github.event_name == 'pull_request' # Only run coverage reporting on main branch where we actually collect coverage
if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main'
steps: steps:
- name: Clean workspace - name: Clean workspace

View file

@ -32,16 +32,7 @@ jobs:
xcodebuild -version xcodebuild -version
swift --version swift --version
- name: Setup Node.js # Node.js/pnpm not needed - web artifacts are downloaded
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 - name: Cache Homebrew packages
uses: actions/cache@v4 uses: actions/cache@v4
@ -61,15 +52,29 @@ jobs:
continue-on-error: true continue-on-error: true
with: with:
path: | path: |
~/Library/Developer/Xcode/DerivedData
~/.swiftpm ~/.swiftpm
key: ${{ runner.os }}-spm-${{ hashFiles('mac/Package.resolved') }} key: ${{ runner.os }}-spm-${{ hashFiles('mac/Package.resolved') }}
restore-keys: | restore-keys: |
${{ runner.os }}-spm- ${{ runner.os }}-spm-
- name: Cache Xcode derived data
uses: actions/cache@v4
continue-on-error: true
with:
path: |
~/Library/Developer/Xcode/DerivedData/**/Build/Products
~/Library/Developer/Xcode/DerivedData/**/Build/Intermediates.noindex
~/Library/Developer/Xcode/DerivedData/**/SourcePackages
key: ${{ runner.os }}-xcode-build-${{ hashFiles('mac/**/*.swift', 'mac/**/*.h', 'mac/**/*.m') }}
restore-keys: |
${{ runner.os }}-xcode-build-
- name: Install all tools - name: Install all tools
shell: bash shell: bash
run: | run: |
# Skip Homebrew update for faster CI
export HOMEBREW_NO_AUTO_UPDATE=1
# Retry logic for brew commands to handle concurrent access # Retry logic for brew commands to handle concurrent access
MAX_ATTEMPTS=5 MAX_ATTEMPTS=5
WAIT_TIME=5 WAIT_TIME=5
@ -85,9 +90,9 @@ jobs:
continue continue
fi fi
# Update Homebrew and install all tools in one command # Install tools without updating Homebrew
# brew install automatically upgrades if already installed # brew install automatically upgrades if already installed
if brew update && brew install swiftlint swiftformat xcbeautify; then if brew install swiftlint swiftformat xcbeautify; then
echo "Successfully installed/upgraded all tools" echo "Successfully installed/upgraded all tools"
break break
else else
@ -107,48 +112,54 @@ jobs:
echo "xcbeautify: $(xcbeautify --version || echo 'not found')" echo "xcbeautify: $(xcbeautify --version || echo 'not found')"
echo "jq: $(which jq || echo 'not found')" echo "jq: $(which jq || echo 'not found')"
- name: Cache pnpm store # Skip pnpm cache - testing if fresh installs are faster
uses: actions/cache@v4 # The cache was extremely large and might be slower than fresh install
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 - name: Download web artifacts from Node.js CI
uses: actions/download-artifact@v4
with:
name: web-build
path: web-artifacts-temp/
- name: Move web artifacts to correct location
run: | run: |
echo "Building web artifacts locally..." # Debug: Show what was downloaded
cd web echo "=== Contents of web-artifacts-temp ==="
# Skip custom Node.js build in CI to avoid timeout find web-artifacts-temp -type f | head -20 || echo "No files found"
export CI=true
pnpm run build # Ensure web directory structure exists
echo "Web artifacts built successfully" mkdir -p web/dist web/public/bundle
# The artifacts are uploaded without the web/ prefix
# So they're at web-artifacts-temp/dist and web-artifacts-temp/public/bundle
if [ -d "web-artifacts-temp/dist" ]; then
# Copy from the root of artifacts
cp -r web-artifacts-temp/dist/* web/dist/ 2>/dev/null || true
echo "Copied dist files"
fi
if [ -d "web-artifacts-temp/public/bundle" ]; then
cp -r web-artifacts-temp/public/bundle/* web/public/bundle/ 2>/dev/null || true
echo "Copied bundle files"
fi
# Debug: Show what we have
echo "=== Web directory structure ==="
ls -la web/ || true
echo "=== Dist contents ==="
ls -la web/dist/ | head -10 || true
echo "=== Bundle contents ==="
ls -la web/public/bundle/ | head -10 || true
# Clean up temp directory
rm -rf web-artifacts-temp
# Verify we have the required files
if [ ! -f "web/dist/server/server.js" ]; then
echo "ERROR: web/dist/server/server.js not found after artifact extraction!"
exit 1
fi
echo "Web artifacts successfully downloaded and positioned"
- name: Resolve Dependencies (once) - name: Resolve Dependencies (once)
run: | run: |
@ -161,13 +172,17 @@ jobs:
xcodebuild -list -workspace VibeTunnel.xcworkspace | grep -A 20 "Schemes:" || true xcodebuild -list -workspace VibeTunnel.xcworkspace | grep -A 20 "Schemes:" || true
# BUILD PHASE # BUILD PHASE
- name: Build Debug (Native Architecture) - name: Build Debug
timeout-minutes: 15 timeout-minutes: 10
id: build
run: | run: |
# Always use Debug for now to match test expectations
BUILD_CONFIG="Debug"
set -o pipefail && xcodebuild build \ set -o pipefail && xcodebuild build \
-workspace VibeTunnel.xcworkspace \ -workspace VibeTunnel.xcworkspace \
-scheme VibeTunnel-Mac \ -scheme VibeTunnel-Mac \
-configuration Debug \ -configuration $BUILD_CONFIG \
-destination "platform=macOS" \ -destination "platform=macOS" \
-showBuildTimingSummary \ -showBuildTimingSummary \
CODE_SIGN_IDENTITY="" \ CODE_SIGN_IDENTITY="" \
@ -177,9 +192,12 @@ jobs:
ENABLE_HARDENED_RUNTIME=NO \ ENABLE_HARDENED_RUNTIME=NO \
PROVISIONING_PROFILE_SPECIFIER="" \ PROVISIONING_PROFILE_SPECIFIER="" \
DEVELOPMENT_TEAM="" \ DEVELOPMENT_TEAM="" \
COMPILER_INDEX_STORE_ENABLE=NO COMPILER_INDEX_STORE_ENABLE=NO || {
echo "::error::Build failed"
exit 1
}
# LINT PHASE # LINT PHASE (after build to avoid conflicts)
- name: Run SwiftFormat (check mode) - name: Run SwiftFormat (check mode)
id: swiftformat id: swiftformat
continue-on-error: true continue-on-error: true
@ -210,15 +228,26 @@ jobs:
echo "web/dist does not exist" echo "web/dist does not exist"
fi fi
# Use xcodebuild test for workspace testing with coverage enabled # 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 && \ set -o pipefail && \
xcodebuild test \ xcodebuild test \
-workspace VibeTunnel.xcworkspace \ -workspace VibeTunnel.xcworkspace \
-scheme VibeTunnel-Mac \ -scheme VibeTunnel-Mac \
-configuration Debug \ -configuration $TEST_CONFIG \
-destination "platform=macOS" \ -destination "platform=macOS" \
-enableCodeCoverage YES \ -enableCodeCoverage $ENABLE_COVERAGE \
-resultBundlePath TestResults.xcresult \ -resultBundlePath TestResults.xcresult \
-quiet \
CODE_SIGN_IDENTITY="" \ CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_ALLOWED=NO \
@ -308,13 +337,16 @@ jobs:
echo "Searching for build products..." echo "Searching for build products..."
find ~/Library/Developer/Xcode/DerivedData -name "VibeTunnel.app" -type d 2>/dev/null || echo "No build products found" 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 - name: Upload build artifacts
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: mac-build-artifacts name: mac-build-artifacts
path: | path: |
~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug/VibeTunnel.app ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug/VibeTunnel.app
~/Library/Developer/Xcode/DerivedData/*/Build/Products/Release/VibeTunnel.app ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Release/VibeTunnel.app
retention-days: 3
- name: Upload coverage artifacts - name: Upload coverage artifacts
if: always() if: always()
@ -372,7 +404,8 @@ jobs:
name: Report Coverage Results name: Report Coverage Results
runs-on: blacksmith-8vcpu-ubuntu-2404-arm runs-on: blacksmith-8vcpu-ubuntu-2404-arm
needs: [build-lint-test] needs: [build-lint-test]
if: always() && github.event_name == 'pull_request' # Only run coverage reporting on main branch where we actually collect coverage
if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main'
steps: steps:
- name: Clean workspace - name: Clean workspace

View file

@ -33,19 +33,8 @@ jobs:
version: 9 version: 9
run_install: false run_install: false
- name: Get pnpm store directory # Skip pnpm cache - testing if fresh installs are faster
shell: bash # The cache was extremely large and might be slower than fresh install
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: useblacksmith/cache@v5
continue-on-error: true
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install system dependencies - name: Install system dependencies
run: | run: |
@ -54,7 +43,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: web working-directory: web
run: pnpm install --frozen-lockfile run: |
pnpm config set network-concurrency 4
pnpm config set child-concurrency 2
pnpm install --frozen-lockfile --prefer-offline
- name: Check formatting with Biome - name: Check formatting with Biome
id: biome-format id: biome-format
@ -137,19 +129,8 @@ jobs:
version: 9 version: 9
run_install: false run_install: false
- name: Get pnpm store directory # Skip pnpm cache - testing if fresh installs are faster
shell: bash # The cache was extremely large and might be slower than fresh install
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: useblacksmith/cache@v5
continue-on-error: true
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install system dependencies - name: Install system dependencies
run: | run: |
@ -170,7 +151,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: web working-directory: web
run: pnpm install --frozen-lockfile run: |
pnpm config set network-concurrency 4
pnpm config set child-concurrency 2
pnpm install --frozen-lockfile --prefer-offline
- name: Build node-pty - name: Build node-pty
working-directory: web working-directory: web
@ -179,7 +163,10 @@ jobs:
- name: Build frontend and backend - name: Build frontend and backend
working-directory: web working-directory: web
run: pnpm run build:ci run: |
# Use all available cores for esbuild
export ESBUILD_MAX_WORKERS=$(nproc)
pnpm run build:ci
- name: Run client tests with coverage - name: Run client tests with coverage
id: test-client-coverage id: test-client-coverage
@ -279,14 +266,23 @@ jobs:
web/coverage/client/lcov.info web/coverage/client/lcov.info
web/coverage/server/lcov.info web/coverage/server/lcov.info
- name: List build artifacts before upload
working-directory: web
run: |
echo "=== Contents of dist directory ==="
find dist -type f | head -20 || echo "No files in dist"
echo "=== Contents of public/bundle directory ==="
find public/bundle -type f | head -20 || echo "No files in public/bundle"
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: web-build-${{ github.sha }} name: web-build
path: | path: |
web/dist/ web/dist/
web/public/bundle/ web/public/bundle/
retention-days: 1 retention-days: 1
if-no-files-found: error
type-check: type-check:
name: TypeScript Type Checking name: TypeScript Type Checking
@ -309,19 +305,8 @@ jobs:
version: 9 version: 9
run_install: false run_install: false
- name: Get pnpm store directory # Skip pnpm cache - testing if fresh installs are faster
shell: bash # The cache was extremely large and might be slower than fresh install
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: useblacksmith/cache@v5
continue-on-error: true
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install system dependencies - name: Install system dependencies
run: | run: |
@ -330,7 +315,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: web working-directory: web
run: pnpm install --frozen-lockfile run: |
pnpm config set network-concurrency 4
pnpm config set child-concurrency 2
pnpm install --frozen-lockfile --prefer-offline
- name: Build node-pty for TypeScript - name: Build node-pty for TypeScript
working-directory: web working-directory: web
@ -366,9 +354,10 @@ jobs:
# || true to not fail the build on vulnerabilities, but still report them # || true to not fail the build on vulnerabilities, but still report them
report-coverage: report-coverage:
name: Report Coverage Results name: Report Coverage Results
runs-on: blacksmith-8vcpu-ubuntu-2404-arm runs-on: blacksmith-8vcpu-ubuntu-2404-arm
needs: [build-and-test] needs: [build-and-test]
# Keep Node.js coverage reporting for PRs since it's fast
if: always() && github.event_name == 'pull_request' if: always() && github.event_name == 'pull_request'
steps: steps:

View file

@ -25,7 +25,26 @@ execSync('esbuild src/client/sw.ts --bundle --outfile=public/sw.js --format=iife
// Build server TypeScript // Build server TypeScript
console.log('Building server...'); console.log('Building server...');
execSync('tsc', { stdio: 'inherit' }); // Force a clean build in CI to avoid incremental build issues
execSync('npx tsc --build --force', { stdio: 'inherit' });
// Verify dist directory exists
if (fs.existsSync(path.join(__dirname, '../dist'))) {
const files = fs.readdirSync(path.join(__dirname, '../dist'));
console.log(`Server build created ${files.length} files in dist/`);
console.log('Files in dist:', files.join(', '));
// Check for the essential server.js file
if (!fs.existsSync(path.join(__dirname, '../dist/server/server.js'))) {
console.error('ERROR: dist/server/server.js not found after tsc build!');
console.log('Contents of dist directory:');
execSync('find dist -type f | head -20', { stdio: 'inherit', cwd: path.join(__dirname, '..') });
process.exit(1);
}
} else {
console.error('ERROR: dist directory does not exist after tsc build!');
process.exit(1);
}
// Skip native executable build in CI // Skip native executable build in CI
console.log('Skipping native executable build in CI environment...'); console.log('Skipping native executable build in CI environment...');