Skip to content

Commit b37c9f2

Browse files
grichaclaude
andauthored
Optimize test CI with parallel jobs and BuildKit caching (#4)
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 <noreply@anthropic.com>
1 parent b7d5ba9 commit b37c9f2

File tree

2 files changed

+164
-19
lines changed

2 files changed

+164
-19
lines changed

.github/workflows/test.yml

Lines changed: 145 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,158 @@ on:
66
pull_request:
77
branches: [ main ]
88

9+
env:
10+
REGISTRY: ghcr.io
11+
IMAGE_NAME: ${{ github.repository }}
12+
913
jobs:
10-
test:
14+
lint:
1115
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Bun
20+
uses: oven-sh/setup-bun@v2
21+
with:
22+
bun-version: latest
23+
24+
- name: Cache bun dependencies
25+
uses: actions/cache@v4
26+
with:
27+
path: |
28+
~/.bun/install/cache
29+
node_modules
30+
web/node_modules
31+
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', '**/package.json') }}
32+
restore-keys: |
33+
${{ runner.os }}-bun-
34+
35+
- name: Install dependencies
36+
run: |
37+
bun install
38+
cd web && bun install
1239
40+
- name: Lint
41+
run: bun run lint
42+
43+
- name: Format check
44+
run: bun run format:check
45+
46+
- name: Typecheck
47+
run: bun x tsc --noEmit
48+
49+
build:
50+
runs-on: ubuntu-latest
1351
steps:
14-
- uses: actions/checkout@v4
52+
- uses: actions/checkout@v4
53+
54+
- name: Set up Bun
55+
uses: oven-sh/setup-bun@v2
56+
with:
57+
bun-version: latest
58+
59+
- name: Cache bun dependencies
60+
uses: actions/cache@v4
61+
with:
62+
path: |
63+
~/.bun/install/cache
64+
node_modules
65+
web/node_modules
66+
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', '**/package.json') }}
67+
restore-keys: |
68+
${{ runner.os }}-bun-
69+
70+
- name: Install dependencies
71+
run: |
72+
bun install
73+
cd web && bun install
74+
75+
- name: Build
76+
run: bun run build
77+
78+
- name: Upload build artifacts
79+
uses: actions/upload-artifact@v4
80+
with:
81+
name: dist
82+
path: dist/
83+
retention-days: 1
84+
85+
test:
86+
runs-on: ubuntu-latest
87+
needs: build
88+
steps:
89+
- uses: actions/checkout@v4
90+
with:
91+
fetch-depth: 0
92+
93+
- name: Download build artifacts
94+
uses: actions/download-artifact@v4
95+
with:
96+
name: dist
97+
path: dist/
98+
99+
- name: Set up Bun
100+
uses: oven-sh/setup-bun@v2
101+
with:
102+
bun-version: latest
103+
104+
- name: Cache bun dependencies
105+
uses: actions/cache@v4
106+
with:
107+
path: |
108+
~/.bun/install/cache
109+
node_modules
110+
web/node_modules
111+
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock', '**/package.json') }}
112+
restore-keys: |
113+
${{ runner.os }}-bun-
114+
115+
- name: Install dependencies
116+
run: |
117+
bun install
118+
cd web && bun install
15119
16-
- name: Set up Node.js
17-
uses: actions/setup-node@v4
18-
with:
19-
node-version: '22'
20-
cache: 'npm'
120+
- name: Set up Docker Buildx
121+
uses: docker/setup-buildx-action@v3
21122

22-
- name: Set up Bun
23-
uses: oven-sh/setup-bun@v2
123+
- name: Log in to Container Registry
124+
uses: docker/login-action@v3
125+
with:
126+
registry: ${{ env.REGISTRY }}
127+
username: ${{ github.actor }}
128+
password: ${{ secrets.GITHUB_TOKEN }}
24129

25-
- name: Install dependencies
26-
run: |
27-
bun install
28-
cd web && bun install
130+
- name: Check for Dockerfile changes
131+
id: docker-changes
132+
run: |
133+
# For PRs, compare against base branch
134+
# For pushes, compare against previous commit
135+
if [ "${{ github.event_name }}" = "pull_request" ]; then
136+
BASE_SHA="${{ github.event.pull_request.base.sha }}"
137+
else
138+
BASE_SHA="${{ github.event.before }}"
139+
fi
29140
30-
- name: Typecheck
31-
run: bun x tsc --noEmit
141+
# Check if any files in perry/ directory changed
142+
if git diff --name-only "$BASE_SHA" HEAD 2>/dev/null | grep -q "^perry/"; then
143+
echo "Dockerfile or related files changed - will rebuild and update cache"
144+
echo "changed=true" >> $GITHUB_OUTPUT
145+
else
146+
echo "No Dockerfile changes detected - using cached layers"
147+
echo "changed=false" >> $GITHUB_OUTPUT
148+
fi
32149
33-
- name: Build
34-
run: bun run build
150+
- name: Build workspace image (with cache)
151+
uses: docker/build-push-action@v6
152+
with:
153+
context: ./perry
154+
load: true
155+
tags: workspace:latest
156+
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-test
157+
cache-to: ${{ steps.docker-changes.outputs.changed == 'true' && format('type=registry,ref={0}/{1}:buildcache-test,mode=max', env.REGISTRY, env.IMAGE_NAME) || '' }}
35158

36-
- name: Run tests
37-
run: bun run test
159+
- name: Run tests
160+
run: |
161+
# Skip Docker build in test setup since we already have the image
162+
export SKIP_DOCKER_BUILD=true
163+
bun run test

test/setup/global.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,26 @@ async function cleanupOrphanedResources() {
2727
} catch {}
2828
}
2929

30+
function imageExists() {
31+
try {
32+
execSync('docker image inspect workspace:latest', { stdio: 'ignore' });
33+
return true;
34+
} catch {
35+
return false;
36+
}
37+
}
38+
3039
async function buildImage() {
40+
if (process.env.SKIP_DOCKER_BUILD === 'true' && imageExists()) {
41+
console.log('\n✅ Using existing workspace:latest image (SKIP_DOCKER_BUILD=true)\n');
42+
return;
43+
}
44+
45+
if (imageExists() && !process.env.FORCE_DOCKER_BUILD) {
46+
console.log('\n✅ Using existing workspace:latest image\n');
47+
return;
48+
}
49+
3150
return new Promise((resolve, reject) => {
3251
console.log('\n🏗️ Building workspace Docker image once for all tests...\n');
3352

0 commit comments

Comments
 (0)