CI/CD Pipeline Optimization with Claude Code
The Problem
Your CI/CD pipeline takes 15+ minutes to run. Developers wait for builds, merge queues back up, and deployment velocity drops. You know the pipeline could be faster but optimizing it requires understanding caching, parallelization, job dependencies, and build tool configuration.
Quick Start
Ask Claude Code to audit your pipeline:
Read my GitHub Actions workflows in .github/workflows/ and identify
optimization opportunities. Focus on:
- Jobs that can run in parallel
- Missing or misconfigured caching
- Redundant steps across jobs
- Slow steps that could be optimized
Give me a specific optimization plan with estimated time savings.
What’s Happening
CI/CD pipelines slow down as projects grow. Common causes include:
- Serial execution: Jobs that could run in parallel are chained sequentially
- No caching: Dependencies reinstall from scratch on every run
- Redundant work: The same build step runs in multiple jobs
- Over-testing: Every test runs on every PR, even when only docs changed
- Large Docker images: Build images include development dependencies
- Flaky tests: Failed tests trigger retries that double pipeline duration
Claude Code can read your entire pipeline configuration, understand job dependencies, and restructure the pipeline for maximum parallelism while maintaining correctness.
Step-by-Step Guide
Step 1: Baseline your pipeline performance
Ask Claude Code to add timing annotations:
Add step-level timing to my GitHub Actions workflow. I want to see
how long each step takes so we can identify the bottlenecks.
You can also check existing run times:
# List recent workflow runs with duration
gh run list --limit 10 --json durationMs,conclusion,displayTitle \
--jq '.[] | "\(.displayTitle): \(.durationMs/1000)s - \(.conclusion)"'
Step 2: Parallelize independent jobs
A common before/after transformation:
Before (serial):
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run test
- run: npm run build
- run: npm run test:e2e
After (parallel):
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test
build:
needs: [lint, typecheck, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
e2e:
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run test:e2e
Lint, typecheck, and test now run simultaneously, cutting that phase from 3x to 1x duration.
Step 3: Optimize dependency caching
# Cache npm dependencies
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
# For pnpm (requires pnpm setup first)
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
# Cache Turborepo build outputs
- uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
turbo-${{ runner.os }}-
Step 4: Share build artifacts between jobs
Instead of building in every job, build once and share:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 1
test-e2e:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- run: npm run test:e2e
deploy:
needs: [build, test-e2e]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- run: ./deploy.sh
Step 5: Skip unnecessary work with path filters
Do not run the full pipeline when only docs or configs change:
on:
pull_request:
paths-ignore:
- '**.md'
- 'docs/**'
- '.vscode/**'
- 'LICENSE'
# Or use path filters per job
jobs:
test:
if: |
github.event_name == 'push' ||
contains(github.event.pull_request.changed_files, 'src/') ||
contains(github.event.pull_request.changed_files, 'tests/')
For monorepos, use Turborepo’s --filter flag:
- run: npx turbo run test --filter=...[origin/main]
# Only tests packages changed since main
Step 6: Shard large test suites
Split tests across multiple runners:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx jest --shard=${{ matrix.shard }}/4
Step 7: Fix flaky tests
Ask Claude Code to identify and fix flaky tests:
Analyze the test suite for flaky test patterns:
- Tests that depend on timing (setTimeout, Date.now)
- Tests with race conditions (parallel database access)
- Tests that depend on execution order
- Tests that use real network calls instead of mocks
List every instance and fix them.
Measuring Results
After optimization, compare pipeline duration:
# Compare before and after
gh run list --limit 20 --json durationMs,createdAt \
--jq '.[] | "\(.createdAt): \(.durationMs/1000)s"'
Typical improvements:
| Optimization | Time saved |
|---|---|
| Parallel jobs | 40-60% |
| Dependency caching | 30-60 seconds per job |
| Build artifact sharing | 1-3 minutes |
| Path filtering | Skips entire pipeline for non-code changes |
| Test sharding | Linear speedup with shard count |