CI pipelines pull code hundreds of times a day, and every second counts. Shallow clones in CI can slice checkout time in half, reduce bandwidth, and accelerate your builds without sacrificing the information your tools need. In this guide on shallow clones in CI: a Git & GitHub guide for developers, we'll walk through practical techniques to balance speed with reliability in your GitHub Actions workflows.
Why shallow clones matter in CI
When a CI job runs, it often clones your entire repository history. For mature projects with years of commits, that's a lot of data. A typical repository might have 50,000+ commits, each carrying metadata, diffs, and objects. A full clone can take 30–60 seconds; a shallow clone can finish in 3–5 seconds.
The tradeoff is simple: you trade historical context for speed. Most CI jobs don't need the full history. They need the current commit, maybe a few recent commits for changelog generation or version detection, but rarely the entire log back to the repository's inception.
Shallow clones are especially valuable in high-volume CI environments: pull request checks, scheduled builds, multi-repo pipelines, and monorepo setups where teams run hundreds of builds per day across many services.
Using fetch-depth: 1 in actions/checkout
GitHub Actions' actions/checkout action supports shallow cloning through the fetch-depth parameter. Setting fetch-depth: 1 tells Git to fetch only the most recent commit, not the entire history.
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1This is the simplest optimization available. In practice, we've seen this reduce checkout time from 40 seconds to under 5 seconds for repositories with 100k+ commits.
However, fetch-depth: 1 is aggressive. Some tools and workflows depend on seeing a bit of history:
- Commit message inspection: Tools that read commit messages to auto-generate changelogs or detect breaking changes need at least 2–10 commits.
- Git diff operations: If you need to compare against the previous commit or merge base, you'll want more than just the HEAD.
- Version detection: Some build systems use git tags and history to calculate semantic versions.
A middle ground is fetch-depth: 10 or fetch-depth: 20. This keeps the checkout fast while giving most tools enough context:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 20For most CI jobs—unit tests, linting, Docker image builds—fetch-depth: 1 is safe and recommended. Reserve higher depths for jobs that actually need history.
Fetching more history when tools need it
Sometimes you start with a shallow clone but then discover mid-build that you need more history. Rather than re-cloning the entire repository, you can fetch additional commits incrementally.
This is useful when:
- Your linter or security tool needs to compare against a base branch.
- A changelog generator requires commit history.
- A deploy script needs to know which commits are new since the last release.
Use git fetch --deepen to add more commits to your shallow clone:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Deepen clone for changelog
run: git fetch --deepen=50This starts with just the HEAD commit, then fetches 50 more commits on top. The --deepen parameter adds to the shallow boundary rather than replacing it, so you're only downloading what you need.
In pull request workflows, you often need the merge base—the common ancestor of your PR branch and the target branch. Use fetch-depth: 0 only for that job, or fetch the target branch explicitly:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Fetch base branch
run: git fetch origin main --depth=10This keeps your working copy shallow (just one commit) but ensures you can compute diffs against main.
Tags may need explicit fetching
Shallow clones don't automatically fetch Git tags. If your build depends on tags—for versioning, release detection, or semantic versioning—you'll see surprising behavior:
$ git describe --tags
fatal: No names found, cannot describe anything.Even though tags exist in your repository, they're not in the shallow clone. The solution is to fetch tags explicitly:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Fetch tags
run: git fetch origin --tagsOr, in actions/checkout, pass the fetch-tags parameter:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
fetch-tags: trueNote that fetching all tags can still be expensive if your repository has thousands of tags (common in multi-service setups). If you only need recent tags, fetch them selectively:
- name: Fetch recent tags
run: git fetch origin --tags --depth=50Use partial clone for big repos
Shallow clones fetch full commit histories but limit the depth. For truly massive repositories—monorepos with millions of files or long histories spanning decades—you can go further with partial clone (introduced in Git 2.20).
Partial clone downloads only the files you need, not the entire object database. It defers downloading blob data until you access it:
git clone --filter=blob:none --sparse origin/repoGitHub Actions supports this through the actions/checkout action:
- name: Checkout with sparse clone
uses: actions/checkout@v4
with:
fetch-depth: 1
filter: blob:none
sparse-checkout: |
src/
package.json
DockerfileThis checks out only the specified directories or files. The remaining code is never downloaded. For a monorepo where you're building a single service, this can reduce clone time from 2 minutes to 20 seconds.
The tradeoff: if your build later tries to access a file outside the sparse-checkout definition, Git will fetch it on demand (which might be slow or might fail if the CI environment has restricted network access). Use sparse clone only when you control all build steps and are certain they won't access other parts of the repo.
Sparse clone pairs well with shallow clone:
- name: Partial sparse clone
uses: actions/checkout@v4
with:
fetch-depth: 10
filter: blob:none
sparse-checkout: |
services/api/
libs/shared/Common mistakes
Mistake 1: Using fetch-depth: 1 everywhere, then debugging with tools that need history. Tools like git log --oneline -20, git describe, or semantic versioning libraries will fail silently or produce wrong results. Test your toolchain against shallow clones before rolling out the optimization.
Mistake 2: Forgetting to fetch tags. CI builds that depend on git describe --tags or version detection will mysteriously fail in shallow clones. Always explicitly fetch tags if your build uses them.
Mistake 3: Over-aggressive sparse checkouts. Sparse checkouts are powerful but fragile. If a dependency or build step is accidentally excluded, the build breaks in a way that's hard to debug. Document your sparse-checkout patterns and test them thoroughly.
Mistake 4: Not measuring the impact. Optimization without measurement is guesswork. Compare checkout times before and after. For low-traffic CI, shallow clones might not save enough time to justify the complexity.
Key takeaways
- Start with
fetch-depth: 1for most CI jobs; it's the quickest win and works for tests, linting, and builds. - Use
fetch-depth: 10orfetch-depth: 20if your tools need a bit of history but not the full repo. - Fetch tags explicitly with
fetch-tags: trueif your build depends ongit describeor semantic versioning. - For pull requests, fetch the base branch separately rather than using a large fetch-depth for the entire clone.
- Use
git fetch --deepento add history incrementally instead of re-cloning. - Partial clone with
--filter=blob:noneandsparse-checkoutis powerful for monorepos but adds complexity; test thoroughly. - Measure checkout times before and after optimization to confirm the benefit justifies the added configuration.






Comments
Loading comments…