In the previous post, we set up Git hooks to enforce code quality automatically. Now letâs tackle another Git power feature: submodules.
When you have Terraform modules, Ansible roles, or shared libraries used across multiple repositories, copy-paste becomes a maintenance nightmare. Submodules let you embed one repository inside another while keeping them independently versioned.
What Are Submodules?
A submodule embeds another Git repo at a specific commit. Use them for:
- Shared infrastructure modules consumed by multiple repos
- Vendor code you canât or donât want to fork
- Documentation that must match code versions
The Catch
Submodules point to a commit, not a branch. You must explicitly update the pointer. This trips up every team at least once.
When you clone a repo with submodules, the submodule directories are empty by default. When you update a submodule, youâre checking out a specific commit - not pulling the latest. These behaviors are intentional but surprising if youâre not expecting them.
Basic Workflow
Add a submodule
git submodule add https://github.com/example/shared-terraform-modules.git modules/shared
git commit -m "chore(submodule): add shared terraform modules"
This creates a .gitmodules file tracking the submodule URL and path.
Clone with submodules
git clone --recurse-submodules [email protected]:yourorg/app.git
Or if you already cloned:
git submodule update --init --recursive
Update to a newer upstream commit
cd modules/shared
git fetch origin
git checkout v1.4.2 # pin to a tag for predictability
cd ../..
git add modules/shared
git commit -m "chore(submodule): bump shared modules to v1.4.2"
The parent repo tracks the submoduleâs commit SHA, not a branch name. Pinning to tags makes it clear what version youâre using.
CI Configuration
Your CI must initialize submodules explicitly:
# GitHub Actions
- uses: actions/checkout@v6
with:
submodules: recursive
Without this, your CI will see empty submodule directories and fail in confusing ways.
Hands-On Lab: Add a Submodule
Building on the repo from the hooks article:
cd git-hooks-lab
# Add a submodule
git submodule add https://github.com/example/shared-terraform-modules.git modules/shared
git commit -m "chore(submodule): add shared modules"
Verify it worked:
cat .gitmodules
git submodule status
Common Pitfalls
âModified contentâ warnings
If you see modified content next to a submodule in git status, someone checked out a different
commit inside the submodule without staging it:
git add modules/shared
git commit -m "chore(submodule): update to latest commit"
Detached HEAD in submodule
This is normal. Submodules check out commits, not branches. If you need to make changes inside the submodule, create a branch first:
cd modules/shared
git checkout -b my-feature
# make changes, commit, push
cd ../..
git add modules/shared
git commit -m "chore(submodule): point to my-feature branch"
Teammates see empty directories
They need to initialize submodules after cloning:
git submodule update --init --recursive
Or remind them to clone with --recurse-submodules.
Troubleshooting Guide
| Problem | Cause | Fix |
|---|---|---|
| Submodule shows âmodified contentâ | Checked out new commit without staging | git add modules/shared |
| Empty submodule directory | Not initialized | git submodule update --init --recursive |
| âDetached HEADâ in submodule | Normal when pinned to commit | Use tags for readability |
| CI fails with missing files | Submodules not checked out | Add submodules: recursive to checkout action |
Quick Reference
git submodule add <url> <path> # Add submodule
git submodule update --init --recursive # Initialize after clone
git clone --recurse-submodules <url> # Clone with submodules
git submodule status # Show current commit for each submodule
Whatâs Next
Next post: Managing Large Git Repositories. Weâll cover Git LFS for large binaries, partial clones to skip what you donât need, and sparse checkouts for monorepo workflows.
Happy automating!