<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: robsonheals</title>
    <description>The latest articles on DEV Community by robsonheals (@robsonheals).</description>
    <link>https://dev.to/robsonheals</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4222389%2F418cb6ec-b1d5-44de-957d-54124d34253b.png</url>
      <title>DEV Community: robsonheals</title>
      <link>https://dev.to/robsonheals</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robsonheals"/>
    <language>en</language>
    <item>
      <title>I scanned the GitHub Actions setup of 139 popular JS/TS repos. Only 1 passed 3 basic checks.</title>
      <dc:creator>robsonheals</dc:creator>
      <pubDate>Mon, 02 Nov 2026 12:48:22 +0000</pubDate>
      <link>https://dev.to/robsonheals/i-scanned-the-github-actions-setup-of-139-popular-jsts-repos-only-1-passed-3-basic-checks-26gp</link>
      <guid>https://dev.to/robsonheals/i-scanned-the-github-actions-setup-of-139-popular-jsts-repos-only-1-passed-3-basic-checks-26gp</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I only checked three things, all boring and all cheap to fix:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dependency cache&lt;/strong&gt;: do CI workflows re-download every package on every run?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency on pull requests&lt;/strong&gt;: when you push again to a PR, does the outdated run get cancelled?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job timeouts&lt;/strong&gt;: does each job have &lt;code&gt;timeout-minutes&lt;/code&gt;, or does it fall back to GitHub's default of &lt;strong&gt;6 hours&lt;/strong&gt;?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repos with PR workflows that have &lt;strong&gt;no concurrency group&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;97 of 129 (75%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI jobs with &lt;strong&gt;no &lt;code&gt;timeout-minutes&lt;/code&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,329 of 2,181 (61%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repos that install deps with &lt;strong&gt;no cache&lt;/strong&gt; in at least one CI workflow&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;41 of 121 (34%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repos that passed &lt;strong&gt;all three&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 of 139&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: cancel outdated PR runs (3 lines)
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event_name == 'pull_request' }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This only cancels runs on pull requests. Pushes to &lt;code&gt;main&lt;/code&gt;, and deploys that run in the same workflow, always finish.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Updated after a great catch in the comments: an earlier version used &lt;code&gt;cancel-in-progress: true&lt;/code&gt;, which also cancels runs on &lt;code&gt;main&lt;/code&gt; and can interrupt a deploy.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: put a timeout on every job (1 line)
&lt;/h2&gt;

&lt;p&gt;A test waiting on a port that never opens, or a &lt;code&gt;--watch&lt;/code&gt; flag left on in CI, can hold a runner for 6 hours.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;   &lt;span class="c1"&gt;# about 2x the job's normal duration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix 3: cache dependencies (1 line)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v7&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lts/*&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;   &lt;span class="c1"&gt;# or yarn / pnpm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since setup-node v5, npm projects get this automatically &lt;strong&gt;if&lt;/strong&gt; &lt;code&gt;package.json&lt;/code&gt; has a &lt;code&gt;packageManager&lt;/code&gt; field set to npm. My scanner accounts for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately didn't count
&lt;/h2&gt;

&lt;p&gt;I wanted the numbers to be conservative, not scary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local composite actions and reusable workflows&lt;/strong&gt; are skipped for the cache check. I can't see inside them, and many of them do cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release, publish and deploy workflows&lt;/strong&gt; are skipped. They often avoid caches on purpose, as hardening against cache poisoning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicitly disabled caches&lt;/strong&gt; (&lt;code&gt;package-manager-cache: false&lt;/code&gt;) are treated as a deliberate choice.&lt;/li&gt;
&lt;li&gt;I &lt;strong&gt;checked 15 flagged repos by hand&lt;/strong&gt;. 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cache number is per repo: a repo counts if at least one CI workflow installs without a cache, even if that particular workflow skips the cache on purpose (a sub-minute lint job, for example).&lt;/p&gt;

&lt;p&gt;Bias warning: this is popular open source. Company repos may look better or worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own repo
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://devrescue.vercel.app" rel="noopener noreferrer"&gt;devrescue.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The method and the per-repo raw data are public: &lt;a href="https://devrescue.vercel.app/research" rel="noopener noreferrer"&gt;devrescue.vercel.app/research&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you think a heuristic is wrong, tell me in the comments. That's the most useful feedback I can get.&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>cicd</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
