<?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>Forem: depmedicdev-byte</title>
    <description>The latest articles on Forem by depmedicdev-byte (@depmedicdevbyte).</description>
    <link>https://forem.com/depmedicdevbyte</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3901311%2F0eadbbf0-f410-447d-b3da-c1be7981a26d.png</url>
      <title>Forem: depmedicdev-byte</title>
      <link>https://forem.com/depmedicdevbyte</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/depmedicdevbyte"/>
    <language>en</language>
    <item>
      <title>ci-doctor vs octoscan: when to use which</title>
      <dc:creator>depmedicdev-byte</dc:creator>
      <pubDate>Tue, 28 Apr 2026 13:06:33 +0000</pubDate>
      <link>https://forem.com/depmedicdevbyte/ci-doctor-vs-octoscan-when-to-use-which-4625</link>
      <guid>https://forem.com/depmedicdevbyte/ci-doctor-vs-octoscan-when-to-use-which-4625</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/synacktiv/octoscan" rel="noopener noreferrer"&gt;octoscan&lt;/a&gt; is a Go-based security scanner from &lt;a href="https://www.synacktiv.com/" rel="noopener noreferrer"&gt;Synacktiv&lt;/a&gt; that audits GitHub Actions workflows for supply-chain vulnerabilities. People keep asking how it stacks up against &lt;a href="https://www.npmjs.com/package/ci-doctor" rel="noopener noreferrer"&gt;ci-doctor&lt;/a&gt;. Honest answer: they're complementary - run both.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;octoscan&lt;/strong&gt;: pure security focus. Goes deep on injection, dangerous triggers, runner take-over patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ci-doctor&lt;/strong&gt;: security + cost + reliability. Broader scope, less depth on pure security.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where octoscan wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deep injection analysis.&lt;/strong&gt; Tracks user-controllable context (&lt;code&gt;github.event.*&lt;/code&gt;) into shell commands across step boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dangerous trigger combinations.&lt;/strong&gt; Sub-rule precise on &lt;code&gt;pull_request_target&lt;/code&gt; + checkout patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted runner take-over patterns.&lt;/strong&gt; Catches non-ephemeral runner usage that lets a forked PR hijack the runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synacktiv pedigree.&lt;/strong&gt; They're a real offsec firm with public publications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single Go binary.&lt;/strong&gt; No runtime dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where ci-doctor wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost rules octoscan does not have.&lt;/strong&gt; &lt;code&gt;missing-concurrency&lt;/code&gt;, &lt;code&gt;missing-cache&lt;/code&gt;, &lt;code&gt;expensive-runner&lt;/code&gt;, &lt;code&gt;cron-storm&lt;/code&gt;, &lt;code&gt;wide-paths&lt;/code&gt;, &lt;code&gt;service-no-healthcheck&lt;/code&gt;. These hit the bill, not the build log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability rules octoscan does not have.&lt;/strong&gt; &lt;code&gt;missing-timeout-minutes&lt;/code&gt;, &lt;code&gt;flaky-retries&lt;/code&gt;, &lt;code&gt;legacy-actions-version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-fix mode.&lt;/strong&gt; &lt;code&gt;npx ci-doctor --fix&lt;/code&gt; rewrites four safe categories in place. octoscan is read-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sister CIs.&lt;/strong&gt; &lt;code&gt;gitlab-ci-doctor&lt;/code&gt;, &lt;code&gt;bitbucket-ci-doctor&lt;/code&gt;, &lt;code&gt;azure-pipelines-ci-doctor&lt;/code&gt;, &lt;code&gt;circleci-ci-doctor&lt;/code&gt;. octoscan is GitHub-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero install via &lt;code&gt;npx ci-doctor&lt;/code&gt;.&lt;/strong&gt; No compile, no PATH wiring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Companion &lt;code&gt;ci-doctor-action&lt;/code&gt;&lt;/strong&gt; with sticky PR comment + SARIF.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run them side by side
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-audit&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;octoscan&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -fsSL -o octoscan https://github.com/synacktiv/octoscan/releases/latest/download/octoscan_linux_amd64&lt;/span&gt;
          &lt;span class="s"&gt;chmod +x ./octoscan&lt;/span&gt;
          &lt;span class="s"&gt;./octoscan scan . &amp;gt; octoscan.json || true&lt;/span&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;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&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;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;octoscan.sarif&lt;/span&gt;
          &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;octoscan&lt;/span&gt;
  &lt;span class="na"&gt;ci-doctor&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&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;depmedicdev-byte/ci-doctor-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both reports show under the Security tab in different categories. They don't conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I bother writing these
&lt;/h2&gt;

&lt;p&gt;Same reason I wrote the &lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-zizmor.html" rel="noopener noreferrer"&gt;zizmor comparison&lt;/a&gt; and the &lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-actionlint.html" rel="noopener noreferrer"&gt;actionlint comparison&lt;/a&gt;. Every "X vs Y" comparison written by one of the maintainers is suspicious by default; I'd rather you have the honest version.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/depmedicdev-byte/ci-doctor/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; if anything here is wrong or outdated.&lt;/p&gt;

&lt;p&gt;Full version with styled comparison: &lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-octoscan.html" rel="noopener noreferrer"&gt;/compare/ci-doctor-vs-octoscan.html&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>security</category>
      <category>devops</category>
      <category>ci</category>
    </item>
    <item>
      <title>ci-doctor vs actionlint: when to use which</title>
      <dc:creator>depmedicdev-byte</dc:creator>
      <pubDate>Tue, 28 Apr 2026 13:06:32 +0000</pubDate>
      <link>https://forem.com/depmedicdevbyte/ci-doctor-vs-actionlint-when-to-use-which-56k3</link>
      <guid>https://forem.com/depmedicdevbyte/ci-doctor-vs-actionlint-when-to-use-which-56k3</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://www.npmjs.com/package/ci-doctor" rel="noopener noreferrer"&gt;ci-doctor&lt;/a&gt;. The most common question I get is "isn't this what &lt;a href="https://github.com/rhysd/actionlint" rel="noopener noreferrer"&gt;actionlint&lt;/a&gt; does?"&lt;/p&gt;

