GitHub Actions: Node.js CI/CD Pipeline

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-28 Freshness: 2026-02-28

TL;DR

Constraints

Quick Reference

SettingValue / CommandNotes
Workflow file location.github/workflows/*.ymlAny YAML file in this directory
Trigger on pushon: push: branches: [main]Also supports pull_request, schedule, workflow_dispatch
Checkout codeactions/checkout@v4Always first step in a job
Setup Node.jsactions/setup-node@v4Supports node-version, cache, registry-url
Node version from filenode-version-file: '.nvmrc'Also supports .node-version, package.json
Cache npm depscache: 'npm' on setup-nodeCaches ~/.npm global cache, not node_modules/
Install dependenciesnpm ciDeterministic, fast, respects lockfile exactly
Run testsnpm testOr npx jest --ci --coverage
Matrix strategymatrix: node-version: [18, 20, 22]Runs job in parallel for each version
Upload artifactsactions/upload-artifact@v4Test reports, coverage, build output
Deploy to npmnpm publish with NODE_AUTH_TOKENSet registry-url in setup-node
Environment protectionenvironment: productionRequires approval for protected environments

Decision Tree

START
├── Single Node.js version project?
│   ├── YES → Use fixed node-version (e.g., 20) with npm ci + test + build
│   └── NO ↓
├── Library published to npm?
│   ├── YES → Matrix strategy [18, 20, 22] + publish job with NODE_AUTH_TOKEN
│   └── NO ↓
├── Monorepo with multiple packages?
│   ├── YES → Use workspaces, npm ci at root, run tests per package
│   └── NO ↓
├── Deploy to cloud provider?
│   ├── YES → Add deploy job with environment protection + needs: [test, build]
│   └── NO ↓
└── DEFAULT → Basic CI: checkout → setup-node → npm ci → npm test

Step-by-Step Guide

1. Create the workflow file

Create .github/workflows/ci.yml in your repository. GitHub automatically detects and runs workflows from this directory. [src1]

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

Verify: Push to GitHub → go to Actions tab → see workflow run with 3 parallel matrix jobs.

2. Add linting and build steps

Extend the workflow with lint and build steps that run after dependency installation. [src5]

      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run build

Verify: Deliberately introduce a lint error → push → workflow should fail at the lint step.

3. Configure npm caching

The cache: 'npm' option on setup-node caches the global npm cache directory (~/.npm). It requires package-lock.json to exist. [src2]

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

Verify: Check workflow logs for "Cache hit" message on the second run.

4. Add test coverage reporting

Upload coverage reports as artifacts for review. [src1]

      - run: npx jest --ci --coverage
      - name: Upload coverage
        if: matrix.node-version == 20
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/
          retention-days: 14

Verify: After workflow completes → Artifacts section shows coverage-report download.

5. Add deployment with environment protection

Create a deploy job that only runs after tests pass on the main branch. [src7]

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - name: Deploy
        run: npx wrangler deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Verify: Create a production environment in Settings → Environments → add required reviewers.

6. Publish to npm registry

For libraries, add a publish job triggered on release. [src2]

  publish:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Verify: Create a GitHub Release → Actions tab shows publish job → check npmjs.com for new version.

Code Examples

Complete CI/CD workflow with matrix, lint, test, build, and deploy

# .github/workflows/ci.yml
# Input:  Node.js project with package.json, package-lock.json
# Output: Tested, built, and deployed application

name: Node.js CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  lint:
    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

  test:
    runs-on: ubuntu-latest
    needs: lint
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --ci --coverage
      - name: Upload coverage
        if: matrix.node-version == 20
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - name: Deploy to production
        run: echo "Deploy dist/ to your target"
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

npm publish workflow triggered on release

# .github/workflows/publish.yml
# Input:  npm package with package.json
# Output: Published package on npmjs.com

name: Publish to npm

on:
  release:
    types: [published]

permissions:
  contents: read
  id-token: write

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm test
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Monorepo CI with workspaces

# .github/workflows/monorepo.yml
# Input:  npm workspaces monorepo
# Output: All packages tested and built

name: Monorepo CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

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 test --workspaces --if-present
      - run: npm run build --workspaces --if-present

Anti-Patterns

Wrong: Using npm install instead of npm ci

# ❌ BAD — npm install can modify package-lock.json and is non-deterministic
steps:
  - run: npm install
  - run: npm test

Correct: Using npm ci for deterministic installs

# ✅ GOOD — npm ci is faster, deterministic, and fails if lockfile is out of sync
steps:
  - run: npm ci
  - run: npm test

Wrong: Not pinning action versions

# ❌ BAD — @main can change at any time, breaking your workflow
steps:
  - uses: actions/checkout@main
  - uses: actions/setup-node@main

Correct: Pinning to major version tags

# ✅ GOOD — @v4 is stable and receives only non-breaking updates
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4

Wrong: Caching node_modules directly

# ❌ BAD — node_modules may contain platform-specific binaries, cache is fragile
- uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
- run: npm install

Correct: Using built-in cache on setup-node

# ✅ GOOD — caches ~/.npm global cache, works across Node versions
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'
- run: npm ci

Wrong: Running deploy on pull requests

# ❌ BAD — deploys on every PR, including from forks (security risk)
deploy:
  needs: test
  runs-on: ubuntu-latest
  steps:
    - run: npx deploy-command
      env:
        API_KEY: ${{ secrets.API_KEY }}

Correct: Restricting deploy to main branch pushes

# ✅ GOOD — only deploys on push to main, with environment protection
deploy:
  needs: test
  runs-on: ubuntu-latest
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  environment: production
  steps:
    - run: npx deploy-command
      env:
        API_KEY: ${{ secrets.API_KEY }}

Common Pitfalls

Diagnostic Commands

# Check installed Node.js version in workflow
node --version

# Verify npm ci installed exact lockfile versions
npm ls --depth=0

# List all GitHub Actions workflow files
ls -la .github/workflows/

# Validate workflow YAML locally (requires actionlint)
actionlint .github/workflows/ci.yml

# Check workflow run status via CLI
gh run list --workflow=ci.yml --limit=5

# View specific workflow run logs
gh run view <run-id> --log

# Check available secrets (names only)
gh secret list

Version History & Compatibility

VersionStatusBreaking ChangesMigration Notes
actions/setup-node@v4CurrentDropped Node.js 12/14/16 supportUpdate node-version to 18+
actions/setup-node@v3DeprecatedReplace @v3 with @v4
actions/checkout@v4CurrentRequires Node.js 20+ runnerAutomatic on GitHub-hosted runners
actions/upload-artifact@v4CurrentNew artifact backendUse @v4 for both upload and download
Node.js 22Current LTSSupported on all runner images
Node.js 20Active LTS until Apr 2026Recommended for production
Node.js 18Maintenance until Apr 2025Upgrade to 20 or 22

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Project is hosted on GitHubProject is on GitLabGitLab CI/CD
Need matrix testing across Node versionsNeed complex DAG pipelinesCircleCI or Jenkins
Want native GitHub integration (PR checks)Need self-hosted GPU runnersGitLab CI with custom runners
Publishing npm packagesNeed cross-repo artifact cachingGitHub Packages + custom cache
Simple to moderate CI/CD complexityNeed 100+ concurrent jobsSelf-hosted runner fleet

Important Caveats

Related Units