Most Node.js projects need the same CI job: install, lint, test, build. The default starter workflows do that, but two small settings make a noticeable difference: caching npm downloads, and cancelling runs that a newer push has already replaced.
Here's the whole workflow, then a walk-through.
name: CI (Node.js)
on:
push:
branches: [main]
pull_request:
# Cancel superseded runs on the same branch to save minutes.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
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 --if-present
- run: npm test --if-present
- run: npm run build --if-present
1. Cancel runs you no longer care about
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
If you push twice to the same branch in a row, the first run is now pointless. The concurrency group is the workflow name plus the branch ref, so a new run on that branch cancels the older one instead of letting both finish. That saves Actions minutes, and you get your result sooner.
Use this for CI only. For deploy or release workflows, cancelling halfway through is usually not what you want.
2. Cache npm downloads
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache: npm tells setup-node to cache npm's download cache (~/.npm), keyed on your package-lock.json. When the lockfile hasn't changed, packages you already have aren't downloaded again.
3. Use npm ci, not npm install
npm ci installs exactly what's in your lockfile and fails if package.json and the lockfile disagree. In CI that's what you want: the same dependency tree on every run. It does need a committed package-lock.json.
4. Let missing scripts be skipped
- run: npm run lint --if-present
- run: npm test --if-present
- run: npm run build --if-present
--if-present means npm skips a script that isn't defined in package.json instead of failing. So one workflow works for a library with no build step, or for a new repo that doesn't have a linter yet. Add the scripts later and CI picks them up.
5. Read-only token by default
permissions:
contents: read
The job only needs to read your code, so the GITHUB_TOKEN gets read access to repository contents and nothing more. If a later step needs to write (comment on a PR, push a tag), grant that permission explicitly.
Using it
Save the file as .github/workflows/ci-node.yml, change node-version and the main branch name if yours differ, and push. It runs on pushes to main and on every pull request.
This is a single Node.js CI workflow, not a full CI setup. It doesn't deploy, publish, or test across several Node versions or operating systems.
Get the file
If you'd rather copy a commented version than retype it, it's a free ($0) download on Gumroad: https://8686702537545.gumroad.com/l/sqjvco?ref=devto-nodeci
Disclosure: I make ShipKit, which gives away this sample and sells paid GitHub Actions packs.
Top comments (0)