DEV Community

Anthany Wingo
Anthany Wingo

Posted on

The GitHub Actions CI workflow I copy into every Node project

Every new repo needs CI, and I kept rewriting the same YAML from memory and forgetting something each time. So here's the version I now copy everywhere, with notes on why each setting is there.

The workflow

Save as .github/workflows/ci.yml:

name: CI (Node.js)
on:
  push:
    branches: [main]
  pull_request:
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
Enter fullscreen mode Exit fullscreen mode

Four settings most templates skip

1. Cancel superseded runs

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
Enter fullscreen mode Exit fullscreen mode

Push three fixes to a PR in a row and only the latest run finishes. Fewer wasted minutes and a faster signal. (For deploy workflows, set cancel-in-progress: false so a deploy is never cut off halfway.)

2. Least-privilege token

permissions:
  contents: read
Enter fullscreen mode Exit fullscreen mode

CI only needs to read your code. Saying so explicitly limits what a compromised dependency or action could do with GITHUB_TOKEN.

3. Built-in dependency caching

cache: npm on actions/setup-node caches downloads keyed on package-lock.json, with no extra actions/cache step. Pair it with npm ci for installs that exactly match your lockfile.

4. --if-present

npm run lint --if-present skips the step if the script doesn't exist, so the same file works on day one of a project and on day 500.

Bonus: lint your workflows

bash <(curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint
Enter fullscreen mode Exit fullscreen mode

actionlint catches bad expressions, typos in action inputs and (with shellcheck installed) bugs in your run: scripts before you push.

Get it

What's the one thing you always add to CI? Let me know in the comments.

Top comments (0)