BEGINNERGITHUB-ACTIONSOPTIMIZATION

Skip CI When Only Documentation Changed

Use a paths filter or dorny/paths-filter to skip GitHub Actions CI runs when a PR only touches Markdown or docs files.

By Tested on GitHub Actions ubuntu-latest, dorny/paths-filter@v3

A GitHub Actions pattern that skips a CI workflow entirely when a push only touches documentation files. The simplest version uses GitHub's built-in paths-ignore filter at the workflow level. The more flexible version uses dorny/paths-filter to gate individual jobs. Both can save serious CI minutes on docs-heavy projects.

Tested on GitHub Actions ubuntu-latest, dorny/paths-filter@v3.

When to Use This

  • Docs-heavy projects where Markdown and code mix in the same repo
  • Projects with frequent README and CHANGELOG updates
  • Monorepos where docs changes shouldn't rebuild the whole world
  • Any repo where CI minutes are a real cost

Don't use this when your docs are part of the build (e.g., a docs site that gets built and deployed) — those need CI too. And don't skip security scans even for docs PRs.

Code

The simple version uses paths-ignore at the workflow level:

# .github/workflows/ci.yml
name: CI
 
on:
  push:
    branches: [main]
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - 'LICENSE'
      - '.gitignore'
  pull_request:
    branches: [main]
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - 'LICENSE'
      - '.gitignore'
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # ... real CI work ...

If a push or PR only touches Markdown files, the workflow doesn't even start.

The more flexible version uses dorny/paths-filter inside a job:

name: CI
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
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/**'
              - 'package.json'
              - 'package-lock.json'
              - 'tsconfig.json'
 
  test:
    needs: changes
    if: needs.changes.outputs.code == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

The changes job is fast (10-15 seconds — just a checkout and a diff). The test job only runs if real source code changed.

Usage

A real production setup with multiple gated jobs:

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      code: ${{ steps.filter.outputs.code }}
      docs: ${{ steps.filter.outputs.docs }}
      ci: ${{ steps.filter.outputs.ci }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            code:
              - 'src/**'
              - 'package.json'
              - 'package-lock.json'
            docs:
              - 'docs/**'
              - '**.md'
            ci:
              - '.github/**'
 
  test:
    needs: changes
    if: needs.changes.outputs.code == 'true' || needs.changes.outputs.ci == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: npm ci && npm test
 
  link-check:
    needs: changes
    if: needs.changes.outputs.docs == 'true'
    runs-on: ubuntu-latest
    steps:
      - run: npm run check-links
 
  workflow-lint:
    needs: changes
    if: needs.changes.outputs.ci == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: rhysd/actionlint@v1

Tests only run on code or CI changes. Link checking only runs on docs changes. Workflow linting only runs on CI changes. Each PR runs only what's relevant.

Pitfalls

  • paths-ignore and paths are mutually exclusive at the workflow level. Pick one. Use paths-ignore for "skip these", paths for "only these".
  • paths-ignore only filters by changed files, not by file content. A whitespace-only edit to a .ts file still triggers CI. That's correct behavior.
  • Required status checks break with skipped workflows. If test is a required check and the workflow is skipped, GitHub thinks the check is missing, not passing. Use a separate "always run" workflow that returns success for skipped cases, or mark the check as "skip-when-not-required".
  • dorny/paths-filter needs the right base for PRs. It defaults to comparing against the PR target branch, which is usually what you want.
  • PR checks count even when skipped. GitHub still counts the workflow as having run, just with no jobs. That's fine.
  • Don't use this for security workflows. Code scanning, dependency audits, and license checks should run on every PR regardless of which files changed.

Frequently Asked Questions

Why skip CI for docs changes?

CI minutes are billable on private repos and slow on every plan. A README typo doesn't need a 5-minute build, lint, and test cycle. Skipping docs-only changes can save hours of CI minutes per month on docs-heavy projects without weakening code quality gates.

What's the difference between paths and dorny/paths-filter?

The on.push.paths filter is built into GitHub Actions and skips the entire workflow if no matching files changed. dorny/paths-filter runs inside a job and lets you conditionally skip individual steps based on which files changed. Use paths for whole workflows, dorny for finer control.

X (Twitter)LinkedIn