.github/workflows/.actions/setup-node@v4 with cache: 'npm' and npm ci for deterministic installs.npm install instead of npm ci in workflows -- it's slower, non-deterministic, and can modify package-lock.json.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.actions/checkout@v4) -- never use @main or floating tags in production workflows.permissions block with least-privilege access -- default GITHUB_TOKEN permissions may be too broad.::add-mask:: for dynamic values and store credentials in GitHub Secrets only.| 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 deps | 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 |
| Matrix 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 |
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
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.
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.
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.
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.
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.
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.
# .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 }}
# .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 }}
# .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
# ❌ BAD — npm install can modify package-lock.json and is non-deterministic
steps:
- run: npm install
- run: npm test
# ✅ GOOD — npm ci is faster, deterministic, and fails if lockfile is out of sync
steps:
- run: npm ci
- run: npm test
# ❌ BAD — @main can change at any time, breaking your workflow
steps:
- uses: actions/checkout@main
- uses: actions/setup-node@main
# ✅ GOOD — @v4 is stable and receives only non-breaking updates
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
# ❌ 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
# ✅ GOOD — caches ~/.npm global cache, works across Node versions
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
# ❌ 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 }}
# ✅ 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 }}
npm ci requires package-lock.json to exist. Fix: run npm install locally and commit the lockfile. [src2]node-version to .nvmrc or use node-version-file: '.nvmrc'. [src1]TZ: UTC in env. [src5]on.push.branches must match exactly. Fix: check branch name casing and use ** for wildcards. [src3]pull_request_target with caution. [src6]# 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 | 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 | 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 |
| 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 DAG pipelines | CircleCI or Jenkins |
| Want native GitHub integration (PR checks) | Need self-hosted GPU runners | GitLab CI with custom runners |
| Publishing npm packages | Need cross-repo artifact caching | GitHub Packages + custom cache |
| Simple to moderate CI/CD complexity | Need 100+ concurrent jobs | Self-hosted runner fleet |
npm ci command deletes the entire node_modules/ folder before installing, so caching node_modules/ is pointless when using npm ci.actions/upload-artifact@v4 artifacts expire after 90 days by default. Set retention-days to control this.workflow_dispatch triggers.