GitHub Actions: Matrix Builds, Caching, Artifacts, and Secrets
Introduction
Once a basic workflow works, you will want it to test several environments, run faster, save its output, and use credentials without leaking them. GitHub Actions provides matrix strategies, caching, artifacts, and encrypted secrets for exactly these needs. This lesson covers each with safe defaults.
Prerequisites
- The first CI workflow lesson
Step-by-Step Guide
Step 1: Test several versions with a matrix
A matrix runs the same job once for every combination of the values you list, in parallel. Reference the current value with the matrix context. By default, one failing combination cancels the others, which you can turn off with fail-fast set to false when you want to see all failures.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [22, 24]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm testStep 2: Cache dependencies
The setup actions for Node and Python can cache package downloads for you through their cache input, as in the earlier examples. For anything else, use actions/cache with a key that changes when the dependencies change, typically a hash of the lock file. Caches unused for a week are evicted, and each repository has a size limit.
- uses: actions/cache@v6
with:
path: ~/.cache/my-tool
key: ${{ runner.os }}-my-tool-${{ hashFiles('**/package-lock.json') }}Step 3: Keep build outputs with artifacts
Artifacts let you save files from a run, such as test reports or built packages, and download them later or pass them to another job. With the current artifact actions, each artifact needs a unique name within a run, so include the matrix values when you upload from a matrix job.
- run: npm run build
- uses: actions/upload-artifact@v7
with:
name: build-${{ matrix.os }}-${{ matrix.node }}
path: dist/
retention-days: 7Step 4: Use secrets safely
Add secrets in the repository under Settings, then Secrets and variables, then Actions, using New repository secret. Read them with the secrets context and pass them to the step that needs them, preferably as environment variables. Secrets are not passed to workflows triggered from forks, and you cannot reference a secret directly in an if condition; copy it into an environment variable first. Never print secrets in logs.
- name: Deploy
run: ./deploy.sh
env:
API_TOKEN: ${{ secrets.API_TOKEN }}Step 5: Limit permissions, cancel duplicates, and set timeouts
Set the minimum permissions the workflow needs, because once you set any permission every unspecified one becomes none. Use concurrency to cancel an older run when a newer commit arrives on the same branch, and set a timeout so a hung job does not burn minutes.
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15FAQs
- Are secrets hidden from the logs?
- GitHub masks exact secret values in logs, but masking is a safety net, not a guarantee, since transformed values may not be masked. Never print secrets or pass them on command lines where they might be echoed.
- Why does my artifact upload fail in a matrix job?
- Artifact names must be unique within a run. Add the matrix values to the artifact name so each combination uploads to its own name.
Summary
Use a matrix for multi-environment tests, caching to cut install time, artifacts to keep outputs, secrets for credentials, and minimal permissions, concurrency, and timeouts to keep workflows safe and efficient.
Comments
Log in or create a free account to join the discussion.