CI runs too often
Why docs changes trigger full CI (and how to fix it)
By Keith Mazanec, Founder, CostOps ยท Updated January 30, 2026
A developer fixes a typo in the README. GitHub Actions spins up the full CI pipeline, including linting, unit tests, integration tests, and the build step. Fifteen minutes of compute, billed in full, to validate a one-word change in a Markdown file. By default, GitHub Actions does not distinguish between code changes and documentation changes. Every push runs every workflow. Path filters fix this in a few lines of YAML. This is a specific case of the broader problem covered in why your CI runs on every push.
Symptoms
How to tell if docs-only commits are wasting CI minutes
Open your repository’s Actions tab and look at recent workflow runs. Filter by runs where the commit message mentions "docs", "readme", "typo", or "changelog". If those runs executed the full test suite, you have this problem:
-
Full CI runs on Markdown-only commits. A commit that changes only README.md, CHANGELOG.md, or files in docs/ triggers the same 15-minute workflow as a commit that changes application code. The test suite passes every time because nothing testable changed.
-
Zero failures on non-code changes. Look at the failure rate for runs triggered by docs-only commits. It will be near zero. These runs consume minutes without providing signal, since they cannot fail when there is nothing to test.
-
Config and metadata files trigger builds. Changes to .gitignore, LICENSE, .editorconfig, or .github/CODEOWNERS run the full pipeline. None of these files affect test outcomes, but without path filters, GitHub treats them the same as source code changes.
Metrics
How much docs-only runs cost
In a typical active repository, docs-only changes account for 10–20% of all commits, including README updates, changelog entries, comment fixes, and config tweaks. Every one of those commits triggers a full CI run. Here’s a scenario for a team making 40 pushes/day where 20% are docs-only:
Before path filters
At $0.006/min (Linux 2-core) · 22 working days
After path filters skip docs runs
Save $16/mo · $190/year · per workflow
That’s one workflow on Linux. On macOS runners at $0.062/min, the same 8 docs pushes/day waste $163/mo per workflow. If your repo has 3 workflows (CI, lint, deploy preview) all triggering on every push, multiply accordingly. Path filters eliminate this waste entirely because skipped workflows consume zero minutes.
Fix 1
Add path filters to skip non-code changes
GitHub’s paths-ignore filter tells a workflow to skip entirely when the only changed files match the listed patterns. The run does not start, meaning zero minutes consumed and zero cost. This is the simplest and most effective fix for most repositories.
There are two variants: paths-ignore (a deny-list that runs for everything except the listed paths) and paths (an allow-list that only runs when the listed paths change). You cannot use both on the same trigger. For most repositories, paths-ignore is the right choice because it requires less maintenance, since new source files are automatically included.
name: CI on: push: branches: [main] paths-ignore: - 'docs/**' - '**.md' - 'LICENSE' - '.gitignore' - '.editorconfig' - '.github/CODEOWNERS' pull_request: paths-ignore: - 'docs/**' - '**.md' - 'LICENSE' - '.gitignore' - '.editorconfig' - '.github/CODEOWNERS' jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test
The **.md glob matches Markdown files at any depth. The docs/** pattern matches everything under the docs/ directory. When a commit changes only files matching these patterns, the workflow is skipped entirely.
One caveat: if a commit changes both a Markdown file and a source file, the workflow runs normally. Path filters evaluate the full set of changed files, and the workflow is only skipped when every changed file matches the ignore pattern.
Fix 2
Handle required status checks with conditional jobs
There is a well-known problem with paths-ignore and branch protection rules. If your workflow is a required status check, a skipped workflow leaves the check in a permanent “Pending” state. GitHub does not mark skipped workflows as passing, so the PR is blocked from merging on docs-only changes.
The workaround is to move path filtering from the workflow level into the job level using an action like dorny/paths-filter. The workflow always runs (satisfying the required check), but individual jobs are skipped when only docs files changed. The workflow still reports a passing status to branch protection.
name: CI on: push: branches: [main] pull_request: jobs: changes: runs-on: ubuntu-latest outputs: code: ${{ steps.filter.outputs.code }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 id: filter with: filters: | code: - 'src/**' - 'tests/**' - 'package.json' - 'package-lock.json' test: needs: changes if: ${{ needs.changes.outputs.code == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm test
The changes job runs on every push and uses dorny/paths-filter to detect whether code files changed. The test job only runs when the code output is true. When a job is skipped via an if condition, GitHub reports it as “skipped” rather than “pending,” and branch protection treats skipped jobs as passing.
One caveat: the changes job still runs on every push, billing roughly 10–20 seconds to check out the repository and evaluate the filter. At $0.006/min, that’s under $0.002 per run, which is negligible compared to the 15-minute test suite you’re skipping.
Fix 3
Create a docs-only fast lane workflow
Instead of just skipping CI on docs changes, you can run a lightweight docs-specific workflow. This validates documentation quality, such as broken links, spelling, and Markdown formatting, without running the full test suite. Use the paths filter (allow-list) to trigger this workflow only when docs files change.
name: Docs on: push: branches: [main] paths: - 'docs/**' - '**.md' pull_request: paths: - 'docs/**' - '**.md' jobs: lint-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check Markdown formatting uses: DavidAnson/markdownlint-cli2-action@v19 - name: Check for broken links uses: lycheeverse/lychee-action@v2 with: args: 'docs/**/*.md *.md' fail: true
This workflow only triggers when Markdown or docs files change, and it runs in under a minute. Combined with paths-ignore on your main CI workflow, you get full coverage: code changes run the test suite, docs changes run the linter. No commit goes unchecked, but each commit runs only the checks that matter.
One caveat: if a commit changes both docs and code files, both workflows run. This is correct behavior, since you want the code tested and the docs linted. The paths and paths-ignore filters evaluate independently per workflow.
Reference
When to use paths vs. paths-ignore
Both filters achieve the same goal, but they work in opposite directions. Choose based on how your repository is structured:
| paths-ignore | paths | |
|---|---|---|
| Logic | Run unless only these files changed | Run only when these files changed |
| Best for | Standard repos (exclude docs) | Monorepos (per-package CI) |
| Maintenance | Low, new code files auto-included | Higher, must update when adding directories |
| Combined? | Cannot use both on the same trigger | |
| Required checks | Blocks PRs when skipped | Blocks PRs when skipped |
The required checks problem applies to both variants. Any time the workflow is skipped at the on: level (whether by paths or paths-ignore), GitHub leaves the status check in “Pending.” If you use required status checks, use the job-level filtering approach from Fix 2 instead.
Reference
Non-code files to exclude from CI triggers
Here is a starting list of file patterns that rarely affect test outcomes. Adjust for your repository as needed. If your tests depend on a config file, keep it out of the ignore list.
paths-ignore: # Documentation - 'docs/**' - '**.md' - '**.mdx' - '**.txt' # Repository metadata - 'LICENSE' - 'LICENSE.*' - '.gitignore' - '.gitattributes' - '.editorconfig' # GitHub metadata (not workflow files) - '.github/CODEOWNERS' - '.github/ISSUE_TEMPLATE/**' - '.github/PULL_REQUEST_TEMPLATE/**' - '.github/FUNDING.yml' - '.github/SECURITY.md'
Do not add .github/workflows/** to the ignore list. Changes to workflow files should always trigger CI so you can validate the workflow itself. Similarly, avoid ignoring lockfiles (package-lock.json, Gemfile.lock) or build configs (Dockerfile, tsconfig.json), since those directly affect build and test outcomes.
Related guides
Stop CI Running on Every Push
Concurrency groups, scoped triggers, and path filters to cut redundant runs 30-50%.
Monorepo CI: Why Every Change Runs Everything
Path-based job selection and affected-target tooling for monorepo CI.
Fix Duplicate CI Runs from Misconfigured Triggers
Eliminate double runs caused by unscoped push and pull_request triggers.
Optimize Lint and Typecheck ROI
Make lint and typecheck jobs faster and more cost-effective in CI.