ci-doctor rules reference
Every rule that ci-doctor runs
against your .github/workflows/*.yml files. 14 rules, three severities,
three categories (cost, security, reliability). The four marked with
--fix can be auto-applied. Run npx ci-doctor in any repo
to see your own findings.
Contents
missing-concurrencywarn
Workflows triggered on push or pull_request should declare a concurrency group with cancel-in-progress: true. Auto-fixable with --fix.
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps: [...]
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps: [...]
missing-timeoutwarn
Jobs without timeout-minutes default to 360 (6 hours). A runaway job burns minutes you pay for. Auto-fixable with --fix.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- run: npm test
missing-cachewarn
setup-* actions without a cache option re-download dependencies on every run.
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
node_modules from npm registry costs ~30-60 seconds per job. With cache: npm the second run pulls from GitHub's cache in ~3 seconds. setup-python, setup-go, setup-java all support the same flag.missing-permissionswarn
Without a top-level permissions block, GITHUB_TOKEN gets the repository default - usually write-all.
on: pull_request: jobs: test: ...
on: pull_request: permissions: contents: read jobs: test: ...
GITHUB_TOKEN to push to main, create releases, or delete branches if permissions are wide. Set the workflow's permissions to the minimum it needs.pinned-action-shawarn
Third-party actions should be pinned to a full commit SHA, not a tag or branch. Use npx pin-actions to bulk-fix.
- uses: tj-actions/changed-files@v45
- uses: tj-actions/changed-files@a284dc1814e3fc6e30d9f170b3d17d61b4a7156c # v45.0.4
tj-actions/changed-files compromise replaced the published tag with a payload that exfiltrated secrets. Repos that pinned to a SHA were unaffected. actions/* from GitHub itself is the only safe-to-tag namespace.deprecated-actionerror
Pinned to a deprecated major version (Node12/16, set-output, save-state, ::set-env). GitHub will start failing these.
- uses: actions/checkout@v2 - run: echo "::set-output name=x::y"
- uses: actions/checkout@v4 - run: echo "x=y" >> "$GITHUB_OUTPUT"
::set-output from new runners and Node12-based actions stopped working in 2023. Node16 sunset is in progress. Pinning to a v2 of an official action is a ticking time bomb.expensive-runnerwarn
macos-* runners cost 10x and windows-* costs 2x ubuntu. Use them only when platform-specific commands are present.
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- run: npm test
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
xcodebuild, codesign, or use macOS-only APIs, drop the macOS runner.matrix-overcommitwarn
A matrix that crosses many OS or version axes can multiply CI minutes silently.
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [16, 18, 20, 22]
pg: [13, 14, 15, 16]
strategy:
matrix:
os: [ubuntu-latest]
node: [18, 20, 22]
include:
- os: macos-latest
node: 20
- os: windows-latest
node: 20
include: to add specific cells without multiplying everything.stale-cache-keywarn
actions/cache step has a key that does not include a lockfile hash, so the cache never invalidates when deps change.
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-cache
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
package-lock.json. Builds get stale dependencies and you are paying for cache storage that never gets refreshed.fail-fast-trueinfo
Matrix job uses default fail-fast: true, which kills sibling jobs on first failure - wasting their already-billed minutes and hiding parallel failures.
strategy:
matrix:
node: [18, 20, 22]
# fail-fast defaults to true
strategy:
fail-fast: false
matrix:
node: [18, 20, 22]
fail-fast: false is almost always the right call.always-run-on-prinfo
A heavy step (docker build, e2e, codeql) runs on every PR with no paths: filter, no label gate, and no condition. It runs whether or not the PR touched anything that matters to it.
on:
pull_request:
jobs:
e2e:
steps:
- uses: cypress-io/github-action@v6
with:
spec: cypress/e2e/**/*.cy.ts
on:
pull_request:
paths:
- 'src/**'
- 'cypress/**'
- 'package*.json'
jobs:
e2e:
steps:
- uses: cypress-io/github-action@v6
with:
spec: cypress/e2e/**/*.cy.ts
paths: filters or if: contains(github.event.pull_request.labels.*.name, 'needs-e2e') to gate expensive steps.artifact-no-retentioninfo
upload-artifact without retention-days uses the repo default (often 90 days) and bills storage for the full window.
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
retention-days: 7
fetch-depth-zeroinfo
actions/checkout with fetch-depth: 0 pulls full history. Slow and rarely needed.
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4 # default fetch-depth: 1 (just HEAD)
fetch-depth: 0 downloads every commit ever. Tools like semantic-release, git diff main...HEAD, and changelog generators need it - but most jobs do not. If you only need the diff against the base branch, use fetch-depth: 2 or fetch the base ref selectively.wide-triggerinfo
on: push without a branches filter runs the workflow on every branch push.
on: push:
on:
push:
branches: [main]
pull_request:
branches filter, every feature branch push runs the workflow once on push and again on the PR open. Restrict push to your protected branch and let pull_request handle the rest.Run all 14 rules in 6 ms
npx ci-doctor in any repo. Zero config. Or pipe to GitHub Code Scanning with npx ci-doctor --sarif > results.sarif.
Want the full pattern set?
The Cut Your CI Bill cookbook is 30 paste-ready GitHub Actions patterns plus 5 hardened workflow templates - the long-form versions of these rules with edge cases handled. $19, one-time, MIT-licensed templates.
Get the cookbook Free preview (5 patterns)See also: paste-and-audit your YAML · cost estimator · how 20 popular OSS repos score · per-repo deep dives