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
# 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.
- uses: actions/checkout@v7
with:
fetch-depth: 0 # clones the ENTIRE git history, slow on large reposSeen 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 - uses: actions/checkout@v4
with:
# Force fresh checkout to avoid any caching issues
fetch-depth: 1 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 1Why 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.
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?
Search for the setting:
grep -rn 'fetch-depth' .github/workflows/.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.A missing
fetch-depthis 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.
- Caching & Setup
Cache dependencies in GitHub Actions
Cache your package manager's downloads so CI restores dependencies from a keyed cache instead of reinstalling them from scratch on every run.
- Trigger Scope
Filter workflows by path in GitHub Actions
Add
pathsorpaths-ignoreto expensive workflows so a docs-only or unrelated change doesn't trigger the full test suite.
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)
Last updated 2026-07-16