DEV Community

robsonheals
robsonheals

Posted on

I scanned the GitHub Actions setup of 139 popular JS/TS repos. Only 1 passed 3 basic checks.

I've been building a GitHub Actions linter and didn't want to trust my own heuristics, so I ran it on real projects first: the most-starred JavaScript and TypeScript repos with 1,000–20,000 stars that were active since September 2026. That gave 149 repos, and 139 of them had workflows.

I only checked three things, all boring and all cheap to fix:

  1. Dependency cache: do CI workflows re-download every package on every run?
  2. Concurrency on pull requests: when you push again to a PR, does the outdated run get cancelled?
  3. Job timeouts: does each job have timeout-minutes, or does it fall back to GitHub's default of 6 hours?

The results

Check Result
Repos with PR workflows that have no concurrency group 97 of 129 (75%)
CI jobs with no timeout-minutes 1,329 of 2,181 (61%)
Repos that install deps with no cache in at least one CI workflow 41 of 121 (34%)
Repos that passed all three 1 of 139

These are popular, well-maintained projects run by experienced people. That isn't a criticism of them. It shows how easy these defaults are to miss.

Fix 1: cancel outdated PR runs (2 lines)

Without a concurrency group, every push to a PR starts a full new run while the previous one keeps going. Add this at the top level of the workflow:

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

For deploy workflows, use cancel-in-progress: false. You never want to cancel a deploy halfway.

Fix 2: put a timeout on every job (1 line)

A test waiting on a port that never opens, or a --watch flag left on in CI, can hold a runner for 6 hours.

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15   # about 2x the job's normal duration
Enter fullscreen mode Exit fullscreen mode

Fix 3: cache dependencies (1 line)

- uses: actions/setup-node@v7
  with:
    node-version: lts/*
    cache: npm   # or yarn / pnpm
Enter fullscreen mode Exit fullscreen mode

Since setup-node v5, npm projects get this automatically if package.json has a packageManager field set to npm. My scanner accounts for that.

What I deliberately didn't count

I wanted the numbers to be conservative, not scary:

  • Local composite actions and reusable workflows are skipped for the cache check. I can't see inside them, and many of them do cache.
  • Release, publish and deploy workflows are skipped. They often avoid caches on purpose, as hardening against cache poisoning.
  • Explicitly disabled caches (package-manager-cache: false) are treated as a deliberate choice.
  • I checked 15 flagged repos by hand. After the first round I tightened a rule; in the second round, 6 of 7 were correct. The remaining one tests clean installs on purpose.

Bias warning: this is popular open source. Company repos may look better or worse.

Check your own repo

I turned the checks into a free web tool. Paste a public repo and get a 0–100 score with the exact file and line to change. No signup, and nothing is stored:

👉 devrescue.vercel.app

The method and the per-repo raw data are public: devrescue.vercel.app/research

If you think a heuristic is wrong, tell me in the comments. That's the most useful feedback I can get.

Top comments (1)

Collapse
 
techdevfun profile image
TECHDEV •

The conservative counting is what makes these numbers believable — hand-checking 15 flagged repos and skipping composite actions you can't see inside is the part most linter writeups skip. The bias warning too.

One edge worth flagging on Fix 1: the two-line snippet cancels in-progress runs for every trigger, not just PR pushes. Plenty of repos run CI and deploy jobs in one workflow (build → deploy behind an environment gate), and a second push to main would cancel the first run mid-deploy — the exact scenario your "deploy workflows use false" note warns about. Would you scope the linter's concurrency expectation to pull_request-triggered workflows, or recommend gating cancel-in-progress so mixed workflows don't need a whole-file false?

Smaller question on the cache check: a repo where the fast lint workflow intentionally skips cache (few deps, sub-minute install) but the test workflow caches gets flagged as a repo-level miss. Deliberate, or is per-workflow severity a future knob?