A GitHub Actions workflow
Triggers, jobs, steps, a build matrix and artefacts, written as one workflow file you can adapt.
Anatomy of a workflow
A workflow is a YAML file in .github/workflows/. It declares when it runs, which jobs run on which machines, and the steps inside each job. Steps in one job share a filesystem; different jobs do not.
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
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 lint
- run: npm testondecides which events trigger a run: a push to a branch, a pull request, a tag, a schedule or a manualworkflow_dispatch.usespulls in a published action, whilerunexecutes a shell command. Pin third-party actions to a version or a commit SHA.- Each job starts on a fresh runner, so anything installed in one job is gone by the next.
Matrix builds and caching
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # keep other combinations running
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 22]
exclude:
- os: macos-latest
node: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test| Feature | Notes |
|---|---|
strategy.matrix | Runs the job once per combination, in parallel |
fail-fast: false | One failing combination does not cancel the others |
exclude / include | Trim or add specific combinations |
setup-node with cache: npm | Caches the npm download cache, keyed on the lockfile |
A four-way matrix multiplies runner minutes by four. Keep the cross-product small and move extra coverage into a nightly run.
Moving files between jobs
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
package:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
- run: tar -czf site.tar.gz -C dist .💡
needs is what creates the dependency: the runner for package does not start until build has finished. Without it the jobs run in parallel and download-artifact finds nothing.FAQ
Why did my job run twice on a pull request?
The workflow triggers on both
push and pull_request. Restrict push to branches: [main], or add a concurrency group so superseded runs are cancelled.How do I run a workflow by hand?
Add
workflow_dispatch to the on: block, then use the Run workflow button in the Actions tab or the gh workflow run command.Related
Pipeline stages and artefacts Gates, secrets and deploys
Last refreshed 2026-09-18.