&lt;p&gt;Short answer: no, and you should run both. Here's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;Run both. We do, on every depmedic repo. &lt;strong&gt;actionlint&lt;/strong&gt; for correctness, &lt;strong&gt;ci-doctor&lt;/strong&gt; for cost and reliability. They take roughly 6 ms each on a typical repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where actionlint wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shell script linting via shellcheck.&lt;/strong&gt; Catches &lt;code&gt;[[ $foo == "bar" ]]&lt;/code&gt; instead of &lt;code&gt;[[ "$foo" == "bar" ]]&lt;/code&gt;, missing quotes, unsafe glob expansion, the whole shellcheck rule library inside &lt;code&gt;run:&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expression / context typing.&lt;/strong&gt; Knows &lt;code&gt;github.event.repository.private&lt;/code&gt; is a boolean and complains if you compare it to a string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action input validation.&lt;/strong&gt; Reads each action's &lt;code&gt;action.yml&lt;/code&gt; and verifies you're passing the right inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix matrix matrix.&lt;/strong&gt; Rich validation for &lt;code&gt;strategy.matrix.include&lt;/code&gt; / &lt;code&gt;exclude&lt;/code&gt; patterns.&lt;/li&gt;
&lt;li&gt;Pure Go binary, very fast, very mature, written by &lt;a href="https://github.com/rhysd" rel="noopener noreferrer"&gt;rhysd&lt;/a&gt; who knows GitHub Actions cold.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where ci-doctor wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost rules actionlint does not have.&lt;/strong&gt; &lt;code&gt;missing-concurrency&lt;/code&gt;, &lt;code&gt;missing-cache&lt;/code&gt;, &lt;code&gt;expensive-runner&lt;/code&gt;, &lt;code&gt;cron-storm&lt;/code&gt;, &lt;code&gt;wide-paths&lt;/code&gt;. These show up in the bill, not in the build log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability rules actionlint does not have.&lt;/strong&gt; &lt;code&gt;missing-timeout-minutes&lt;/code&gt;, &lt;code&gt;flaky-retries&lt;/code&gt;, &lt;code&gt;legacy-actions-version&lt;/code&gt;, &lt;code&gt;service-no-healthcheck&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-fix mode.&lt;/strong&gt; &lt;code&gt;npx ci-doctor --fix&lt;/code&gt; rewrites four safe categories in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$-denominated cost via &lt;a href="https://www.npmjs.com/package/gha-budget" rel="noopener noreferrer"&gt;&lt;code&gt;gha-budget&lt;/code&gt;&lt;/a&gt;.&lt;/strong&gt; Pairs cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SARIF + sticky PR comment&lt;/strong&gt; via &lt;a href="https://github.com/depmedicdev-byte/ci-doctor-action" rel="noopener noreferrer"&gt;&lt;code&gt;ci-doctor-action&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same engine ports to GitLab, Bitbucket, Azure Pipelines, CircleCI&lt;/strong&gt; with CI-native rules. One mental model across stacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run them side by side
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-audit&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;actionlint&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&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;reviewdog/action-actionlint@v1&lt;/span&gt;
  &lt;span class="na"&gt;ci-doctor&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&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;depmedicdev-byte/ci-doctor-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;12 seconds total. Reports go to different categories (actionlint inline review comments, ci-doctor sticky comment + Code Scanning). They don't conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest comparison styled
&lt;/h2&gt;

&lt;p&gt;Full table at &lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-actionlint.html" rel="noopener noreferrer"&gt;/compare/ci-doctor-vs-actionlint.html&lt;/a&gt;. Other comparisons in the family:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-zizmor.html" rel="noopener noreferrer"&gt;vs zizmor&lt;/a&gt; (security-focused analyzer)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-super-linter.html" rel="noopener noreferrer"&gt;vs super-linter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-mega-linter.html" rel="noopener noreferrer"&gt;vs mega-linter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-octoscan.html" rel="noopener noreferrer"&gt;vs octoscan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I bother writing these
&lt;/h2&gt;

&lt;p&gt;Because every "X vs Y" comparison written by one of the maintainers is suspicious by default, and I'd rather you have the honest version than discover the trade-offs after committing one to your CI pipeline. If anything here is wrong or outdated, &lt;a href="https://github.com/depmedicdev-byte/ci-doctor/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; and I'll fix it.&lt;/p&gt;

&lt;p&gt;We do not pay for placement and we do not accept paid placement.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>ci</category>
      <category>opensource</category>
    </item>
    <item>
      <title>ci-doctor vs zizmor: when to use which (honest take from one of the maintainers)</title>
      <dc:creator>depmedicdev-byte</dc:creator>
      <pubDate>Tue, 28 Apr 2026 12:32:04 +0000</pubDate>
      <link>https://forem.com/depmedicdevbyte/ci-doctor-vs-zizmor-when-to-use-which-honest-take-from-one-of-the-maintainers-47cb</link>
      <guid>https://forem.com/depmedicdevbyte/ci-doctor-vs-zizmor-when-to-use-which-honest-take-from-one-of-the-maintainers-47cb</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://www.npmjs.com/package/ci-doctor" rel="noopener noreferrer"&gt;ci-doctor&lt;/a&gt;. People keep asking how it stacks up against &lt;a href="https://woodruffw.github.io/zizmor/" rel="noopener noreferrer"&gt;zizmor&lt;/a&gt;, William Woodruff's Rust-based GitHub Actions security analyzer. So here's the honest version, not the marketing version.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — If your priority is supply-chain security and you only care about that, run zizmor first. If you also want cost waste catches and reliability rules in the same pass, add ci-doctor. Both have SARIF output, both are MIT.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where zizmor wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deeper template-injection analysis.&lt;/strong&gt; Traces user-controlled context (&lt;code&gt;github.event.issue.title&lt;/code&gt;, &lt;code&gt;github.event.pull_request.head.ref&lt;/code&gt;) into &lt;code&gt;run:&lt;/code&gt; blocks across step boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-rule precision on &lt;code&gt;pull_request_target&lt;/code&gt; + checkout patterns.&lt;/strong&gt; Knows the difference between checking out a PR head, checking out the base, and the new "trusted" mode you can opt into.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audits self-hosted runner labels, impostor commits, ref confusion, cache poisoning.&lt;/strong&gt; ci-doctor does not have any of these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;23+ pure security rules&lt;/strong&gt; vs ci-doctor's 6 security rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Rust binary,&lt;/strong&gt; no Node runtime needed. Single download, fast cold start.&lt;/li&gt;
