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:
- Dependency cache: do CI workflows re-download every package on every run?
- Concurrency on pull requests: when you push again to a PR, does the outdated run get cancelled?
- 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 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 Fix 3: cache dependencies (1 line)
- uses: actions/setup-node@v7 with: node-version: lts/* cache: npm # or yarn / pnpm 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:
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.

