How actions/checkout Handles Git Submodules: Implementation and Configuration

The actions/checkout action handles Git submodules through three configurable modes that execute native Git commands to synchronize and update submodule references after cloning the parent repository.

The actions/checkout action is the standard method for checking out code in GitHub Actions workflows. When your repository contains Git submodules, understanding exactly how actions/checkout handles submodules ensures your CI/CD pipelines correctly fetch all required dependencies.

Parsing the Submodule Input

The process begins in src/input-helper.ts, where the action parses the submodules input and normalizes it into two distinct settings: a boolean submodules flag and a nestedSubmodules recursion flag.

// src/input-helper.ts (lines 127-138)
const submodulesString = (core.getInput('submodules') || '').toUpperCase()
if (submodulesString == 'RECURSIVE') {
  result.submodules = true
} else if (submodulesString == 'TRUE') {
  result.submodules = true
}
core.debug(`submodules = ${result.submodules}`)
core.debug(`recursive submodules = ${result.nestedSubmodules}`)

This logic supports three distinct behaviors:

  • false (default): Submodules are ignored entirely
  • true: Top-level submodules are fetched non-recursively
  • recursive: All submodules including nested ones are fetched recursively

The Submodule Checkout Flow

After the main repository fetch completes, src/git-source-provider.ts orchestrates the submodule operations. The action only proceeds with submodule handling when settings.submodules evaluates to true.

// src/git-source-provider.ts (lines 74-97)
if (settings.submodules) {
  core.startGroup('Setting up auth for fetching submodules')
  await authHelper.configureGlobalAuth()
  core.endGroup()

  core.startGroup('Fetching submodules')
  await git.submoduleSync(settings.nestedSubmodules)
  await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
  await git.submoduleForeach(
    'git config --local gc.auto 0',
    settings.nestedSubmodules
  )
  core.endGroup()
}

The flow executes three critical Git operations in sequence:

  1. submoduleSync: Aligns submodule URLs with the parent repository configuration using git submodule sync
  2. submoduleUpdate: Checks out the specific commits referenced by the parent repository using git submodule update --init --force
  3. submoduleForeach: Disables automatic garbage collection within each submodule by running git config --local gc.auto 0

Git Command Implementation

The actual Git operations are delegated to GitCommandManager in src/git-command-manager.ts. This wrapper abstracts the native Git CLI calls into typed methods that respect the fetch-depth and recursive parameters:

  • submoduleSync(recursive: boolean): Executes git submodule sync to update remote URLs
  • submoduleUpdate(fetchDepth: number, recursive: boolean): Runs git submodule update --init --force with optional --depth and --recursive flags
  • submoduleForeach(command: string, recursive: boolean): Iterates through submodules executing the provided command

These methods ensure that shallow fetching and recursive traversal are handled according to the workflow inputs.

Authentication and Credential Persistence

Submodules often require the same authentication as the parent repository. When persist-credentials is set to true, the action propagates authentication configuration to each submodule after fetching:

// src/git-source-provider.ts (lines 92-96)
if (settings.persistCredentials) {
  core.startGroup('Persisting credentials for submodules')
  await authHelper.configureSubmoduleAuth()
  core.endGroup()
}

This call to configureSubmoduleAuth() in src/git-auth-helper.ts ensures that subsequent Git operations within submodules—such as pushing changes back to remote servers—can authenticate using the same token or SSH key configured for the primary repository.

Edge Cases and Limitations

The implementation includes specific safeguards for edge cases:

  • REST API Fallback: When Git is not available on the runner (forcing a fallback to the REST API), submodule support is explicitly disabled and the action throws an error rather than attempting partial submodule handling
  • Worktree Cleaning: The submoduleStatus helper in src/git-directory-helper.ts detects existing submodule states to determine whether the worktree requires cleaning before checkout, preventing conflicts with orphaned submodule directories

Configuration Examples

Configure submodule handling in your workflow using the submodules input:


# Fetch only top-level submodules

- uses: actions/checkout@v7
  with:
    submodules: true

# Recursive submodule checkout with full history

- uses: actions/checkout@v7
  with:
    submodules: recursive
    fetch-depth: 0

# Disable submodules (default behavior)

- uses: actions/checkout@v7

# Persist credentials for submodule operations

- uses: actions/checkout@v7
  with:
    submodules: recursive
    persist-credentials: true

Summary

  • actions/checkout supports three submodule modes: false (default), true (top-level only), and recursive (nested submodules)
  • Implementation spans four key files: input-helper.ts for parsing, git-source-provider.ts for orchestration, git-command-manager.ts for Git CLI execution, and git-auth-helper.ts for credential propagation
  • Execution sequence: Sync URLs, update commits, disable GC—executed only after the parent repository fetch completes
  • Authentication automatically propagates to submodules when persist-credentials is enabled, ensuring private submodule repositories remain accessible
  • Shallow fetching via fetch-depth applies to submodules when specified, optimizing clone performance for large repositories

Frequently Asked Questions

What happens if I don't specify the submodules input?

By default, actions/checkout ignores submodules entirely. The submodules input defaults to false, meaning the action clones only the parent repository without fetching any submodule content. This minimizes clone time and network usage for workflows that do not require external dependencies.

Does actions/checkout support recursive submodules?

Yes. Set submodules: recursive to fetch all submodules including nested ones. According to the source code in src/git-command-manager.ts, this passes the --recursive flag to git submodule update, ensuring that submodules within submodules are also initialized and updated.

How does authentication work for private submodule repositories?

The action reuses the same authentication mechanism used for the parent repository. When persist-credentials is true, src/git-auth-helper.ts configures authentication for each submodule via configureSubmoduleAuth(). This injects the same GitHub token or SSH key into each submodule's Git configuration, allowing access to private repositories without additional secrets.

Can I use a shallow fetch with submodules?

Yes. When you specify fetch-depth (for example, fetch-depth: 1), the action passes this depth parameter to submoduleUpdate() in src/git-command-manager.ts. However, shallow submodules may cause issues if subsequent build steps require full git history within those submodules. Use fetch-depth: 0 for full history when needed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →