<?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: A.F.</title>
    <description>The latest articles on DEV Community by A.F. (@ace2932).</description>
    <link>https://dev.to/ace2932</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%2F4005999%2F2ead730b-1eb8-4838-8bdd-f4543176d881.png</url>
      <title>DEV Community: A.F.</title>
      <link>https://dev.to/ace2932</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ace2932"/>
    <language>en</language>
    <item>
      <title>Your GitHub Actions bill is mostly macOS minutes (here's how to tell)</title>
      <dc:creator>A.F.</dc:creator>
      <pubDate>Mon, 06 Jul 2026 23:36:52 +0000</pubDate>
      <link>https://dev.to/ace2932/your-github-actions-bill-is-mostly-macos-minutes-heres-how-to-tell-56fk</link>
      <guid>https://dev.to/ace2932/your-github-actions-bill-is-mostly-macos-minutes-heres-how-to-tell-56fk</guid>
      <description>&lt;p&gt;If your GitHub Actions bill surprises you, look at the macOS line first. A macOS runner minute bills at roughly ten times a Linux minute, and the xlarge tiers cost more still. One ten-minute macOS job is a hundred Linux-equivalent minutes. Run it on every PR and it is almost certainly the biggest single line on your bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multipliers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt; is the baseline (about $0.006/min on a standard 2-core runner in 2026).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt; bills roughly 2x Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt; bills roughly 10x Linux, and the arm64 xlarge tiers bill above that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The billing page shows minutes. It does not shout about the multiplier. Ten thousand macOS minutes and ten thousand Linux minutes look the same in a usage table and differ by an order of magnitude in dollars.&lt;/p&gt;

&lt;h2&gt;
  
  
  How serious projects end up here
&lt;/h2&gt;

&lt;p&gt;Not by accident, exactly. By default-following. vscode's PR pipeline calls its darwin test workflow four times per pull request, each on &lt;code&gt;runs-on: macos-14-xlarge&lt;/code&gt;. For an editor that ships a native macOS app, that is a defensible choice made by a team with a Microsoft-sized budget. The pattern to copy is the deliberateness, not the config: most repos running macOS jobs on every PR are not shipping macOS binaries. The job landed in a template years ago and nobody priced it since.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually needs macOS
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Xcode builds, iOS/macOS app compilation, simulator tests&lt;/li&gt;
&lt;li&gt;Code signing and notarization&lt;/li&gt;
&lt;li&gt;Reproducing genuinely platform-specific bugs (filesystem case-insensitivity, BSD userland)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What doesn't: your unit tests, your linter, your web build "just to be safe." If the job would pass in a Linux container, it is paying a 10x premium for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes, cheapest first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Move Linux-capable jobs off premium runners.&lt;/strong&gt; Usually a one-line change:&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="c1"&gt;# was macos-latest — ~10x cheaper&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Gate macOS to where it earns its rate.&lt;/strong&gt; Run it on the default branch or a release tag, not every PR commit:&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;macos-test&lt;/span&gt;&lt;span class="pi"&gt;:&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;github.ref == 'refs/heads/main'&lt;/span&gt;   &lt;span class="c1"&gt;# PRs run the Linux leg only&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;macos-14&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Trim the matrix.&lt;/strong&gt; A full OS x version grid multiplies the premium legs with everything else. And set &lt;code&gt;timeout-minutes&lt;/code&gt; everywhere it hurts most: a hung macOS job burns the 6-hour default at 10x the rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find every premium-runner job you have
&lt;/h2&gt;

&lt;p&gt;I built a free scanner for exactly this class of config waste: it flags every workflow using macOS or Windows runners, plus the other quiet defaults (missing timeouts, no concurrency group, uncached dependencies) with copy-paste fixes. Two minutes to find out which of your workflows are on the expensive tier: &lt;a href="https://gitspider.com/scan?ref=devto" rel="noopener noreferrer"&gt;gitspider.com/scan&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is a syndicated version of &lt;a href="https://gitspider.com/guides/github-actions-macos-runner-cost" rel="noopener noreferrer"&gt;the full guide&lt;/a&gt;, which also covers the free-plan macOS concurrency cap and per-stack examples.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The one-line GitHub Actions cache most repos forget</title>
      <dc:creator>A.F.</dc:creator>
      <pubDate>Wed, 01 Jul 2026 16:56:00 +0000</pubDate>
      <link>https://dev.to/ace2932/the-one-line-github-actions-cache-most-repos-forget-1ke7</link>
      <guid>https://dev.to/ace2932/the-one-line-github-actions-cache-most-repos-forget-1ke7</guid>
      <description>&lt;p&gt;The first thing I check on a slow CI run is the cache, and the cache is usually missing. It is the cheapest speedup there is: one line on your setup step, and every run stops re-downloading and rebuilding the exact dependencies it already had yesterday. On a typical Node or Python project that is 30 to 90 seconds back, every run, forever. People skip it anyway, then pay GitHub to reinstall lodash for the ten-thousandth time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free win: built-in caching
&lt;/h2&gt;

&lt;p&gt;You probably do not need &lt;code&gt;actions/cache&lt;/code&gt; directly. The &lt;code&gt;setup-*&lt;/code&gt; actions cache your package manager for you, you just have to turn it on:&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="c1"&gt;# Node&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-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="m"&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 'yarn' / 'pnpm'&lt;/span&gt;

&lt;span class="c1"&gt;# Python&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="na"&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="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;pip'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go (&lt;code&gt;actions/setup-go&lt;/code&gt;) caches by default. Java (&lt;code&gt;actions/setup-java&lt;/code&gt;) needs &lt;code&gt;cache: 'gradle'&lt;/code&gt; (or &lt;code&gt;'maven'&lt;/code&gt;) set explicitly. Check your setup step first, the one-line fix covers most projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom caching with actions/cache
&lt;/h2&gt;

&lt;p&gt;For anything the setup actions do not cover (a build output, a tool the package manager does not own), reach for &lt;code&gt;actions/cache&lt;/code&gt; with an explicit path and key:&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;actions/cache@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;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.cache/my-tool&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-mytool-${{ hashFiles('**/lockfile') }}&lt;/span&gt;
    &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;${{ runner.os }}-mytool-&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get the key right, or the cache never helps
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;key&lt;/code&gt; should change only when your dependencies change, which is why it hashes the lockfile (&lt;code&gt;hashFiles('**/package-lock.json')&lt;/code&gt;). The &lt;code&gt;restore-keys&lt;/code&gt; prefix lets a run fall back to the most recent matching cache when the exact key misses, so a single dependency bump restores most of the cache instead of rebuilding everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your cache "isn't working"
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Caching the wrong directory: cache the package manager's store, not &lt;code&gt;node_modules&lt;/code&gt; on most setups.&lt;/li&gt;
&lt;li&gt;A key with no lockfile hash, so it never invalidates and you keep serving a stale cache.&lt;/li&gt;
&lt;li&gt;Cache scope: a branch reads its own cache plus the default branch's, but not a sibling branch's, so cross-branch hits can surprise you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Find out if you're missing it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gitspider.com/scan/withastro/astro?ref=devto" rel="noopener noreferrer"&gt;astro&lt;/a&gt;, for instance, has a release workflow with no dependency cache. Curious about yours? See real scorecards on the &lt;a href="https://gitspider.com/showcase?ref=devto" rel="noopener noreferrer"&gt;showcase&lt;/a&gt;, then scan your own.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://gitspider.com/?ref=devto" rel="noopener noreferrer"&gt;GitSpider&lt;/a&gt;, which scans a repo's GitHub Actions and flags the missing cache (and the rest) with the fix for each. Free, no install for public repos: &lt;a href="https://gitspider.com/scan?ref=devto" rel="noopener noreferrer"&gt;gitspider.com/scan&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Cancel duplicate GitHub Actions runs with one block (and the one place you shouldn't)</title>
      <dc:creator>A.F.</dc:creator>
      <pubDate>Wed, 01 Jul 2026 03:33:34 +0000</pubDate>
      <link>https://dev.to/ace2932/cancel-duplicate-github-actions-runs-with-one-block-and-the-one-place-you-shouldnt-2m3k</link>
      <guid>https://dev.to/ace2932/cancel-duplicate-github-actions-runs-with-one-block-and-the-one-place-you-shouldnt-2m3k</guid>
      <description>&lt;p&gt;You push a fix, spot a typo a second later, and push again. GitHub cheerfully runs both. The first run, building a commit you've already replaced, grinds through your entire CI matrix while you sit waiting on the second. On a busy pull request that fans out to a dozen jobs, a real slice of your Actions minutes goes to building commits that were dead on arrival. The fix is one block of YAML, and it's the single most common thing missing from the repos I scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one block
&lt;/h2&gt;

&lt;p&gt;Drop this at the top level of a workflow file, alongside &lt;code&gt;on:&lt;/code&gt; and &lt;code&gt;jobs:&lt;/code&gt;:&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.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;p&gt;&lt;code&gt;group&lt;/code&gt; buckets runs by workflow and branch. When a new run starts in a bucket that already has one going, &lt;code&gt;cancel-in-progress: true&lt;/code&gt; kills the old one. Push twice and only the latest survives, instead of both running to the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the group key actually does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;github.ref&lt;/code&gt; is the branch (or, on a pull request, that PR's ref), so every branch and every PR gets its own bucket. Pushing to your PR cancels your previous run and touches nobody else's. Keeping &lt;code&gt;github.workflow&lt;/code&gt; in the key stops separate workflows (CI vs lint vs docs) from cancelling each other.&lt;/p&gt;

&lt;p&gt;One rule: never put &lt;code&gt;github.sha&lt;/code&gt; or &lt;code&gt;github.run_id&lt;/code&gt; in the group key. They are unique per commit and per run, so every run lands in its own bucket and nothing ever cancels. That's the number-one reason a concurrency block "isn't working." To make the key read as the branch name on pull requests (forks included), use &lt;code&gt;${{ github.workflow }}-${{ github.head_ref || github.ref }}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency on a single job
&lt;/h2&gt;

&lt;p&gt;The block can live on a job, not just the whole workflow, which is how you serialize one step (a deploy) while everything else still fans out:&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;deploy&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;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;deploy-${{ 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;false&lt;/span&gt;   &lt;span class="c1"&gt;# never two deploys at once&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Top-level &lt;code&gt;concurrency:&lt;/code&gt; governs the whole run; job-level governs that one job. Reach for job-level when only the deploy needs to be one-at-a-time and the tests can keep fanning out in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one place you do NOT want cancel-in-progress
&lt;/h2&gt;

&lt;p&gt;Deploy, release, and publish workflows. Cancelling a deploy halfway can leave a half-pushed image or a partially published package. For those you still want a concurrency group, so two never run at once, but with cancellation OFF, which queues them instead:&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;deploy-${{ 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;false&lt;/span&gt;   &lt;span class="c1"&gt;# serialize deploys, never interrupt one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same logic for your default branch: you usually want every commit to &lt;code&gt;main&lt;/code&gt; built and deployed, not cancelled by the next merge. A common pattern cancels only on branches and PRs:&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.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.ref != 'refs/heads/main' }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it's worth the thirty seconds
&lt;/h2&gt;

&lt;p&gt;A cancelled run is minutes you don't spend. On private repos and self-hosted runners that's a direct line on the bill; on a public repo it's still faster feedback and less queue contention, since a superseded run isn't tying up a runner your real build is waiting for. Three lines, the cheapest reliability win in Actions, and almost nobody sets it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does it cancel matrix jobs too?&lt;/strong&gt; Yes. A matrix run is still one workflow run, so a superseded run cancels every matrix leg at once, not one at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why isn't my concurrency cancelling anything?&lt;/strong&gt; Almost always the group key is too unique, usually a &lt;code&gt;github.sha&lt;/code&gt; or &lt;code&gt;github.run_id&lt;/code&gt; that snuck in, so no two runs ever share a bucket. Or &lt;code&gt;cancel-in-progress&lt;/code&gt; is missing (it defaults to false).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I queue runs instead of cancelling them?&lt;/strong&gt; Yes, set &lt;code&gt;cancel-in-progress: false&lt;/code&gt;. GitHub keeps one run going and at most one waiting, cancelling older pending runs but never the one in flight. That's the right mode for deploys and releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find out if you're missing it
&lt;/h2&gt;

&lt;p&gt;It's the most common gap I see, big repos included. &lt;a href="https://gitspider.com/scan/prettier/prettier?ref=devto" rel="noopener noreferrer"&gt;prettier&lt;/a&gt;, for instance, has no concurrency group on its test workflow. See real 30-day Actions scorecards on the &lt;a href="https://gitspider.com/showcase?ref=devto" rel="noopener noreferrer"&gt;showcase&lt;/a&gt;, then scan your own.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://gitspider.com/?ref=devto" rel="noopener noreferrer"&gt;GitSpider&lt;/a&gt;, which scans a repo's GitHub Actions and flags exactly which of these patterns apply, with the fix for each. Free, no install for public repos: &lt;a href="https://gitspider.com/scan?ref=devto" rel="noopener noreferrer"&gt;gitspider.com/scan&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The GitHub Actions workflow that's been failing for weeks (and how to find yours)</title>
      <dc:creator>A.F.</dc:creator>
      <pubDate>Mon, 29 Jun 2026 21:49:48 +0000</pubDate>
      <link>https://dev.to/ace2932/the-github-actions-workflow-thats-been-failing-for-weeks-and-how-to-find-yours-2oj</link>
      <guid>https://dev.to/ace2932/the-github-actions-workflow-thats-been-failing-for-weeks-and-how-to-find-yours-2oj</guid>
      <description>&lt;p&gt;trpc has a scheduled workflow called "Lock Issues &amp;amp; PRs." Its own &lt;a href="https://gitspider.com/scan/trpc/trpc?ref=devto" rel="noopener noreferrer"&gt;scorecard&lt;/a&gt; shows it failing on almost every run. It is still scheduled, still running, still red. trpc ships excellent software, which is exactly the point: if a project this careful has a workflow that has been red for ages, the rest of us almost certainly do too.&lt;/p&gt;

&lt;p&gt;It is not a one-off. &lt;a href="https://gitspider.com/scan/drizzle-team/drizzle-orm?ref=devto" rel="noopener noreferrer"&gt;drizzle-orm&lt;/a&gt; has one ("Unpublish release"). &lt;a href="https://gitspider.com/scan/calcom/cal.com?ref=devto" rel="noopener noreferrer"&gt;cal.com&lt;/a&gt; has one ("PR Update"). I scanned 35 popular open-source repos and the same thing kept turning up: a scheduled workflow that fails on nearly every run, quietly, for a long time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody notices
&lt;/h2&gt;

&lt;p&gt;GitHub does email you when a scheduled workflow fails. So how do these survive? Two reasons.&lt;/p&gt;

&lt;p&gt;First, those emails are routine. You get them for flaky reruns and transient blips too, so you filter them out. Second, a workflow that is &lt;em&gt;always&lt;/em&gt; red stops reading as a signal. It is just how that row looks now.&lt;/p&gt;

&lt;p&gt;I did exactly this on my own project. GitHub emailed me that a workflow had failed. The next day it emailed again. I saw it, told myself I would fix it tomorrow, and promptly forgot. It was my nightly database backup, quietly broken the whole time, and I only caught it when a failure-rate number crept up where I would notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  An always-red workflow is not free
&lt;/h2&gt;

&lt;p&gt;It burns minutes every run to produce nothing but a red X. Worse, it trains you to ignore the failure that actually matters: the day a real one lands in the same inbox you have learned to skim past.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find yours
&lt;/h2&gt;

&lt;p&gt;Open your Actions tab and look at the &lt;em&gt;scheduled&lt;/em&gt; workflows, the cron-triggered ones nobody watches. If the last several runs are all red, you found one. From the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh run list &lt;span class="nt"&gt;--workflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Lock Issues &amp;amp; PRs"&lt;/span&gt; &lt;span class="nt"&gt;--status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;Two honest options: fix it, or if the workflow is genuinely abandoned, turn it off. Do not leave it scheduled and red.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh workflow disable &lt;span class="s2"&gt;"Lock Issues &amp;amp; PRs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or drop the &lt;code&gt;schedule&lt;/code&gt; trigger from the workflow file if it should not run on a timer at all. A disabled workflow is honest. A red one you have trained yourself to ignore is a liability.&lt;/p&gt;




&lt;p&gt;I built a free scanner that flags always-failing workflows (and other CI waste) for any public repo, no install required: &lt;a href="https://gitspider.com/scan?ref=devto" rel="noopener noreferrer"&gt;gitspider.com/scan&lt;/a&gt;. The full writeup with the config fixes lives &lt;a href="https://gitspider.com/guides/github-actions-workflow-always-failing?ref=devto" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>6 reasons your GitHub Actions CI is slow, with the one-line fix for each</title>
      <dc:creator>A.F.</dc:creator>
      <pubDate>Mon, 29 Jun 2026 21:49:26 +0000</pubDate>
      <link>https://dev.to/ace2932/why-your-github-actions-ci-is-slow-and-how-to-speed-it-up-19la</link>
      <guid>https://dev.to/ace2932/why-your-github-actions-ci-is-slow-and-how-to-speed-it-up-19la</guid>
      <description>&lt;p&gt;Two days ago GitHub emailed me to say one of my workflows had failed. The next day it emailed me again. I saw it, told myself I'd fix it tomorrow, and promptly forgot. It was my nightly database backup, quietly broken the whole time, and I only caught it because a failure-rate number nudged up.&lt;/p&gt;

&lt;p&gt;A &lt;em&gt;failed&lt;/em&gt; run at least gets you an email. A &lt;em&gt;slow&lt;/em&gt; run gets you nothing. GitHub never pings you when CI quietly takes twice as long, runs the whole suite twice per PR, or rebuilds dependencies from scratch every time. That waste compounds where no one looks. Here are the usual culprits, each with the exact fix.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When I scanned 35 popular open-source repos, not one had a fully clean config. 32 of 35 had no concurrency control, 33 of 35 had no job timeouts, and 22 of 35 ran the full suite twice on every PR. If projects this polished leave minutes on the table, the rest of us definitely do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Your suite runs twice on every PR (push + pull_request)
&lt;/h2&gt;

&lt;p&gt;Trigger a workflow on both &lt;code&gt;push&lt;/code&gt; and &lt;code&gt;pull_request&lt;/code&gt; and, for a branch in the same repo, opening a PR fires both. You just paid for two identical runs. This one is pure waste and it can roughly halve your PR-related minutes. Trigger on &lt;code&gt;pull_request&lt;/code&gt;, and keep &lt;code&gt;push&lt;/code&gt; for your default branch:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Old runs don't cancel when you push again (no concurrency group)
&lt;/h2&gt;

&lt;p&gt;Push a fix 30 seconds after the first push and, with no concurrency group, both runs go to completion. The first is dead weight, and it is holding a slot in your queue while it finishes. This hides even when you do have a group: a concurrency block &lt;em&gt;without&lt;/em&gt; &lt;code&gt;cancel-in-progress&lt;/code&gt; queues the new run behind the old one instead of cancelling it, so the superseded run still finishes. Add a group keyed on the branch, with &lt;code&gt;cancel-in-progress&lt;/code&gt;, so a new push supersedes the old run:&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.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;h2&gt;
  
  
  Every run reinstalls dependencies from scratch (no cache)
&lt;/h2&gt;

&lt;p&gt;No cache means every run re-downloads and rebuilds your dependencies. On a typical Node or Python project that is roughly 30 to 90 seconds per run, every run, forever. The &lt;code&gt;setup-*&lt;/code&gt; actions cache for free, you just have to ask:&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;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="m"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Your matrix is bigger than it needs to be
&lt;/h2&gt;

&lt;p&gt;Every OS times every version multiplies your minutes. Most regressions show up on a single combination, so run a slim matrix on PRs and save the full grid for your default branch or a nightly run:&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;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# add macos/windows only on main or nightly&lt;/span&gt;
    &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A README typo runs your whole test suite (no paths filter)
&lt;/h2&gt;

&lt;p&gt;Without a &lt;code&gt;paths&lt;/code&gt; filter, any change triggers full CI, docs-only commits included. Scope the trigger to the files that actually affect the build:&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;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;src/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package.json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A hung job can run for six hours (no timeout-minutes)
&lt;/h2&gt;

&lt;p&gt;With no &lt;code&gt;timeout-minutes&lt;/code&gt;, a stuck step runs until GitHub's 6-hour ceiling. One wedged run can quietly eat a day of minutes. Cap every job:&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;build&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to find which ones you have
&lt;/h2&gt;

&lt;p&gt;Curious how the big projects do it? The &lt;a href="https://gitspider.com/showcase?ref=devto" rel="noopener noreferrer"&gt;showcase&lt;/a&gt; has real 30-day Actions scorecards for popular repos. Then point GitSpider at your own.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://gitspider.com/?ref=devto" rel="noopener noreferrer"&gt;GitSpider&lt;/a&gt;, which scans a repo's GitHub Actions and flags exactly which of these apply, with the fix for each. Free, no install for public repos: &lt;a href="https://gitspider.com/scan?ref=devto" rel="noopener noreferrer"&gt;gitspider.com/scan&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I scanned 35 popular repos' GitHub Actions. Here's what's broken.</title>
      <dc:creator>A.F.</dc:creator>
      <pubDate>Sun, 28 Jun 2026 05:59:20 +0000</pubDate>
      <link>https://dev.to/ace2932/i-scanned-35-popular-repos-github-actions-heres-whats-broken-335b</link>
      <guid>https://dev.to/ace2932/i-scanned-35-popular-repos-github-actions-heres-whats-broken-335b</guid>
      <description>&lt;p&gt;The 2026 pricing changes got me curious how the projects I actually use run their CI. So I scanned 35 well-known open-source repos (vite, next.js, prisma, astro, eslint, the usual suspects) and pulled their public GitHub Actions data: recent runs, config patterns, failure rates.&lt;/p&gt;

&lt;p&gt;A few things genuinely surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some workflows have been failing nearly every run. And nobody turned them off.
&lt;/h2&gt;

&lt;p&gt;The one I didn't expect: trpc has a scheduled workflow ("Lock Issues &amp;amp; PRs") that's failing nearly every run, and it's still on. Its own scorecard flags it outright: "failing almost every run." (see it: &lt;a href="https://gitspider.com/scan/trpc/trpc" rel="noopener noreferrer"&gt;https://gitspider.com/scan/trpc/trpc&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;It wasn't alone. drizzle-orm and cal.com each have a workflow stuck in the same red-forever state.&lt;/p&gt;

&lt;p&gt;This isn't a knock on those teams (they ship great software). It's how universal the "set a scheduled workflow and never look at it again" pattern is. If projects this good have one that's been red for months, most of us do too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premium runners are the norm, not the exception.
&lt;/h2&gt;

&lt;p&gt;20 of the 35 run at least some CI on macOS or Windows, which bill ~10x and ~2x a Linux minute. Free on public repos, but if you're private and on macOS, your bill is a different universe from the Linux-only projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not one of the 35 had a clean config
&lt;/h2&gt;

&lt;p&gt;Every single repo had at least one fixable finding. Most common across all 35:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing job timeout: 33/35&lt;/li&gt;
&lt;li&gt;no paths filters: 32/35&lt;/li&gt;
&lt;li&gt;no concurrency cancel: 32/35&lt;/li&gt;
&lt;li&gt;push + pull_request both running the full suite: 22/35&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are hard to fix. They're just spread across dozens of workflow files, so nobody ever sits down and does it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability varies more than I'd have guessed
&lt;/h2&gt;

&lt;p&gt;Failure rates ran from near-zero up to about a third of recent runs: solid ~34%, trpc ~32%, drizzle ~23%. Some is flaky tests, some is those always-red workflows dragging the average.&lt;/p&gt;




&lt;p&gt;Caveats so this is honest: these are public repos, so Actions is free for them. The dollar figures (on the scorecards) are "what it would cost at paid rates," not their bill. And the scanner samples recent runs (capped at 500), so the counts are a sample, not a 30-day total.&lt;/p&gt;

&lt;p&gt;I built the scanner (it's free, no install for public repos). The scorecards are here: &lt;a href="https://gitspider.com/showcase?ref=devto" rel="noopener noreferrer"&gt;https://gitspider.com/showcase?ref=devto&lt;/a&gt; , and you can scan your own at &lt;a href="https://gitspider.com/scan?ref=devto" rel="noopener noreferrer"&gt;https://gitspider.com/scan?ref=devto&lt;/a&gt; . Honestly the most useful thing is just checking your own repo for that one workflow that's been red for months.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I scanned 35 popular repos' GitHub Actions. Here's what's broken.</title>
      <dc:creator>A.F.</dc:creator>
      <pubDate>Sun, 28 Jun 2026 05:59:20 +0000</pubDate>
      <link>https://dev.to/ace2932/i-scanned-35-popular-repos-github-actions-heres-whats-broken-npm</link>
      <guid>https://dev.to/ace2932/i-scanned-35-popular-repos-github-actions-heres-whats-broken-npm</guid>
      <description>&lt;p&gt;The 2026 pricing changes got me curious how the projects I actually use run their CI. So I scanned 35 well-known open-source repos (vite, next.js, prisma, astro, eslint, the usual suspects) and pulled their public GitHub Actions data: recent runs, config patterns, failure rates.&lt;/p&gt;

&lt;p&gt;A few things genuinely surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some workflows have been failing nearly every run. And nobody turned them off.
&lt;/h2&gt;

&lt;p&gt;The one I didn't expect: trpc has a scheduled workflow ("Lock Issues &amp;amp; PRs") that's failing nearly every run, and it's still on. Its own scorecard flags it outright: "failing almost every run." (see it: &lt;a href="https://gitspider.com/scan/trpc/trpc" rel="noopener noreferrer"&gt;https://gitspider.com/scan/trpc/trpc&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;It wasn't alone. drizzle-orm and cal.com each have a workflow stuck in the same red-forever state.&lt;/p&gt;

&lt;p&gt;This isn't a knock on those teams (they ship great software). It's how universal the "set a scheduled workflow and never look at it again" pattern is. If projects this good have one that's been red for months, most of us do too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premium runners are the norm, not the exception.
&lt;/h2&gt;

&lt;p&gt;20 of the 35 run at least some CI on macOS or Windows, which bill ~10x and ~2x a Linux minute. Free on public repos, but if you're private and on macOS, your bill is a different universe from the Linux-only projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not one of the 35 had a clean config
&lt;/h2&gt;

&lt;p&gt;Every single repo had at least one fixable finding. Most common across all 35:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing job timeout: 33/35&lt;/li&gt;
&lt;li&gt;no paths filters: 32/35&lt;/li&gt;
&lt;li&gt;no concurrency cancel: 32/35&lt;/li&gt;
&lt;li&gt;push + pull_request both running the full suite: 22/35&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are hard to fix. They're just spread across dozens of workflow files, so nobody ever sits down and does it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability varies more than I'd have guessed
&lt;/h2&gt;

&lt;p&gt;Failure rates ran from near-zero up to about a third of recent runs: solid ~34%, trpc ~32%, drizzle ~23%. Some is flaky tests, some is those always-red workflows dragging the average.&lt;/p&gt;




&lt;p&gt;Caveats so this is honest: these are public repos, so Actions is free for them. The dollar figures (on the scorecards) are "what it would cost at paid rates," not their bill. And the scanner samples recent runs (capped at 500), so the counts are a sample, not a 30-day total.&lt;/p&gt;

&lt;p&gt;I built the scanner (it's free, no install for public repos). The scorecards are here: &lt;a href="https://gitspider.com/showcase?ref=devto" rel="noopener noreferrer"&gt;https://gitspider.com/showcase?ref=devto&lt;/a&gt; , and you can scan your own at &lt;a href="https://gitspider.com/scan?ref=devto" rel="noopener noreferrer"&gt;https://gitspider.com/scan?ref=devto&lt;/a&gt; . Honestly the most useful thing is just checking your own repo for that one workflow that's been red for months.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
