CI/CD Pipeline Optimization: Caching, Parallelism, and Artifact Management in GitHub Actions
CI/CD Pipeline Optimization: Caching, Parallelism, and Artifact Management in GitHub Actions
GitHub Actions workflows can become slow as projects grow, especially when runs repeat the same setup steps, compile identical artifacts, or serialize jobs that could run concurrently. This article presents practical patterns to reduce workflow execution time and improve developer feedback loops.
Why Pipeline Matters
A slow CI pipeline delays feedback, increases context switching for developers, and can discourage frequent commits. Common bottlenecks include:
- Re-downloading dependencies on every run
- Compiling the same code multiple times across jobs
- Uploading and downloading large artifacts unnecessarily
- Running tests and builds sequentially when they are independent
1. Directory Caching with actions/cache
The actions/cache action stores downloaded dependencies and compiled artifacts between runs. When the cache key matches, the restored content is available instantly without re-downloading.
Cache dependency installation (Node.js example)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache Node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: $-node-$
restore-keys: |
$-node
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: $-pnpm-$
- name: Install dependencies
run: pnpm install --frozen-lockfile
Cache build artifacts
- name: Cache Go build cache
uses: actions/cache@v4
with:
path: ~/go/build/cache
key: $-go-$
restore-keys: |
$-go
Cache key strategy: Include a hash of the dependency lock file so the cache invalidates when dependencies change. restore-keys allows falling back to an earlier cache if the exact key is unavailable.
2. Parallel Jobs
Many CI workflows serialize steps that could run independently. Breaking a job into parallel units reduces total execution time, especially when the steps involve distinct toolchains or test suites.
Example: Lint, type-check, and compile in parallel
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- tool: eslint
test: unit
- tool: stylelint
test: integration
- tool: tsc
test: e2e
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run $
run: pnpm run $
Parallelizing test suites across runners
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: "unit-tests"
file: "test/unit"
- name: "integration-tests"
file: "test/integration"
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Run $
run: pnpm test -- --testPathPattern="$"
Matrix strategy: The strategy.matrix approach spins up separate jobs for each configuration. Each job runs independently, and the overall workflow waits for all matrix items to complete before proceeding.
3. Artifact Management
Artifacts persist data between jobs and workflow runs. Use them to share build outputs, generated binaries, or test results without re-computing them.
Uploading a build artifact
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build binary
run: go build -o myapp ./cmd/myapp
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: myapp-linux-amd64
path: myapp
retention-days: 30
Downloading an artifact in a subsequent job
jobs:
integration-test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: myapp-linux-amd64
path: .
- name: Run integration test
run: ./myapp -test-integration
Artifact retention: Set retention-days to control how long artifacts are stored. Default is 7 days; increase for longer-lived workflows or decrease to conserve storage.
4. Combined Pattern: Cache + Parallelism + Artifacts
A complete workflow often combines all three patterns. Here is a realistic example for a Node.js project that runs lint, type-check, and tests in parallel while caching dependencies:
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-hit: $
steps:
- uses: actions/checkout@v4
- name: Cache node modules
id: cache
uses: actions/cache@v4
with:
path: ~/.npm
key: $-node-$
restore-keys: |
$-node
- name: Report cache status
run: echo "Cache hit: $"
build-and-test:
needs: setup
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: "Lint"
cmd: "pnpm run lint"
- name: "Type check"
cmd: "pnpm run typecheck"
- name: "Unit tests"
cmd: "pnpm test -- --run"
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies (skip if cache hit)
if: steps.setup.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Run $
run: $
package:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-outputs
path: .
- name: Create release package
run: |
mkdir -p release
cp -r dist/* release/
echo "package built successfully"
Key takeaways:
- Use
actions/cachefor dependencies and build outputs that change infrequently. - Use
strategy.matrixto parallelize independent tasks (lint, type-check, test suites). - Use
actions/upload-artifactandactions/download-artifactto share data between jobs without committing to version control. - Cache keys should include a hash of lock files or other change-detection mechanisms.
- Set
retention-daysconsciously based on your storage budget and pipeline needs.
Conclusion
Optimizing a CI/CD pipeline is an iterative process. Start with caching the most expensive dependencies, then parallelize independent test suites, and finally refine artifact management for multi-job workflows. Each pattern reduces cycle time, and together they compound into faster feedback and more productive developers.
This article draws on common GitHub Actions patterns and real-world pipeline optimization experiences. Adjust cache keys and matrix configurations to match your project’s dependency ecosystem and test configuration.