Cover image for My CI ran the full test suite every time I edited a markdown file

My CI ran the full test suite every time I edited a markdown file


June 05, 2026

Cover image for My CI ran the full test suite every time I edited a markdown file
Hero: a CI billing dashboard showing roughly 42% of the month's included minutes already spent, next to the cost of a two-line markdown change

I open the GitHub Actions usage page for the month and 1,269 of my 3,000 included minutes are already gone. There is no real feature in the app yet. It is still mostly scaffolding, a few foundation pieces, a lot of planning docs. So I click into the run history to see where the minutes went, and the answer is sitting right at the top: a pull request that changed two markdown files.

Twenty minutes to change two lines of prose

That pull request edited two log files. Markdown. Nothing a build, a test, or a deploy could possibly care about.

It ran anyway. The full pipeline: typecheck, lint, format check, the production build, the unit tests, a Playwright run across four browsers, a throwaway Postgres branch spun up for the integration tests, and a preview deploy. GitHub bills each job rounded up to the next minute, so the small jobs still cost a full minute each. Add them up and that one docs PR billed about nine minutes.

Then I merged it, and both of my workflows fired again on the main branch. Another eleven minutes or so.

Roughly twenty billable minutes so two lines of prose could land. And here is the part that made it a real problem instead of a curiosity: a small automation in this project writes one of those log files every single weekday, and the repo produces a steady stream of spec edits, planning notes, and content docs on top of that. None of it touches the app. All of it was paying the full CI bill. It was a structural leak, not a one-off.

The obvious fix, and the trap underneath it

The obvious fix is path filtering. Tell the workflow to ignore markdown and docs folders, run only when real code changes. GitHub Actions has paths-ignore built in for exactly this, right at the top of the workflow file.

So I reached for it. And this is where I learned something I did not know.

Four of these checks are required status checks on my main branch. The branch protection rule will not let a pull request merge until each one reports a passing result. That is the whole point of marking them required; it is what stops me from merging something red.

Here is the trap. If I add paths-ignore at the workflow level, a docs-only PR makes the entire workflow not run. Which means those four required checks never report any result at all. Which means the pull request sits forever on "Expected, waiting for status to be reported," and it cannot be merged without an admin override.

So the naive fix does not just fail quietly. It takes the easiest, safest changes I make, the one-line doc edits, and makes every single one of them unmergeable. The filter meant to save me time would have jammed the door shut.

The thing that actually works lives one level down. You let the workflow start, and you skip the individual jobs with a condition on each job instead. A job that gets skipped reports its conclusion as "skipped," and branch protection counts a skipped required check as satisfied. The PR goes green. It merges. The heavy compute never runs.

Same goal, two places to put the filter, and only one of them leaves the PR mergeable. That distinction is not where you would look first. I found it by doing the wrong one and watching a test PR hang.

How the gate actually works

The shape I landed on is a small job that runs first and looks at what changed. It checks out the history, diffs the pull request against its base, and sorts the changed files into buckets. Is there any real code in here, or is it all docs? Did the frontend change? The backend? A shared package that both apps depend on?

It writes those answers out as simple true/false flags. Then every expensive job declares that it needs the gate job, and that it only runs when its own flag is true.

That bought me per-area scoping almost for free. A backend-only PR skips the four-browser test run, because the frontend never moved. A frontend-only PR skips the integration tests and the Postgres branch they need. A change to a shared package runs everything, because shared code can break both apps at once. A docs-only PR skips all four heavy jobs and finishes in seconds.

The part I was most careful about

The dangerous failure here is not running CI when you did not need to. That just wastes a couple of minutes, and minutes are cheap. The dangerous failure is the opposite: skipping CI on a change that actually needed it, and merging something broken because the tests never ran.

So I built the classifier as a denylist, not an allowlist. It treats a change as real code by default, and only marks it docs-only when every changed file matches a known-harmless pattern. A brand new code directory I have not thought of yet triggers the full suite automatically, because it does not match the ignore list. The gate is biased toward running too much, never too little.

And the diff step fails open. If it cannot work out the base commit, or the diff command errors for any reason at all, it sets every flag to true and runs the whole suite. The gate is allowed to waste minutes. It is never allowed to wave a broken change through in silence.

The deploy workflow got the simpler treatment. It does not gate any merges, so there is no required-check trap to worry about there. On that one I used the workflow-level filter, the exact tool I could not use on the PR checks.

What the work actually was

I verified the whole thing on a throwaway PR before trusting it. Edited a markdown file, watched the four required checks report "skipped," watched the PR go mergeable anyway. Then pushed a code change to the same PR and watched all four run.

I worked through this the same way I am building everything else in this project, with Claude alongside me. It is fast at writing the YAML and fast at explaining what paths-ignore does. It did not tell me that the workflow-level filter would wedge my required checks; I found that by trying it and watching the PR hang. The speed is real. The judgment about what to actually do with it is still mine to get right.

The whole fix is maybe forty lines of YAML. The work was never the forty lines. It was noticing a billing number that did not match the work, treating my own pipeline like something worth debugging instead of background noise, and knowing the easy fix had a sharp edge before it shipped rather than after.


#BuildInPublic #ClaudeCode #CICD #DevOps #GitHubActions