How to fix fatal: Submodule update failed in CI - shallow clone doesn't support submodules in Git

GitINTERMEDIATEMEDIUMUpdated Dec 2, 2025ยท 211 views

This error occurs in CI/CD pipelines when Git attempts to update submodules in a shallow clone. Shallow clones (using --depth) lack the full commit history that submodules need to resolve their target commits, causing the update to fail.

Quick fix

Fixes fatal: Submodule update failed in CI - shallow clone doesn't support submodules

- uses: actions/checkout@v4
  with:
    fetch-depth: 0  # Fetch all history for all branches and tags
    submodules: recursive  # Checkout submodules
  1. Disable shallow clone in GitHub Actions
  2. Configure GitLab CI for submodules
  3. Fix shallow clone in Jenkins
  4. Configure CircleCI for submodules
git โ€” error
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all branches and tags
submodules: recursive # Checkout submodules
fatal: Submodule update failed in CI - shallow clone doesn't support submodules

What this error means

When you perform a shallow clone with `git clone --depth 1`, Git only fetches a limited number of commits from the repository's history. This optimization significantly reduces clone time and disk usage, which is why many CI/CD systems use it by default. However, Git submodules reference specific commits in their parent repository. When Git tries to update a submodule, it needs to: 1. Find the commit SHA that the submodule should be checked out to 2. Locate that commit in the submodule's history 3. Check out the submodule at that specific commit With a shallow clone, the parent repository may not have enough history to determine which commit the submodule should point to. Additionally, fetching the submodule itself may fail if the referenced commit is not within the shallow depth. This issue is particularly common in CI environments like GitHub Actions, GitLab CI, Jenkins, and CircleCI, where shallow clones are the default behavior for performance reasons.

Typical symptoms

  • CI pipeline fails at the checkout or submodule update step
  • Error message mentions 'shallow clone doesn't support submodules'
  • Submodule directories remain empty after checkout
  • Build works locally but fails in CI environment
  • Error occurs after adding submodules to an existing project
  • Pipeline worked before but fails after switching CI providers or updating checkout action

Common causes

  1. CI/CD system uses shallow clone by default (--depth 1)
  2. GitHub Actions checkout action has fetch-depth: 1 by default
  3. GitLab CI uses shallow clones by default in recent versions
  4. Jenkins Git plugin configured with shallow clone option
  5. The submodule points to a commit that is not in the shallow history
  6. Submodules were added recently and CI configuration was not updated
  7. CI cache or workspace contains stale shallow clone data

How to fix "fatal: Submodule update failed in CI - shallow clone doesn't support submodules"

1Disable shallow clone in GitHub Actions

Update your GitHub Actions workflow to fetch the full repository history:

yaml
- uses: actions/checkout@v4
  with:
    fetch-depth: 0  # Fetch all history for all branches and tags
    submodules: recursive  # Checkout submodules

If you only need submodules without full history, you can try a compromise:

yaml
- uses: actions/checkout@v4
  with:
    fetch-depth: 2  # Minimal history, may work for some submodule setups
    submodules: recursive

For the most reliable results, use fetch-depth: 0.

2Configure GitLab CI for submodules

In GitLab CI, set the submodule strategy and disable shallow cloning:

yaml
variables:
  GIT_SUBMODULE_STRATEGY: recursive
  GIT_DEPTH: 0  # Disable shallow clone (0 = full clone)

# Or use GIT_SUBMODULE_DEPTH for submodule-specific depth
variables:
  GIT_SUBMODULE_STRATEGY: recursive
  GIT_SUBMODULE_DEPTH: 0

If you're using GitLab Runner 13.0+, you may also need:

yaml
variables:
  GIT_SUBMODULE_FORCE_HTTPS: "true"  # If using SSH URLs in .gitmodules
3Fix shallow clone in Jenkins

In Jenkins, modify your pipeline or job configuration:

Declarative Pipeline:

groovy
pipeline {
    agent any
    stages {
        stage('Checkout') {
            steps {
                checkout([
                    $class: 'GitSCM',
                    branches: [[name: '*/main']],
                    extensions: [
                        [$class: 'SubmoduleOption',
                         disableSubmodules: false,
                         recursiveSubmodules: true,
                         trackingSubmodules: false],
                        [$class: 'CloneOption',
                         depth: 0,  // Full clone
                         noTags: false,
                         shallow: false]
                    ],
                    userRemoteConfigs: [[url: 'https://github.com/your/repo.git']]
                ])
            }
        }
    }
}

Job Configuration:
In the Git plugin settings, uncheck "Shallow clone" and enable "Recursively update submodules".

4Configure CircleCI for submodules

CircleCI uses shallow clones by default. Override this in your config:

yaml
version: 2.1

