Best practice · Caching & Setup

Use a shallow checkout with fetch-depth in GitHub Actions

Let `actions/checkout` fetch only the commit under test (its default `fetch-depth: 1`) instead of the whole git history, and reach for `fetch-depth: 0` only when a job genuinely needs history.

AI agents open the PR

StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.

Do this

actions/checkout runs at its default depth of 1, a single commit, for the vast majority of jobs. When a job needs the PR diff, fetch-depth: 2 is enough. Full history (fetch-depth: 0) is reserved for the specific jobs that actually need it: release-notes generation, git blame, or tools that walk tags. On a large monorepo where checkout itself is the bottleneck, depth is the first lever but not the last: a cone-mode sparse-checkout limits the working tree to the directories a job actually builds, and filter: blob:none makes a blobless clone that downloads a file's contents only when something reads it.123

Fetch only what the job needs: depth, tags, paths
# Most jobs: the default fetches only the commit under test
- uses: actions/checkout@v7

# Need the PR diff? Depth 2 is enough, not the whole history
- uses: actions/checkout@v7
  with:
    fetch-depth: 2

# Need the tags themselves (release naming, an exact-match tag lookup)? Fetch
# the tags, not the history. fetch-tags defaults to false, which is why tags go
# missing on a shallow clone. Note git describe is different: it walks ancestry
# to find the nearest reachable tag, so it still needs fetch-depth: 0.
- uses: actions/checkout@v7
  with:
    fetch-tags: true

# Huge monorepo and a job only touches a few directories? Narrow the working
# tree, not just the history. Cone-mode sparse-checkout materializes the paths
# you list (plus top-level files) and skips everything else, so a job that
# builds one app never pulls the other forty-nine packages.
- uses: actions/checkout@v7
  with:
    sparse-checkout: |
      apps/web
      packages/ui

# Want a blobless clone of the full tree (fetch commits and trees, download each
# file's contents only when something reads it)? Set filter: blob:none. It
# composes with sparse-checkout rather than disabling it: a sparse-checkout is
# already blobless under the hood, and an explicit filter only replaces the
# filter it applies for you, so you can set both.
- uses: actions/checkout@v7
  with:
    filter: 'blob:none'

Avoid this

Every run downloads the entire history, even though the job only needs the latest commit.

Full history on every run
- uses: actions/checkout@v7
  with:
    fetch-depth: 0   # clones the ENTIRE git history, slow on large repos

Seen in the wild

Claude Code, browser-use, and Sentry limit checkout history with fetch-depth so CI does not clone more Git data than the job needs.

      - name: Checkout repository
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4
        with:
          fetch-depth: 1
Asks for depth 1 explicitly rather than leaning on the default, and pins the checkout action to a commit while it is there.
      - uses: actions/checkout@v4
        with:
          # Force fresh checkout to avoid any caching issues
          fetch-depth: 1
The job that only needs to list the test files takes depth 1 explicitly, rather than relying on the default staying that way.
      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
        with:
          fetch-depth: 1
Sentry's change-detection job clones only the tip -- a shallow checkout is all its path filter needs to tell which files a pull request touched.

Why it matters

On a repo with a long history, fetch-depth: 0 downloads far more than the job needs, and it pays that cost on every run. The default shallow fetch is often the single easiest checkout win. The fix is safe as long as you keep full history on the few jobs that require it. Past depth, the next levers narrow *what* you download rather than *how far back*, and their payoff scales with how much of the repo a job never touches. Size them honestly: a blobless clone (filter: blob:none) can be a wash when the job ultimately reads every file, because git then re-fetches those blobs on demand, so it pays off most paired with a sparse checkout where the excluded files are never fetched at all.

When to use

Use it when

Almost every job. Drop an explicit fetch-depth: 0 unless you can name why the job needs history.

Be careful when

Keep full history when a job runs changelog/release tooling, git blame, git describe, or anything that walks the full commit graph. For those, fetch-depth: 0 is correct - git describe in particular names the nearest tag *reachable* from HEAD, and a shallow clone has no path back to it. A job that only needs the tags to exist (release naming, an exact-match lookup) does not need history at all: actions/checkout@v7 takes a fetch-tags: true input, which fetches tags at the shallow depth you already have.

