Skip to content

Caching and Artifact Patterns

Three techniques avoid redundant work: skip expensive operations when dependencies haven't changed, cache deterministic build outputs, and build once then reuse artifacts across test jobs.

Performance Optimization

These patterns reduce workflow execution time and cost. Combine multiple techniques for maximum efficiency.


Pattern 7: Skipping Unchanged Dependencies

Track dependency changes:

jobs:
  check-deps:
    runs-on: ubuntu-latest
    outputs:
      go-mod-changed: ${{ steps.filter.outputs.go-mod }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            go-mod:
              - '**/go.mod'
              - '**/go.sum'

  vendor:
    needs: check-deps
    if: needs.check-deps.outputs.go-mod-changed == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Vendor dependencies
        run: go mod vendor

Skip expensive vendor operations if dependencies didn't change.

Language-Specific Dependency Files

filters: |
  go-deps:
    - '**/go.mod'
    - '**/go.sum'

  node-deps:
    - '**/package.json'
    - '**/package-lock.json'

  python-deps:
    - '**/requirements.txt'
    - '**/poetry.lock'

  rust-deps:
    - '**/Cargo.toml'
    - '**/Cargo.lock'

Use when: Dependency installation is expensive (npm install takes 3+ minutes).


Pattern 9: Caching Matrix Outputs

Avoid rebuilding identical artifacts:

jobs:
  build:
    strategy:
      matrix:
        arch: [amd64, arm64]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check cache
        id: cache
        uses: actions/cache@v4
        with:
          path: dist/binary-${{ matrix.arch }}
          key: build-${{ matrix.arch }}-${{ hashFiles('**/*.go') }}

      - name: Build
        if: steps.cache.outputs.cache-hit != 'true'
        run: |
          GOARCH=${{ matrix.arch }} go build -o dist/binary-${{ matrix.arch }}

How it works:

First run:

  • hashFiles('**/*.go') = abc123
  • Key = build-amd64-abc123
  • Cache miss, build runs
  • Binary cached with key

Second run (unchanged source):

  • hashFiles('**/*.go') = abc123 (same hash)
  • Key = build-amd64-abc123 (same key)
  • Cache hit, build skipped

Third run (changed source):

  • hashFiles('**/*.go') = def456 (different hash)
  • Key = build-amd64-def456 (new key)
  • Cache miss, build runs

Cache key strategies:

# OS-specific cache
key: ${{ runner.os }}-build-${{ hashFiles('**/go.sum') }}

# Restore from previous version if exact match fails
restore-keys: |
  ${{ runner.os }}-build-

# Multiple hash inputs
key: build-${{ hashFiles('go.sum') }}-${{ hashFiles('Makefile') }}

Use when: Deterministic builds, unchanged source = identical output.


Pattern 10: Work Avoidance with Artifacts

One job builds, many jobs test:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build binary
        run: make build
      - uses: actions/upload-artifact@v4
        with:
          name: app-binary
          path: dist/app

  test:
    needs: build
    strategy:
      matrix:
        suite: [unit, integration, e2e]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app-binary
          path: dist/
      - name: Run ${{ matrix.suite }} tests
        run: make test-${{ matrix.suite }}

Workflow:

  1. build job: Compile once
  2. Upload artifact (available to all jobs in workflow)
  3. test jobs: Download artifact, run tests in parallel

Result: Build once, test in parallel. Avoid rebuilding per test suite.

Multi-Artifact Pattern

- uses: actions/upload-artifact@v4
  with:
    name: app-${{ matrix.platform }}
    path: dist/

# Download specific artifact
- uses: actions/download-artifact@v4
  with:
    name: app-linux
    path: dist/

Use when: Build is expensive, multiple test suites need the same artifact.


Cache vs Artifacts

Feature Cache Artifacts
Purpose Speed up repeated builds Share outputs between jobs
Scope Across workflow runs Within single workflow run
Retention 7 days (default) 90 days (default)
Size limit 10 GB per repo 10 GB per workflow
Use case Dependencies, build outputs Build → test handoff

Cache: npm_modules/, compiled binaries (cross-run optimization)

Artifacts: Freshly built binaries for current run's test jobs


Performance Impact

Dependency Tracking

Before: go mod vendor runs every push (2 minutes)

After: Runs only when go.mod changes (once per dependency update)

Savings: 90% of runs skip vendor step.

Caching

Before: Cross-compile for 3 architectures every push (9 minutes total)

After: Cache hits on unchanged code (0 minutes)

Savings: 100% on cache hit (typically 80% of pushes).

Artifacts

Before: Build + 5 test suites = 6 builds (30 minutes)

After: Build once + 5 test jobs in parallel (6 minutes: 1 build + 5 parallel tests)

Savings: 80% reduction.



Dependencies unchanged. Cache hit. Vendor step skipped. Build reused from cache. Artifact shared across 5 test jobs. CI time: 6 minutes down from 30.

Comments