&lt;li&gt;Maintained by a well-known supply-chain security researcher who's been doing this for years.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where ci-doctor wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost rules zizmor does not have.&lt;/strong&gt; &lt;code&gt;missing-concurrency&lt;/code&gt;, &lt;code&gt;missing-cache&lt;/code&gt;, &lt;code&gt;expensive-runner&lt;/code&gt;, &lt;code&gt;cron-storm&lt;/code&gt;, &lt;code&gt;wide-paths&lt;/code&gt;, &lt;code&gt;service-no-healthcheck&lt;/code&gt;. These are real money on free-tier and paid-tier alike.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability rules zizmor does not have.&lt;/strong&gt; &lt;code&gt;missing-timeout-minutes&lt;/code&gt;, &lt;code&gt;flaky-retries&lt;/code&gt;, &lt;code&gt;legacy-actions-version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-fix mode.&lt;/strong&gt; &lt;code&gt;npx ci-doctor --fix&lt;/code&gt; rewrites four safe categories in place. zizmor is read-only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pairs with &lt;a href="https://www.npmjs.com/package/gha-budget" rel="noopener noreferrer"&gt;&lt;code&gt;gha-budget&lt;/code&gt;&lt;/a&gt; for $-denominated cost numbers per workflow.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero install via &lt;code&gt;npx ci-doctor&lt;/code&gt;&lt;/strong&gt; — no compile, no PATH wiring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same engine ports to GitLab, Bitbucket, Azure, CircleCI.&lt;/strong&gt; One mental model across CIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rule overlap, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;zizmor&lt;/th&gt;
&lt;th&gt;ci-doctor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pin &lt;code&gt;uses:&lt;/code&gt; to SHAs&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template injection from user input&lt;/td&gt;
&lt;td&gt;deep&lt;/td&gt;
&lt;td&gt;basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pull_request_target&lt;/code&gt; + checkout&lt;/td&gt;
&lt;td&gt;yes (sub-rule precise)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container image not pinned to digest&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted runner label spoofing&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache poisoning vectors&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service container missing healthcheck&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;concurrency:&lt;/code&gt; (cost)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;timeout-minutes:&lt;/code&gt; (cost+reliability)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing language/dep cache (cost)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expensive runner (cost)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-fix mode&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes (&lt;code&gt;--fix&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SARIF output&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other CIs (GitLab, Bitbucket, Azure, CircleCI)&lt;/td&gt;
&lt;td&gt;no (GHA-only)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Run them side by side
&lt;/h2&gt;

&lt;p&gt;This is what I recommend for any team that takes both security and cost seriously:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-audit&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;zizmor&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&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-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install zizmor&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zizmor --format sarif . &amp;gt; zizmor.sarif&lt;/span&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;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;zizmor.sarif&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;zizmor&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;ci-doctor&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&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;depmedicdev-byte/ci-doctor-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both reports land in the same Security tab under different categories. No conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about actionlint, super-linter, MegaLinter, woodpecker?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://woodpecker-ci.org/" rel="noopener noreferrer"&gt;Woodpecker&lt;/a&gt; is a CI &lt;em&gt;system&lt;/em&gt;, not a workflow linter, so it isn't in the same category.&lt;/li&gt;
&lt;li&gt;actionlint is the gold standard for &lt;strong&gt;syntactic and shell-safety&lt;/strong&gt; checks. ci-doctor is &lt;strong&gt;cost and reliability first&lt;/strong&gt;. They're complementary - I run both on my own repos. Honest comparison: &lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-actionlint.html" rel="noopener noreferrer"&gt;/compare/ci-doctor-vs-actionlint.html&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;super-linter and MegaLinter are heavyweight wrappers that bundle 50+ language linters and incidentally include actionlint. Different problem entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I wrote this
&lt;/h2&gt;

&lt;p&gt;Because every "X vs Y" comparison written by one of the maintainers is suspicious by default, and I'd rather you have the honest version than discover the trade-offs after committing one to your CI.&lt;/p&gt;

&lt;p&gt;If anything here is wrong or outdated, &lt;a href="https://github.com/depmedicdev-byte/ci-doctor/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; and I'll fix it. We do not pay for placement and we do not accept paid placement.&lt;/p&gt;

&lt;p&gt;Full version with the table styled and link colors fixed: &lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-zizmor.html" rel="noopener noreferrer"&gt;/compare/ci-doctor-vs-zizmor.html&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>security</category>
      <category>devops</category>
      <category>ci</category>
    </item>
    <item>
      <title>I shipped 4 things this week: a GitHub Action, an Azure Pipelines auditor, a CircleCI auditor, and a comparison page nobody asked for</title>
      <dc:creator>depmedicdev-byte</dc:creator>
      <pubDate>Tue, 28 Apr 2026 12:30:33 +0000</pubDate>
      <link>https://forem.com/depmedicdevbyte/i-shipped-4-things-this-week-a-github-action-an-azure-pipelines-auditor-a-circleci-auditor-and-5fge</link>
      <guid>https://forem.com/depmedicdevbyte/i-shipped-4-things-this-week-a-github-action-an-azure-pipelines-auditor-a-circleci-auditor-and-5fge</guid>
      <description>&lt;p&gt;Week 2 of &lt;a href="https://depmedicdev-byte.github.io" rel="noopener noreferrer"&gt;depmedic&lt;/a&gt;. Four shipments, all free, all MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. ci-doctor-action - the easiest way to put ci-doctor on a PR
&lt;/h2&gt;

&lt;p&gt;Until now you'd have to write a workflow that ran &lt;code&gt;npx ci-doctor&lt;/code&gt;, piped output to &lt;code&gt;tee&lt;/code&gt;, then either uploaded SARIF or posted a comment. That's three steps, and most people never got past step one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/depmedicdev-byte/ci-doctor-action" rel="noopener noreferrer"&gt;ci-doctor-action&lt;/a&gt; is a composite Action that does it all in three lines:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-doctor&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.github/workflows/**'&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;audit&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&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;depmedicdev-byte/ci-doctor-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;sticky markdown PR comment&lt;/strong&gt; with the findings table (one per PR, updated in place when you push fixes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SARIF&lt;/strong&gt; uploaded to GitHub Code Scanning so findings show under the Security tab&lt;/li&gt;
&lt;li&gt;a configurable &lt;code&gt;fail-on&lt;/code&gt; threshold (default &lt;code&gt;error&lt;/code&gt;; bump to &lt;code&gt;warn&lt;/code&gt; for stricter teams)&lt;/li&gt;
&lt;li&gt;pinnable down to a SHA for fully reproducible audits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;v1&lt;/code&gt; mutable tag is already pushed at the same commit so consumers can use &lt;code&gt;@v1&lt;/code&gt;. Pinning the action AND its CLI is the gold-standard pattern - more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. azure-pipelines-ci-doctor - the fourth sister CLI
&lt;/h2&gt;

&lt;p&gt;The depmedic family now covers GitHub Actions, GitLab CI, Bitbucket Pipelines, and Azure Pipelines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/azure-pipelines-ci-doctor" rel="noopener noreferrer"&gt;azure-pipelines-ci-doctor&lt;/a&gt; ships with eight rules tuned to the quirks of Azure DevOps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;expensive-vm-image&lt;/code&gt; - flags &lt;code&gt;macOS-latest&lt;/code&gt; (~10x cost) and &lt;code&gt;windows-latest&lt;/code&gt; (~2x) when the steps don't actually need them&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;container-no-pin&lt;/code&gt; - floating &lt;code&gt;container.image&lt;/code&gt; tags&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;missing-timeout-in-minutes&lt;/code&gt; - default is &lt;strong&gt;60 min&lt;/strong&gt; hosted, &lt;strong&gt;360 min&lt;/strong&gt; self-hosted; one hang can burn the whole window&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;missing-cache&lt;/code&gt; - npm/pip/maven/gradle/cargo/go/bundler installs without a &lt;code&gt;Cache@2&lt;/code&gt; task&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wide-trigger&lt;/code&gt; - unscoped &lt;code&gt;trigger:&lt;/code&gt; or &lt;code&gt;pr:&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inline-secret-leak&lt;/code&gt; - &lt;code&gt;$(SECRET_NAME)&lt;/code&gt; macros that expand inline in build logs (the #1 Azure secret-handling mistake)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;legacy-task-version&lt;/code&gt; - outdated built-in task majors (e.g. &lt;code&gt;UseNode@1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unbounded-parallelism&lt;/code&gt; - &lt;code&gt;strategy.parallel &amp;gt;= 5&lt;/code&gt; without &lt;code&gt;maxParallel&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx azure-pipelines-ci-doctor              &lt;span class="c"&gt;# audit current repo&lt;/span&gt;
npx azure-pipelines-ci-doctor &lt;span class="nt"&gt;--markdown&lt;/span&gt;   &lt;span class="c"&gt;# PR-comment friendly&lt;/span&gt;
npx azure-pipelines-ci-doctor &lt;span class="nt"&gt;--rules&lt;/span&gt;      &lt;span class="c"&gt;# list all 8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In-browser scanner at &lt;a href="https://depmedicdev-byte.github.io/scan-azure.html" rel="noopener noreferrer"&gt;/scan-azure.html&lt;/a&gt; alongside the GitHub, GitLab, and Bitbucket equivalents.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. circleci-ci-doctor - the fifth sister CLI
&lt;/h2&gt;

&lt;p&gt;I promised this was "drafting now" in last week's newsletter, so I shipped it the same day. &lt;a href="https://www.npmjs.com/package/circleci-ci-doctor" rel="noopener noreferrer"&gt;circleci-ci-doctor&lt;/a&gt; is 8 rules over &lt;code&gt;.circleci/config.yml&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;expensive-resource-class&lt;/code&gt; - xlarge / 2xlarge / 3xlarge without heavy build commands (each tier roughly doubles credits/min)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;macos-executor&lt;/code&gt; - &lt;code&gt;macos:&lt;/code&gt; executor without xcodebuild/swift/fastlane (~10x Linux Docker cost)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker-no-pin&lt;/code&gt; - &lt;code&gt;docker.image&lt;/code&gt; not pinned to &lt;code&gt;@sha256:...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;missing-cache&lt;/code&gt; - install commands with no &lt;code&gt;restore_cache&lt;/code&gt;/&lt;code&gt;save_cache&lt;/code&gt; pair&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;orb-no-pin&lt;/code&gt; - orb ref not &lt;code&gt;MAJOR.MINOR.PATCH&lt;/code&gt; (e.g. &lt;code&gt;circleci/node@5&lt;/code&gt; or &lt;code&gt;@volatile&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;missing-no-output-timeout&lt;/code&gt; - hang-prone &lt;code&gt;run:&lt;/code&gt; step (tests, deploys, migrations) without &lt;code&gt;no_output_timeout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;secret-echo&lt;/code&gt; - &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;printenv&lt;/code&gt;, &lt;code&gt;set -x&lt;/code&gt;, &lt;code&gt;echo $TOKEN&lt;/code&gt; in a &lt;code&gt;run:&lt;/code&gt; block&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wide-filters&lt;/code&gt; - workflow job has no &lt;code&gt;filters:&lt;/code&gt; (runs on every branch)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser scanner at &lt;a href="https://depmedicdev-byte.github.io/scan-circleci.html" rel="noopener noreferrer"&gt;/scan-circleci.html&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. ci-doctor vs zizmor - an honest comparison nobody asked for
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://woodruffw.github.io/zizmor/" rel="noopener noreferrer"&gt;zizmor&lt;/a&gt; is great. It's a Rust-based static analyzer focused on supply-chain and template-injection security audits. People keep asking which to use.&lt;/p&gt;

&lt;p&gt;The honest answer is: &lt;strong&gt;run both&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;zizmor&lt;/th&gt;
&lt;th&gt;ci-doctor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pin &lt;code&gt;uses:&lt;/code&gt; to SHAs&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template injection from user input&lt;/td&gt;
&lt;td&gt;deep&lt;/td&gt;
&lt;td&gt;basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pull_request_target&lt;/code&gt; + checkout&lt;/td&gt;
&lt;td&gt;yes (sub-rule precise)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container image not pinned to digest&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted runner label spoofing&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache poisoning vectors&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service container missing healthcheck&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;concurrency:&lt;/code&gt; (cost)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;timeout-minutes:&lt;/code&gt; (cost+reliability)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing language/dep cache (cost)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expensive runner (cost)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-fix mode&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes (&lt;code&gt;--fix&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SARIF output&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other CIs (GitLab, Bitbucket, Azure, CircleCI)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Side-by-side 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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci-audit&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;zizmor&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&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-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.12'&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install zizmor&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zizmor --format sarif . &amp;gt; zizmor.sarif&lt;/span&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;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;zizmor.sarif&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;zizmor&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;ci-doctor&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;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&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/checkout@v4&lt;/span&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;depmedicdev-byte/ci-doctor-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full comparison: &lt;a href="https://depmedicdev-byte.github.io/compare/ci-doctor-vs-zizmor.html" rel="noopener noreferrer"&gt;/compare/ci-doctor-vs-zizmor.html&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern of the week: pin the action AND its CLI
&lt;/h2&gt;

&lt;p&gt;If you adopt &lt;code&gt;ci-doctor-action&lt;/code&gt; for production gating, pin both:&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="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;depmedicdev-byte/ci-doctor-action@1bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt; &lt;span class="c1"&gt;# v1.0.0&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;ci-doctor-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That way an upstream npm publish can't change what your gate flags or silently bump severity. Reproducible audits are the only audits worth gating on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The CircleCI Pin Bar - same methodology as the &lt;a href="https://depmedicdev-byte.github.io/blog/pin-bar-2026.html" rel="noopener noreferrer"&gt;GitHub Actions one (59% pinned to SHA)&lt;/a&gt;, applied to the top 30 OSS repos that publish their &lt;code&gt;.circleci/config.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An end-to-end scan of &lt;code&gt;.circleci/config.yml&lt;/code&gt; files across the 100-repo leaderboard.&lt;/li&gt;
&lt;li&gt;A "how much will my repo cost on each CI?" tool that takes a single set of workflows and prices them on GHA, GitLab, Bitbucket, Azure, and CircleCI side by side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subscribe at &lt;a href="https://depmedicdev-byte.github.io/newsletter.html" rel="noopener noreferrer"&gt;/newsletter.html&lt;/a&gt; if you want issue #3 in your inbox Sunday.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>ci</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I scanned 5 popular OSS repos in 5 minutes. Here's what I found.</title>
      <dc:creator>depmedicdev-byte</dc:creator>
      <pubDate>Tue, 28 Apr 2026 03:06:33 +0000</pubDate>
      <link>https://forem.com/depmedicdevbyte/i-scanned-5-popular-oss-repos-in-5-minutes-heres-what-i-found-3f9o</link>
      <guid>https://forem.com/depmedicdevbyte/i-scanned-5-popular-oss-repos-in-5-minutes-heres-what-i-found-3f9o</guid>
      <description>&lt;p&gt;Earlier today I shipped &lt;a href="https://depmedicdev-byte.github.io/scan.html" rel="noopener noreferrer"&gt;&lt;code&gt;scan.html&lt;/code&gt;&lt;/a&gt;: a one-page in-browser tool that takes any public GitHub repo URL, fetches its &lt;code&gt;.github/workflows/*.yml&lt;/code&gt;, and returns a per-workflow report using &lt;a href="https://www.npmjs.com/package/ci-doctor" rel="noopener noreferrer"&gt;&lt;code&gt;ci-doctor&lt;/code&gt;&lt;/a&gt; (14 rules) and &lt;a href="https://www.npmjs.com/package/gha-budget" rel="noopener noreferrer"&gt;&lt;code&gt;gha-budget&lt;/code&gt;&lt;/a&gt; (per-job pricing). Runs entirely client-side via the GitHub public API. No signup, nothing uploaded.&lt;/p&gt;

&lt;p&gt;To make sure it actually works on real-world repos and not just on the canned examples I built it against, I picked 5 well-known npm-ecosystem repos that I had not specifically optimized for, and ran them through. All 5 are maintained by experienced engineers. None of these are random small repos; they all matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 repos
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Workflows&lt;/th&gt;
&lt;th&gt;Per-run $&lt;/th&gt;
&lt;th&gt;Modeled $/mo*&lt;/th&gt;
&lt;th&gt;Findings (err / warn / info)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/axios/axios" rel="noopener noreferrer"&gt;axios/axios&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;$2.62&lt;/td&gt;
&lt;td&gt;$2,362&lt;/td&gt;
&lt;td&gt;40 (12 / 27 / 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/eslint/eslint" rel="noopener noreferrer"&gt;eslint/eslint&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;$1.54&lt;/td&gt;
&lt;td&gt;$1,382&lt;/td&gt;
&lt;td&gt;46 (0 / 40 / 6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/vitejs/vite" rel="noopener noreferrer"&gt;vitejs/vite&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;$1.09&lt;/td&gt;
&lt;td&gt;$979&lt;/td&gt;
&lt;td&gt;26 (0 / 25 / 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/prettier/prettier" rel="noopener noreferrer"&gt;prettier/prettier&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;$1.02&lt;/td&gt;
&lt;td&gt;$922&lt;/td&gt;
&lt;td&gt;32 (6 / 23 / 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/sveltejs/svelte" rel="noopener noreferrer"&gt;sveltejs/svelte&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$0.70&lt;/td&gt;
&lt;td&gt;$634&lt;/td&gt;
&lt;td&gt;14 (0 / 10 / 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$6.97&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$6,279&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;158 (18 / 125 / 15)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Modeled at 30 runs/day, 8 min/job, on standard &lt;code&gt;ubuntu-latest&lt;/code&gt; GitHub-hosted runner pricing. Real spend depends on actual run frequency, runner choice, and OSS rate-limit credits. The point of the column is comparison, not accusation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same 3 rules show up in all 5 repos
&lt;/h2&gt;

&lt;p&gt;This is the part I find genuinely interesting. These repos have nothing in common architecturally - vite is a bundler, axios is an HTTP client, eslint is a static analyzer, etc. - but the top-3 ci-doctor findings are nearly identical across all of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;missing-timeout&lt;/code&gt;&lt;/strong&gt; (76 hits across 5 repos). No &lt;code&gt;timeout-minutes:&lt;/code&gt; on jobs, so a hung step bills until GitHub's 6-hour default cap. Every repo has this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;missing-concurrency&lt;/code&gt;&lt;/strong&gt; (20 hits). Push 3 commits to a PR in 30 seconds, you get 3 stacked CI runs and GitHub bills all 3. &lt;code&gt;concurrency:&lt;/code&gt; with &lt;code&gt;cancel-in-progress: true&lt;/code&gt; kills the first 2 in milliseconds. Free 30-50% CI savings on PR-heavy repos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;missing-cache&lt;/code&gt;&lt;/strong&gt; (16 hits, mostly in eslint). &lt;code&gt;actions/setup-node&lt;/code&gt; without &lt;code&gt;cache: 'npm'&lt;/code&gt; / &lt;code&gt;'pnpm'&lt;/code&gt; / &lt;code&gt;'yarn'&lt;/code&gt; means every job re-downloads &lt;code&gt;node_modules&lt;/code&gt;. Slow and expensive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The interesting outlier is &lt;code&gt;axios/axios&lt;/code&gt; with &lt;strong&gt;12 error-severity findings&lt;/strong&gt;. All 12 are &lt;code&gt;deprecated-action&lt;/code&gt;: workflows still pinned to &lt;code&gt;actions/checkout@v3&lt;/code&gt;, &lt;code&gt;actions/setup-node@v3&lt;/code&gt;, and &lt;code&gt;actions/upload-artifact@v3&lt;/code&gt;. v3 of upload-artifact was deprecated in late 2024 and the v3 endpoint is being shut off. These are not "save 3% of your CI bill" findings; these are "your CI will silently start failing" findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these specific 3 are everywhere
&lt;/h2&gt;

&lt;p&gt;My theory: GitHub Actions doesn't push you to add any of them. The workflow files YAML-validates fine without a timeout, without concurrency, without a cache. The CI passes. The PR ships. There is no linter built in to nudge anyone toward better defaults. So the same 3 smells survive in every repo I scan, including in mine before I built ci-doctor.&lt;/p&gt;

&lt;p&gt;This is a tooling problem, not a competence problem. The maintainers of all 5 of these repos are excellent engineers. The smells are just invisible until something points at them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it (5-minute fixes)
&lt;/h2&gt;

&lt;p&gt;For each of the top 3 rules, here's the smallest possible change:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add a job-level timeout
&lt;/h3&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;# &amp;lt;-- add this&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Add concurrency at workflow scope
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# top of every workflow that runs on PRs&lt;/span&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.head_ref || 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="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Tell setup-node what package manager you use
&lt;/h3&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@v4&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;      &lt;span class="c1"&gt;# or 'pnpm' or 'yarn'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just run &lt;code&gt;npx ci-doctor --fix&lt;/code&gt; and let it patch &lt;code&gt;missing-concurrency&lt;/code&gt;, &lt;code&gt;missing-timeout&lt;/code&gt;, &lt;code&gt;wide-trigger&lt;/code&gt;, and &lt;code&gt;artifact-no-retention&lt;/code&gt; in place. The other 10 rules need a human to look at them, but those 4 are mechanical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run this scan on your own repo
&lt;/h2&gt;

&lt;p&gt;Free, browser-only, no signup: &lt;a href="https://depmedicdev-byte.github.io/scan.html" rel="noopener noreferrer"&gt;https://depmedicdev-byte.github.io/scan.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paste any public GitHub repo URL. Get the same per-workflow report above for your own code in about 10 seconds. Shareable result URL. Nothing uploaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All 5 repos were pulled via the GitHub public API, no auth.&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;.github/workflows/*.yml&lt;/code&gt; was passed through &lt;code&gt;ci-doctor&lt;/code&gt; 0.4.1 (14 rules) and &lt;code&gt;gha-budget&lt;/code&gt; for per-job pricing.&lt;/li&gt;
&lt;li&gt;Cost = sum of all jobs at GitHub-hosted standard &lt;code&gt;ubuntu-latest&lt;/code&gt; rates, assuming 8 min/job.&lt;/li&gt;
&lt;li&gt;Monthly = per-run * 30 runs/day * 30 days. Self-hosted and large-runner jobs are unpriced.&lt;/li&gt;
&lt;li&gt;This is the same engine that runs in the browser at &lt;a href="https://depmedicdev-byte.github.io/scan.html" rel="noopener noreferrer"&gt;&lt;code&gt;/scan.html&lt;/code&gt;&lt;/a&gt;. The 20-repo version of this analysis lives at &lt;a href="https://depmedicdev-byte.github.io/benchmarks.html" rel="noopener noreferrer"&gt;&lt;code&gt;/benchmarks.html&lt;/code&gt;&lt;/a&gt; with &lt;a href="https://depmedicdev-byte.github.io/examples/" rel="noopener noreferrer"&gt;per-repo deep dives&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this is NOT
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;This isn't an attack on any of these projects. They all ship excellent software, and "modeled $/mo" is not the same as "actual $/mo" - large OSS projects get free GitHub Actions credits, run things conditionally, and use self-hosted runners for the heavy lifting. The point is that the &lt;em&gt;same three workflow-YAML patterns&lt;/em&gt; show up in every repo I scan, including mine before I started building ci-doctor.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Free CLIs: &lt;a href="https://github.com/depmedicdev-byte/ci-doctor" rel="noopener noreferrer"&gt;ci-doctor&lt;/a&gt;, &lt;a href="https://github.com/depmedicdev-byte/gha-budget" rel="noopener noreferrer"&gt;gha-budget&lt;/a&gt;, &lt;a href="https://github.com/depmedicdev-byte/pin-actions" rel="noopener noreferrer"&gt;pin-actions&lt;/a&gt;. All MIT.&lt;/p&gt;

&lt;p&gt;If the in-browser report flags 5 things in your workflow and you'd rather just copy a known-good template than fix one rule at a time, the &lt;strong&gt;Cut Your CI Bill&lt;/strong&gt; cookbook ships 30 production patterns: monorepo dispatch, OIDC publish, security gates, matrix trims, the works. $19 one-time, MIT-licensed templates. &lt;a href="https://depmedicdev-byte.github.io" rel="noopener noreferrer"&gt;https://depmedicdev-byte.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>opensource</category>
      <category>ci</category>
    </item>
    <item>
      <title>GitHub Actions linters compared - actionlint, ci-doctor, sherif, octoscan</title>
      <dc:creator>depmedicdev-byte</dc:creator>
      <pubDate>Mon, 27 Apr 2026 23:41:03 +0000</pubDate>
      <link>https://forem.com/depmedicdevbyte/github-actions-linters-compared-actionlint-ci-doctor-sherif-octoscan-3cc3</link>
      <guid>https://forem.com/depmedicdevbyte/github-actions-linters-compared-actionlint-ci-doctor-sherif-octoscan-3cc3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Disclosure: I maintain &lt;code&gt;ci-doctor&lt;/code&gt;. The comparison below describes each tool by what it documents and ships, not by my opinion of its authors. Run all four on the same workflow to see for yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;GitHub Actions YAML is small enough that one tool could in theory validate everything: syntax, secret hygiene, runner cost, supply-chain pinning, deprecated actions, untrusted inputs. In practice, the open-source landscape splits this work across four projects. They overlap in some areas and miss each other in others.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;actionlint&lt;/th&gt;
&lt;th&gt;ci-doctor&lt;/th&gt;
&lt;th&gt;sherif&lt;/th&gt;
&lt;th&gt;octoscan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;YAML / shell syntax&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Untrusted input injection&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action ref pinned to SHA&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top-level &lt;code&gt;permissions&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency / cancel-in-progress&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job &lt;code&gt;timeout-minutes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache hint on setup-* actions&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact retention-days&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matrix combinatorial explosion&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost projection in dollars&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;via &lt;code&gt;gha-budget&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-fix in place&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-pin actions to SHA&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;via &lt;code&gt;pin-actions&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SARIF output for Code Scanning&lt;/td&gt;
&lt;td&gt;via wrappers&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo / multi-repo focus&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  actionlint
&lt;/h2&gt;

&lt;p&gt;The reference syntax checker. Written in Go, fast, no dependencies. Catches expression syntax errors, unknown event names, unknown contexts, malformed shell scripts in &lt;code&gt;run:&lt;/code&gt; blocks (it invokes &lt;code&gt;shellcheck&lt;/code&gt;), and a small number of security patterns like &lt;code&gt;${{ github.event.pull_request.title }}&lt;/code&gt; in a shell context.&lt;/p&gt;

&lt;p&gt;What it doesn't do: enforce policy. &lt;code&gt;actionlint&lt;/code&gt; tells you whether your YAML parses and runs; it does not tell you whether your workflow is cheap, secure-by-default, or maintainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it for:&lt;/strong&gt; pre-merge syntax validation. Catching shell quoting bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  ci-doctor
&lt;/h2&gt;

&lt;p&gt;Policy and cost focus. Eleven rules grouped into cost, security, and maintenance. Includes &lt;code&gt;--fix&lt;/code&gt; mode that auto-applies safe fixes (permissions, concurrency, timeouts, artifact retention) and &lt;code&gt;--sarif&lt;/code&gt; for GitHub Code Scanning. Pairs with &lt;a href="https://www.npmjs.com/package/pin-actions" rel="noopener noreferrer"&gt;&lt;code&gt;pin-actions&lt;/code&gt;&lt;/a&gt; for SHA pinning and &lt;a href="https://www.npmjs.com/package/gha-budget" rel="noopener noreferrer"&gt;&lt;code&gt;gha-budget&lt;/code&gt;&lt;/a&gt; for dollar cost projection.&lt;/p&gt;

&lt;p&gt;What it doesn't do: validate YAML or shell syntax (use actionlint), detect injection vulnerabilities at the expression level (use octoscan), or compare workflows across repos (use sherif).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it for:&lt;/strong&gt; cost discipline. Default-secure policy. PR comments. Code Scanning ingestion. Auto-fixing the four common issues that have a single safe answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  sherif
&lt;/h2&gt;

&lt;p&gt;Cross-repo / monorepo lens. Sherif's value is comparing workflows across many repos under one org and surfacing inconsistency: same job, different timeout; same matrix, different runner; same checkout step, different version.&lt;/p&gt;

&lt;p&gt;What it doesn't do: ship policy rules of its own. It tells you which workflows disagree; it does not say which one is right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it for:&lt;/strong&gt; standardising CI across an org once you've decided on a baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  octoscan
&lt;/h2&gt;

&lt;p&gt;Security-first. Looks for untrusted-input injection patterns specifically (the &lt;code&gt;${{ github.event.* }}&lt;/code&gt; in shell context class), unpinned actions, missing top-level &lt;code&gt;permissions&lt;/code&gt;, and similar hardening misses. Emits SARIF.&lt;/p&gt;

&lt;p&gt;What it doesn't do: cost analysis, auto-fix, cache hints, retention policy, matrix sanity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it for:&lt;/strong&gt; security audit before a release. Quick check that your &lt;code&gt;actions/checkout@...&lt;/code&gt; isn't shipping someone else's code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to combine them
&lt;/h2&gt;

&lt;p&gt;None of these tools is a superset of the others. The cheapest composition that covers all four lenses is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax&lt;/span&gt;
actionlint

&lt;span class="c"&gt;# cost + maintenance + auto-fix&lt;/span&gt;
npx ci-doctor &lt;span class="nt"&gt;--fix&lt;/span&gt;
npx ci-doctor &lt;span class="nt"&gt;--sarif&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ci-doctor.sarif

&lt;span class="c"&gt;# supply chain&lt;/span&gt;
npx pin-actions &lt;span class="nt"&gt;--check&lt;/span&gt;

&lt;span class="c"&gt;# expression-level injection scanning&lt;/span&gt;
octoscan run

&lt;span class="c"&gt;# cross-repo consistency, if you run an org&lt;/span&gt;
sherif &lt;span class="nt"&gt;--workspace&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total runtime on a normal repo: under five seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually run in CI
&lt;/h2&gt;

&lt;p&gt;In order, fail fast:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;actionlint&lt;/code&gt; - syntax must be valid before policy makes sense.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npx pin-actions --check&lt;/code&gt; - cheap, supply chain.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npx ci-doctor --sarif &amp;gt; ci-doctor.sarif&lt;/code&gt; + &lt;code&gt;codeql-action/upload-sarif&lt;/code&gt; - findings as PR annotations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;octoscan&lt;/code&gt; if your workflow accepts external input.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first three are non-negotiable for any repo with public CI. The fourth is non-negotiable for any workflow that runs against PRs from forks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try without installing
&lt;/h2&gt;

&lt;p&gt;If you want to see what &lt;code&gt;ci-doctor&lt;/code&gt; would say about your workflow without installing anything, paste it at &lt;a href="https://depmedicdev-byte.github.io/audit.html" rel="noopener noreferrer"&gt;https://depmedicdev-byte.github.io/audit.html&lt;/a&gt;. Same engine, runs entirely in your browser, share the result by URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  See also
&lt;/h2&gt;

&lt;p&gt;I priced the workflows of 20 famous OSS projects and counted what each linter would catch: &lt;a href="https://depmedicdev-byte.github.io/benchmarks.html" rel="noopener noreferrer"&gt;https://depmedicdev-byte.github.io/benchmarks.html&lt;/a&gt; (229 workflows, 902 findings, all data linked).&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>security</category>
      <category>ci</category>
    </item>
    <item>
      <title>I priced the GitHub Actions workflows of 20 famous OSS projects. The results were ugly.</title>
      <dc:creator>depmedicdev-byte</dc:creator>
      <pubDate>Mon, 27 Apr 2026 23:25:53 +0000</pubDate>
      <link>https://forem.com/depmedicdevbyte/i-priced-the-github-actions-workflows-of-20-famous-oss-projects-the-results-were-ugly-f03</link>
      <guid>https://forem.com/depmedicdevbyte/i-priced-the-github-actions-workflows-of-20-famous-oss-projects-the-results-were-ugly-f03</guid>
      <description>&lt;p&gt;There is a class of "best practices for GitHub Actions" article that just lists the same five tips - cancel concurrent runs, set a timeout, cache your dependencies, pin actions to a SHA, narrow your matrix. Every one of those articles is correct, and almost none of them tell you how often even the best open source projects miss those exact things.&lt;/p&gt;

&lt;p&gt;So I pulled the live workflow YAML out of &lt;code&gt;.github/workflows&lt;/code&gt; for 20 well-known repositories, priced every job at GitHub's published per-minute rates, and ran a linter against the whole set. The data and the tools are public.&lt;/p&gt;

&lt;p&gt;Headline numbers, all from public workflow YAML on &lt;code&gt;main&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;20 repos&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;229 workflows scanned&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;388 priced jobs&lt;/strong&gt; (159 more were jobs running on self-hosted or unknown runners and excluded from cost math)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;902 CI findings&lt;/strong&gt; flagged by &lt;code&gt;ci-doctor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;About $51,000 of combined monthly spend&lt;/strong&gt; assuming each job runs 30 times per day at 8 minutes per run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interactive table is at &lt;a href="https://depmedicdev-byte.github.io/benchmarks.html" rel="noopener noreferrer"&gt;https://depmedicdev-byte.github.io/benchmarks.html&lt;/a&gt;. A few of the per-repo numbers from the dataset:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;$/run&lt;/th&gt;
&lt;th&gt;Monthly @ 30/day&lt;/th&gt;
&lt;th&gt;Findings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;denoland/deno&lt;/td&gt;
&lt;td&gt;$18.30&lt;/td&gt;
&lt;td&gt;$16,473.60&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;facebook/react&lt;/td&gt;
&lt;td&gt;$16.19&lt;/td&gt;
&lt;td&gt;$14,572.80&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vercel/next.js&lt;/td&gt;
&lt;td&gt;(varies, see table)&lt;/td&gt;
&lt;td&gt;(varies)&lt;/td&gt;
&lt;td&gt;(varies)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The full table has all 20 repos, sorted by monthly spend.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What every project gets wrong
&lt;/h2&gt;

&lt;p&gt;Across 902 findings, the distribution is heavy on a small number of issues:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;missing-timeout&lt;/td&gt;
&lt;td&gt;364&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;missing-cache&lt;/td&gt;
&lt;td&gt;113&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pinned-action-sha&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;missing-permissions&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;artifact-no-retention&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;matrix-overcommit&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;missing-concurrency&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deprecated-action&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch-depth-zero&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expensive-runner&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wide-trigger&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;missing-timeout&lt;/code&gt; is the single biggest one. &lt;strong&gt;Out of 388 priced jobs, 364 of them are running without a &lt;code&gt;timeout-minutes&lt;/code&gt;.&lt;/strong&gt; A stuck job in CI does not exit on its own. It runs until GitHub kills it at 360 minutes, which is six hours of paid runner time, on top of whatever the job was actually trying to do.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;missing-cache&lt;/code&gt; is the second. Most projects have one cached language tool (npm, or pip, or whatever) and then forget to cache the rest. Every &lt;code&gt;pnpm install&lt;/code&gt; or &lt;code&gt;bundle install&lt;/code&gt; or &lt;code&gt;cargo build&lt;/code&gt; that runs without a cache costs you 1-3 paid minutes per job, every run.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;missing-concurrency&lt;/code&gt; only shows up 52 times but it is the highest-leverage one to fix. Without a &lt;code&gt;concurrency:&lt;/code&gt; block, a developer who pushes three quick fixes in a row triggers three full CI runs back to back. With a &lt;code&gt;concurrency:&lt;/code&gt; block, the first two cancel and only the third runs. On an active PR this can cut your monthly spend in half.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the data was generated
&lt;/h2&gt;

&lt;p&gt;Three small open-source tools, all on npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Audit a workflow:&lt;/span&gt;
npx ci-doctor .github/workflows/ci.yml

&lt;span class="c"&gt;# Estimate the cost of a workflow:&lt;/span&gt;
npx gha-budget .github/workflows/ci.yml &lt;span class="nt"&gt;--runs-per-day&lt;/span&gt; 30 &lt;span class="nt"&gt;--minutes&lt;/span&gt; 8

&lt;span class="c"&gt;# Pin every action in a workflow to a SHA:&lt;/span&gt;
npx pin-actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benchmark fetched workflow YAML for each repo via the GitHub REST API, then ran &lt;code&gt;ci-doctor&lt;/code&gt; and &lt;code&gt;gha-budget&lt;/code&gt; against every file. Each priced job was multiplied by 30 runs/day and 30 days/month. Self-hosted runners and &lt;code&gt;runs-on&lt;/code&gt; values not in the GitHub price sheet were excluded from cost math but still scanned for findings. Code is in &lt;a href="https://github.com/depmedicdev-byte" rel="noopener noreferrer"&gt;https://github.com/depmedicdev-byte&lt;/a&gt; if you want to reproduce it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do today
&lt;/h2&gt;

&lt;p&gt;If you have a workflow you suspect is expensive, the fastest check is to paste the YAML into one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://depmedicdev-byte.github.io/audit.html" rel="noopener noreferrer"&gt;https://depmedicdev-byte.github.io/audit.html&lt;/a&gt; - paste, get findings, no install&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://depmedicdev-byte.github.io/budget.html" rel="noopener noreferrer"&gt;https://depmedicdev-byte.github.io/budget.html&lt;/a&gt; - paste, get $/run and monthly projection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both run entirely in your browser. They share findings and totals via URL hash so you can send a teammate a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually do
&lt;/h2&gt;

&lt;p&gt;If I had to fix one thing per repo for maximum dollar impact, the order would be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }&lt;/code&gt; to every PR-triggered workflow.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;timeout-minutes: 15&lt;/code&gt; (or whatever your real p99 is) to every job.&lt;/li&gt;
&lt;li&gt;Cache whatever your install step is downloading. &lt;code&gt;actions/cache&lt;/code&gt; for ad-hoc dirs, the official setup actions for language tools.&lt;/li&gt;
&lt;li&gt;Pin every &lt;code&gt;uses:&lt;/code&gt; to a full commit SHA. This is a security move more than a cost move, but the same &lt;code&gt;pin-actions&lt;/code&gt; CLI handles it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those four changes alone, applied across the 20 projects in the dataset, would cut the combined monthly spend roughly in half. The longer set of patterns (60+) is in the &lt;a href="https://buy.polar.sh/polar_cl_E2HGFeAVxJ64gU0Tv0qGwAueuxvhuq6A0pjhE4BWTyD" rel="noopener noreferrer"&gt;Cut Your CI Bill cookbook&lt;/a&gt; but you do not need it to fix the top three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-runs and updates
&lt;/h2&gt;

&lt;p&gt;The dataset will be re-run monthly. If you want a specific repo added, open an issue on any of the tool repos and I will include it.&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>opensource</category>
      <category>ci</category>
    </item>
  </channel>
</rss>
