From b6c4bbf195c8fd8e3a1e7a854d30ead08e95a765 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+Gricha@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:06:32 +0000 Subject: [PATCH] Optimize test CI with parallel jobs and BuildKit caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key optimizations: 1. **Parallel jobs**: Split into lint, build, and test jobs - lint: Runs linting, format check, and typecheck in parallel with build - build: Builds CLI and web UI, uploads artifacts - test: Downloads artifacts and runs integration tests 2. **Smart Docker caching with BuildKit**: - Always builds Docker image (ensures tests use correct image) - Uses registry-based layer caching (cache-from) - Detects perry/ directory changes to decide when to update cache - Fast builds (~30s) when using cached layers - Full rebuild when Dockerfile/scripts change 3. **Bun dependency caching**: Proper caching of bun install cache and node_modules directories 4. **Test setup optimization**: Skip Docker build in test setup when image already exists (CI pre-builds it) Expected performance: - No Dockerfile changes: ~6-7min (cached Docker layers) - Dockerfile changes: ~8-9min (full Docker rebuild) - Previous: ~10min šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/test.yml | 164 ++++++++++++++++++++++++++++++++----- test/setup/global.js | 19 +++++ 2 files changed, 164 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d89599fb..fb10adf3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,32 +6,158 @@ on: pull_request: branches: [ main ] +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + jobs: - test: + lint: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + web/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', '**/package.json') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: | + bun install + cd web && bun install + - name: Lint + run: bun run lint + + - name: Format check + run: bun run format:check + + - name: Typecheck + run: bun x tsc --noEmit + + build: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + web/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', '**/package.json') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: | + bun install + cd web && bun install + + - name: Build + run: bun run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 1 + + test: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Cache bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + web/node_modules + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', '**/package.json') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: | + bun install + cd web && bun install - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Set up Bun - uses: oven-sh/setup-bun@v2 + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Install dependencies - run: | - bun install - cd web && bun install + - name: Check for Dockerfile changes + id: docker-changes + run: | + # For PRs, compare against base branch + # For pushes, compare against previous commit + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE_SHA="${{ github.event.pull_request.base.sha }}" + else + BASE_SHA="${{ github.event.before }}" + fi - - name: Typecheck - run: bun x tsc --noEmit + # Check if any files in perry/ directory changed + if git diff --name-only "$BASE_SHA" HEAD 2>/dev/null | grep -q "^perry/"; then + echo "Dockerfile or related files changed - will rebuild and update cache" + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "No Dockerfile changes detected - using cached layers" + echo "changed=false" >> $GITHUB_OUTPUT + fi - - name: Build - run: bun run build + - name: Build workspace image (with cache) + uses: docker/build-push-action@v6 + with: + context: ./perry + load: true + tags: workspace:latest + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-test + cache-to: ${{ steps.docker-changes.outputs.changed == 'true' && format('type=registry,ref={0}/{1}:buildcache-test,mode=max', env.REGISTRY, env.IMAGE_NAME) || '' }} - - name: Run tests - run: bun run test + - name: Run tests + run: | + # Skip Docker build in test setup since we already have the image + export SKIP_DOCKER_BUILD=true + bun run test diff --git a/test/setup/global.js b/test/setup/global.js index 6ab161dc..48a0491b 100644 --- a/test/setup/global.js +++ b/test/setup/global.js @@ -27,7 +27,26 @@ async function cleanupOrphanedResources() { } catch {} } +function imageExists() { + try { + execSync('docker image inspect workspace:latest', { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + async function buildImage() { + if (process.env.SKIP_DOCKER_BUILD === 'true' && imageExists()) { + console.log('\nāœ… Using existing workspace:latest image (SKIP_DOCKER_BUILD=true)\n'); + return; + } + + if (imageExists() && !process.env.FORCE_DOCKER_BUILD) { + console.log('\nāœ… Using existing workspace:latest image\n'); + return; + } + return new Promise((resolve, reject) => { console.log('\nšŸ—ļø Building workspace Docker image once for all tests...\n');