jobs:
  build:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Initialize submodules with full history
          command: |
            git fetch --unshallow || true
            git submodule sync --recursive
            git submodule update --init --recursive

The git fetch --unshallow command converts a shallow clone into a full clone. The || true prevents failure if the clone is already complete.

5Manually unshallow and update submodules

If you can't modify the CI configuration, add a script step after checkout:

bash
#!/bin/bash
set -e

# Check if we're in a shallow clone
if [ -f "$(git rev-parse --git-dir)/shallow" ]; then
    echo "Unshallowing repository..."
    git fetch --unshallow
fi

# Sync submodule URLs in case they changed
git submodule sync --recursive

# Initialize and update all submodules
git submodule update --init --recursive

echo "Submodules updated successfully"

Save this as scripts/init-submodules.sh and call it in your CI pipeline.

6Use SSH deploy keys for private submodules

If submodules are private repositories, ensure proper authentication:

GitHub Actions:

yaml
- uses: actions/checkout@v4
  with:
    fetch-depth: 0
    submodules: recursive
    ssh-key: ${{ secrets.SUBMODULE_DEPLOY_KEY }}

GitLab CI:

yaml
before_script:
  - eval $(ssh-agent -s)
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  - mkdir -p ~/.ssh
  - chmod 700 ~/.ssh
  - ssh-keyscan github.com >> ~/.ssh/known_hosts

Generate a deploy key with read access to all submodule repositories and add it to your CI secrets.

7Convert submodule URLs for CI compatibility

Sometimes CI systems can't authenticate SSH URLs. Convert to HTTPS in CI:

bash
# In your CI before_script or setup step
git config --global url."https://github.com/".insteadOf "git@github.com:"
git config --global url."https://gitlab.com/".insteadOf "git@gitlab.com:"

# Then update submodules
git submodule sync --recursive
git submodule update --init --recursive

For GitLab CI, you can use the built-in variable:

yaml
variables:
  GIT_SUBMODULE_FORCE_HTTPS: "true"

Advanced notes

Understanding the shallow clone limitation:

A shallow clone is like reading only the last page of a book. Git submodules, however, might reference commits from earlier in history. When Git tries to check out the submodule at a specific commit, that commit simply doesn't exist in the truncated history.

Performance considerations:

Fetching full history (fetch-depth: 0) increases CI time, especially for large repositories. Consider these optimizations:

yaml
# GitHub Actions - fetch only tags and the current branch
- uses: actions/checkout@v4
  with:
    fetch-depth: 0
    fetch-tags: true
    submodules: recursive

# Or use a blobless clone (faster for repos with large files)
- run: |
    git clone --filter=blob:none --recurse-submodules https://github.com/org/repo.git

Submodule depth vs repository depth:

In GitLab CI, you can set different depths:

yaml
variables:
  GIT_DEPTH: 1                # Shallow clone the main repo
  GIT_SUBMODULE_DEPTH: 0      # Full clone for submodules only
  GIT_SUBMODULE_STRATEGY: recursive

Caching considerations:

If your CI caches the Git directory between runs, stale shallow clone data can cause issues:

yaml
# Clear submodule cache before updating
git submodule deinit --all -f
git submodule update --init --recursive

Alternative: vendoring submodule content:

For very slow CI pipelines, consider vendoring (copying) submodule content directly into your repository:

bash
# One-time: copy submodule content instead of using submodules
git submodule update --init
cp -r submodule-dir/ vendor/submodule-dir/
git submodule deinit submodule-dir
git rm submodule-dir
git add vendor/submodule-dir/
git commit -m "Vendor submodule content for CI performance"

This eliminates submodule fetch issues but requires manual updates when the upstream changes.

Frequently asked questions

What does "fatal: Submodule update failed in CI - shallow clone doesn't support s" mean?
When you perform a shallow clone with git clone --depth 1, Git only fetches a limited number of commits from the repository's history. This optimization significantly reduces clone time and disk usage, which is why many CI/CD systems use it by default. However, Git submodules reference specific commits in their parent repository.
How do I fix "fatal: Submodule update failed in CI - shallow clone doesn't support s"?
This error occurs in CI/CD pipelines when Git attempts to update submodules in a shallow clone. Shallow clones (using --depth) lack the full commit history that submodules need to resolve their target commits, causing the update to fail.
What causes "fatal: Submodule update failed in CI - shallow clone doesn't support s"?
CI/CD system uses shallow clone by default (--depth 1) GitHub Actions checkout action has fetch-depth: 1 by default GitLab CI uses shallow clones by default in recent versions
Is the "fatal: Submodule update failed in CI - shallow clone doesn't support s" error serious?
It is a medium-severity error โ€” more of a blocker than a risk. The steps above resolve it.

Official resources & further reading