Triggers, concurrency and matrix builds
Event and path filters, scheduled and manual runs, concurrency groups that cancel stale work, matrix expansion, fail-fast, and job dependencies with needs.
Choosing what starts a pipeline
name: ci
on:
push:
branches: [main]
paths-ignore: ['docs/**', '*.md']
pull_request:
types: [opened, synchronize, reopened]
paths: ['src/**', 'package.json', 'package-lock.json']
workflow_dispatch:
inputs:
environment:
description: Target environment
type: choice
options: [staging, production]
required: true
schedule:
- cron: '0 3 * * *' # nightly full suite in UTC
# cancel a running job when a newer commit arrives on the same branch
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: truepathsfilters prevent a docs-only change from running the whole test suite, which is the easiest pipeline speed-up available.pull_requestruns against a merge commit;pull_request_targetruns in the base repository context with secrets — never check out untrusted code in that mode.- A schedule always runs on the default branch and uses UTC, so a nightly job at 03:00 UTC drifts against local time when daylight saving changes.
cancel-in-progresson a per-branch group stops a superseded run and frees a runner immediately.
Matrix builds
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 4
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [20, 22]
exclude:
- os: windows-latest
node: 20
include:
- os: ubuntu-latest
node: 22
coverage: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
- if: matrix.coverage
run: npm run test:coverage| Setting | Effect | When to use |
|---|---|---|
fail-fast: false | Keep other cells running after one fails | When you need the full failure picture |
max-parallel | Cap concurrent cells | Shared infrastructure with a limit |
exclude | Remove unsupported combinations | An OS that may not be paired with a version |
include | Add one extra cell or extra keys | A single special case |
| Dynamic matrix | A job outputs the matrix JSON | Build only the projects that changed |
A matrix grows multiplicatively. Three operating systems, four language versions and two architectures is twenty-four cells, each consuming a runner and paid minutes — trim it to the combinations a user actually has.
Dependencies between jobs
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- id: meta
run: echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- run: ./build.sh
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
integration:
needs: build # starts only after build succeeds
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { name: dist }
- run: ./integration.sh
deploy:
needs: [build, integration]
if: github.ref == 'refs/heads/main' && needs.build.result == 'success'
runs-on: ubuntu-latest
environment: production # gates on required reviewers
steps:
- run: ./deploy.sh --version ${{ needs.build.outputs.version }}⚠️
A skipped or cancelled dependency propagates: a job with
needs does not run when its parent is skipped, unless you use if: always(). Use that deliberately for reporting jobs, not to bypass a real gate.FAQ
Should I cancel in-progress runs on a pull request?
Yes for the test pipeline, so a new commit stops wasting a runner on the old one. Do not cancel a deployment pipeline mid-flight — a partially applied release is worse than a slow one.
How do I avoid running the full matrix on every commit?
Run the current runtime versions on every pull request and the full matrix on the default branch or nightly. You keep fast feedback and still catch version-specific problems before release.
Related
CI/CD concepts and choosing a platform Testing strategy inside the pipeline
Last refreshed 2026-09-18.