GitHub Actions for Node
Configure GitHub Actions for Node.js 24 TypeScript services: matrix builds, npm cache, artifacts, and OIDC deploy.
Recipe
Quick-reference recipe card - copy-paste ready.
name: CI
on: [pull_request, push]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run lint && npm run typecheck && npm run testWhen to reach for this: Every Node repo on GitHub. This is the default CI skeleton before Docker and deploy jobs.
Working Example
# .github/workflows/ci.yml
name: CI
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
cache-dependency-path: package-lock.json
- name: Install
run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Test with coverage
run: npm run test -- --coverage
- name: Upload coverage
if: matrix.node-version == 24
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
retention-days: 7
docker:
needs: quality
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/acme/api:${{ github.sha }}
ghcr.io/acme/api:main
cache-from: type=gha
cache-to: type=gha,mode=maxWhat this demonstrates:
- Matrix Node 22 and 24 for LTS compatibility
cache: npmkeyed on lockfile viasetup-nodeconcurrencycancels superseded PR runs- Docker build only on
mainpush after quality job passes - BuildKit GHA cache for faster image layers
Deep Dive
npm Cache Behavior
actions/setup-node@v4 with cache: npm hashes package-lock.json. For monorepos:
cache-dependency-path: |
package-lock.json
services/api/package-lock.jsonArtifacts vs Caching
| Feature | Use for |
|---|---|
actions/cache | node_modules, Docker layers |
actions/upload-artifact | Coverage reports, built dist/, Lambda zips |
- uses: actions/upload-artifact@v4
with:
name: lambda-zip
path: function.zipDownstream deploy job downloads the same zip tested in CI.
OIDC to AWS (No Static Keys)
permissions:
id-token: write
contents: read
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: us-east-1Trust policy on IAM role limits to repo:acme/api:ref:refs/heads/main.
Service Containers for Integration Tests
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 3s
--health-retries 5
steps:
- run: npm run test:integration
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgresGotchas
npm installinstead ofnpm ci- non-reproducible CI. Fix: commit lockfile; alwaysnpm ci.- Matrix without
fail-fast: false- Node 22 failure hides Node 24 result. Fix: setfail-fast: falsewhen comparing versions. - Caching
node_modulesmanually - often slower thansetup-nodecache. Fix: use built-in npm cache unless monorepo tooling requires custom paths. :latestdeploy tags only - cannot rollback. Fix: always push${{ github.sha }}tag.- Missing
concurrency- queued PR pushes waste minutes. Fix: cancel in-progress runs on same branch. - Docker on every PR without path filter - slow and expensive. Fix:
paths: ['Dockerfile', 'src/**']or build only on main.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| GitHub Actions | Repo on GitHub | GitLab-only org (use GitLab CI) |
| CircleCI / Buildkite | Custom runners, large monorepos | Simple single-service repos |
| Nx Cloud / Turborepo | Affected task graph | Tiny single-package apps |
| Self-hosted runners | GPU or VPC-internal tests | Default OSS public repos |
FAQs
Should we matrix Node 20, 22, and 24?
Matrix 22 and 24 if you support both Active LTS lines. Drop 20 when officially sunset for your product.
pnpm or yarn cache?
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
cache: pnpmHow long should CI take?
Under 10 minutes for unit stages. Split slow integration tests into a nightly workflow if needed.
Can one workflow do CI and CD?
Possible but separate workflows improve clarity: ci.yml on PR, release.yml on tag. See CI/CD Basics.
How do we run Playwright?
Use npx playwright install --with-deps step and shard tests across matrix jobs for large suites.
Secrets scanning?
Enable GitHub secret scanning and gitleaks action on PRs. Block commits with .env contents.
Related
- CI/CD Basics - PR vs release pipelines
- Quality Gates - required checks
- Preview Environments - PR deploy workflows
- Docker Best Practices - image build in CI
- CI/CD Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.