---
# === IDENTITY ===
id: software/devops/github-actions-nodejs/2026
canonical_question: "GitHub Actions reference: Node.js CI/CD pipeline"
aliases:
  - "GitHub Actions Node.js workflow"
  - "Node.js CI pipeline GitHub Actions"
  - "GitHub Actions npm test build deploy"
  - "Node.js GitHub Actions matrix strategy"
entity_type: software_reference
domain: software > devops > GitHub Actions Node.js CI/CD
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-28
confidence: 0.93
version: 1.0
first_published: 2026-02-28

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "actions/setup-node@v4 replaced v3 (Oct 2023); Node.js 16 EOL removed from default runners (Sep 2023)"
  next_review: 2026-08-27
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Always use npm ci instead of npm install in CI -- npm ci is faster, deterministic, and respects package-lock.json exactly"
  - "Pin action versions to major tags (e.g., actions/checkout@v4) -- never use @main or @master in production workflows"
  - "Node.js 16 is EOL and no longer available on GitHub-hosted runners -- minimum supported version is Node.js 18"
  - "GITHUB_TOKEN has limited permissions by default -- explicitly set permissions block for least-privilege access"
  - "Secrets must never be printed to logs -- use masking and never pass secrets as command-line arguments"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need Python CI/CD pipeline instead of Node.js"
    use_instead: "software/devops/github-actions-python/2026"
  - condition: "Need Docker image build and push workflow"
    use_instead: "software/devops/github-actions-docker/2026"
  - condition: "Need GitLab CI instead of GitHub Actions"
    use_instead: "software/devops/gitlab-ci-pipeline/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "package_manager"
    question: "Which package manager does your project use?"
    type: choice
    options: ["npm", "yarn", "pnpm"]
  - key: "deploy_target"
    question: "Where do you deploy your Node.js application?"
    type: choice
    options: ["Cloudflare Pages/Workers", "AWS (Lambda/ECS/EC2)", "Vercel", "Heroku", "Docker container", "npm registry"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/github-actions-nodejs/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-28)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/devops/github-actions-docker/2026"
      label: "GitHub Actions Docker Build and Push"
    - id: "software/devops/github-actions-python/2026"
      label: "GitHub Actions Python CI/CD Pipeline"
  solves: []
  alternative_to:
    - id: "software/devops/gitlab-ci-pipeline/2026"
      label: "GitLab CI Basic Pipeline"
  often_confused_with:
    - id: "software/devops/github-actions-docker/2026"
      label: "GitHub Actions Docker Build and Push"

# === SOURCES ===
sources:
  - id: src1
    title: "Building and testing Node.js"
    author: GitHub
    url: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src2
    title: "actions/setup-node: Set up your GitHub Actions workflow with a specific version of node.js"
    author: GitHub
    url: https://github.com/actions/setup-node
    type: official_docs
    published: 2024-06-01
    reliability: authoritative
  - id: src3
    title: "Workflow syntax for GitHub Actions"
    author: GitHub
    url: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src4
    title: "Caching dependencies to speed up workflows"
    author: GitHub
    url: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src5
    title: "CI/CD in Node.js with GitHub Actions"
    author: LogRocket
    url: https://blog.logrocket.com/ci-cd-node-js-github-actions/
    type: technical_blog
    published: 2024-08-20
    reliability: moderate_high
  - id: src6
    title: "Security hardening for GitHub Actions"
    author: GitHub
    url: https://docs.github.com/en/actions/security-for-github-actions/security-hardening-for-github-actions
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src7
    title: "Using environments for deployment"
    author: GitHub
    url: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
---

# GitHub Actions: Node.js CI/CD Pipeline

## TL;DR

- **Bottom line**: GitHub Actions provides native CI/CD for Node.js projects with matrix testing across versions, built-in npm caching, and deployment to any target -- all configured in a single YAML file at `.github/workflows/`.
- **Key tool/command**: `actions/setup-node@v4` with `cache: 'npm'` and `npm ci` for deterministic installs.
- **Watch out for**: Using `npm install` instead of `npm ci` in workflows -- it's slower, non-deterministic, and can modify package-lock.json.
- **Works with**: Node.js 18.x, 20.x, 22.x on ubuntu-latest, windows-latest, macos-latest runners.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Always use `npm ci` instead of `npm install` -- `npm ci` deletes `node_modules/` first, installs exact versions from `package-lock.json`, and fails if the lockfile is out of sync.
- Pin action versions to major tags (e.g., `actions/checkout@v4`) -- never use `@main` or floating tags in production workflows.
- Node.js 16 is EOL and removed from GitHub-hosted runners since September 2023 -- minimum supported is Node.js 18.
- Set explicit `permissions` block with least-privilege access -- default `GITHUB_TOKEN` permissions may be too broad.
- Never echo or log secrets -- use `::add-mask::` for dynamic values and store credentials in GitHub Secrets only.

## Quick Reference