Verify on your repo

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.

Prompt for your coding agent
Inspect this repo's .github/workflows for `fetch-depth` on `actions/checkout`. Find every step that sets `fetch-depth: 0` (full history) and, for each, determine whether the job actually needs history: changelog or release tooling, `git blame`, `git describe`, or anything that walks tags. If nothing in the job needs history, remove the `fetch-depth: 0` (back to the default shallow depth of 1), or drop it to `fetch-depth: 2` when only a parent diff is needed. Leave jobs that genuinely need full history untouched. Show me the diff and open a PR rather than applying it blindly.

Prefer to check by hand?

  1. Search for the setting: grep -rn 'fetch-depth' .github/workflows/.

  2. For each fetch-depth: 0, ask what in the job needs history. If nothing does, remove it (back to the default 1) or drop to 2 for diff detection.

  3. A missing fetch-depth is fine, the default is already 1.

Automate this with StarSling.

Install the StarSling GitHub App. It moves supported jobs onto StarSling runners, then AI agents keep inspecting your workflows and opening optimization PRs you review.

More best practices for GitHub Actions

Where to go next in the CI best-practices catalog.

All CI best practices

FAQ

fetch-depth 0 vs 1: what's the difference?

fetch-depth: 1, the GitHub Actions default, clones only the single commit being tested, while fetch-depth: 0 clones the complete history and every tag, which you need only for tooling that walks the full graph. 2 sits between them: it adds the parent commit, enough to diff the merge commit's two parents (the default checkout) or a single-commit PR, though a multi-commit PR checked out at its head ref needs the merge base instead.

My diff-based tool broke after I removed fetch-depth: 0. Why?

It needs more than one commit to compare against. Use fetch-depth: 2 for a simple parent diff, or fetch the specific base ref your tool diffs against, rather than the whole history.

Do I lose my tags if I drop fetch-depth: 0?

Yes, by default. actions/checkout fetches no tags on a shallow clone: its fetch-tags input defaults to false, and fetch-depth: 0 is what pulls in all history for all branches and tags. If a job only needs the tags to be present (release naming, an exact-match lookup), set fetch-tags: true and keep the shallow depth rather than cloning the entire history to get them. git describe is the exception: it reports the distance to the nearest tag reachable from HEAD, so it needs the history too.

How do I speed up checkout beyond fetch-depth on a huge repo?

Narrow what you clone, not just how far back. actions/checkout takes a sparse-checkout: input (cone mode by default) that limits the working tree to the directories you list plus top-level files, so a job that builds one app in a fifty-package monorepo never materializes the other forty-nine. For an even lighter clone, filter: blob:none makes a blobless partial clone that downloads a file's contents only when git needs them. These narrow the files, where fetch-depth narrows the history, and they stack with a shallow depth.

Can I use sparse-checkout and filter: blob:none together?

Yes. actions/checkout's docs say filter "overrides sparse-checkout," but that wording is misleading: an explicit filter only replaces the filter that sparse-checkout applies for you, not the path-narrowing itself, so the two compose. In fact a sparse-checkout is already blobless - the action sets filter: blob:none under the hood - so sparse-checkout alone gives you a blobless clone limited to the directories you list, which is the biggest win on a wide monorepo. Set an explicit filter when you want a different partial-clone filter (say tree:0) while keeping the sparse paths, or filter: blob:none on its own when a job needs the whole tree but you want to defer downloading file contents.

Does a blobless (filter: blob:none) clone always make CI faster?

No. A blobless clone skips file contents up front and fetches each blob on demand the first time git reads it. A build that ends up reading most of the tree re-downloads those blobs anyway, in extra round trips, so the clone can come out a wash or slightly slower. It pays off when the job touches only part of the tree, which is why it shines paired with a sparse checkout, and for history-only work where you never check out files at all.

Sources

1actions/checkout · fetch-depth (opens in new tab)

2Git · partial clone (opens in new tab)

3Git · git-sparse-checkout (opens in new tab)

Last updated 2026-07-16