| Setting | Value / Command | Notes |
|---|---|---|
| Workflow file location | `.github/workflows/*.yml` | Any YAML file in this directory |
| Trigger on push | `on: push: branches: [main]` | Also supports `pull_request`, `schedule`, `workflow_dispatch` |
| Checkout code | `actions/checkout@v4` | Always first step in a job |
| Setup Node.js | `actions/setup-node@v4` | Supports `node-version`, `cache`, `registry-url` |
| Node version from file | `node-version-file: '.nvmrc'` | Also supports `.node-version`, `package.json` |
| Cache npm dependencies | `cache: 'npm'` on setup-node | Caches `~/.npm` global cache, not `node_modules/` |
| Install dependencies | `npm ci` | Deterministic, fast, respects lockfile exactly |
| Run tests | `npm test` | Or `npx jest --ci --coverage` for specific runners |
| Run linter | `npm run lint` | ESLint, Prettier, etc. |
| Build project | `npm run build` | TypeScript compilation, bundling, etc. |
| Matrix strategy | `strategy: matrix: node-version: [18, 20, 22]` | Runs job in parallel for each version |
| Upload artifacts | `actions/upload-artifact@v4` | Test reports, coverage, build output |
| Deploy to npm | `npm publish` with `NODE_AUTH_TOKEN` | Set `registry-url` in setup-node |
| Environment protection | `environment: production` | Requires 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]

```yaml
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]

```yaml
      - 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]

```yaml
      - 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]

```yaml
      - 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 job with environment protection

Create a deploy job that only runs after tests pass on the main branch. [src7]

```yaml
  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 → push → deploy job waits for approval.

### 6. Publish to npm registry

For libraries, add a publish job triggered on release. [src2]

```yaml
  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

> Full script: [complete-ci-cd-workflow-with-matrix-lint-test-buil.yml](scripts/complete-ci-cd-workflow-with-matrix-lint-test-buil.yml) (72 lines)

```yaml
# .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:
# ... (see full script)
```

### npm publish workflow triggered on release

```yaml
# .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

```yaml
# .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

```yaml
# ❌ 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

```yaml
# ✅ 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

```yaml
# ❌ 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

```yaml
# ✅ 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

```yaml
# ❌ 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

```yaml
# ✅ 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

```yaml
# ❌ 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

```yaml
# ✅ 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

- **Missing package-lock.json**: `npm ci` requires `package-lock.json` to exist. If your repo only has `package.json`, `npm ci` will fail. Fix: run `npm install` locally and commit the lockfile. [src2]
- **Node version mismatch**: Your workflow uses Node 22 but your app only works on Node 18. Fix: match `node-version` to the version in `.nvmrc` or use `node-version-file: '.nvmrc'`. [src1]
- **Cache not restoring**: Cache key includes OS and lockfile hash -- any lockfile change invalidates the cache. Fix: this is expected behavior; `npm ci` with cache miss still works, just slower. [src4]
- **Tests pass locally but fail in CI**: Often caused by timezone differences (CI uses UTC), missing environment variables, or case-sensitive file systems on Linux runners. Fix: set `TZ: UTC` in env and check import casing. [src5]
- **Workflow not triggering**: Branch names in `on.push.branches` must match exactly. Fix: check branch name casing and use `**` for wildcard matching. [src3]
- **Secrets not available in fork PRs**: GitHub does not expose secrets to workflows triggered by pull requests from forks. Fix: use `pull_request_target` with caution or require maintainer approval. [src6]

## Diagnostic Commands

```bash
# 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

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| actions/setup-node@v4 | Current | Dropped Node.js 12/14/16 support | Update `node-version` to 18+ |
| actions/setup-node@v3 | Deprecated | — | Replace `@v3` with `@v4` |
| actions/checkout@v4 | Current | Requires Node.js 20+ runner | Automatic on GitHub-hosted runners |
| actions/upload-artifact@v4 | Current | New artifact backend, not backward-compatible with v3 | Use `@v4` for both upload and download |
| Node.js 22 | Current LTS | — | Supported on all runner images |
| Node.js 20 | Active LTS until Apr 2026 | — | Recommended for production |
| Node.js 18 | Maintenance until Apr 2025 | — | Upgrade to 20 or 22 |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Project is hosted on GitHub | Project is on GitLab | GitLab CI/CD |
| Need matrix testing across Node versions | Need complex build pipelines with DAG | CircleCI or Jenkins |
| Want native GitHub integration (PR checks, deployments) | Need self-hosted runners with GPU | GitLab CI with custom runners |
| Publishing npm packages | Need artifact caching across repositories | GitHub Packages + custom cache |
| Simple to moderate CI/CD complexity | Need 100+ concurrent jobs | Self-hosted runner fleet |

## Important Caveats

- GitHub-hosted runners have a 6-hour job timeout and 35-day log retention. Self-hosted runners have no timeout by default.
- The `npm ci` command deletes the entire `node_modules/` folder before installing, so caching `node_modules/` is pointless when using `npm ci`.
- Free tier GitHub Actions provides 2,000 minutes/month for private repos. Public repos get unlimited minutes. Monitor usage in Settings → Billing.
- `actions/upload-artifact@v4` artifacts expire after 90 days by default. Set `retention-days` to control this.
- Workflow files must be on the default branch to appear in the Actions tab for `workflow_dispatch` triggers.

## Related Units

- [GitHub Actions: Docker Build and Push](/software/devops/github-actions-docker/2026)
- [GitHub Actions: Python CI/CD Pipeline](/software/devops/github-actions-python/2026)
- [GitLab CI: Basic Pipeline](/software/devops/gitlab-ci-pipeline/2026)
