Scaffold a repo that pulls in your chosen Git submodules and keeps them up to date automatically.
When you manage a project that depends on a collection of independent Git repositories, keeping everything wired together and up to date can become tedious:
- You don't want to manually
git submodule addevery new dependency. - You want a single, uniform place to list your submodules.
- You want daily automation to pull upstream changes and commit them back.
This template gives you all three out of the box:
- One-file list (
submodules.txt) — edit this list to add/remove dependencies. - Bootstrap script (
scripts/init-submodules.sh) — a single command to populatemodules/. - Built-in GitHub Action (
.github/workflows/update-submodules.yml) — runs on a cron schedule to update pointers automatically. - Optional: a
cookiecutter-template/folder for interactive Cookiecutter users.
/
├── .github/
│ └── workflows/
│ └── update-submodules.yml
├── scripts/
│ └── init-submodules.sh
├── submodules.txt.example
├── README.md ← you are here
├── LICENSE
├── modules/ ← (empty — populated by init-submodules.sh)
└── cookiecutter-template/ ← optional interactive template
├── cookiecutter.json
└── {{cookiecutter.project_slug}}/
├── .github/
│ └── workflows/update-submodules.yml
├── scripts/init-submodules.sh
├── submodules.txt.example
└── README.md
This effectively only applies if you intend to use private repositories as your submodules. If that's not the case, you can skip this part.
You can use the default ${{ secrets.GITHUB_TOKEN }} and configure actions/checkout to pull submodules recursively.
To use private repositories as submodules in GitHub Actions workflows (e.g., for automatically pulling submodule updates), you need to ensure the following:
-
GitHub Pro or Higher Subscription: A GitHub Pro subscription (or higher, like GitHub Team or Enterprise) is required to use private repositories, including private submodules, in GitHub Actions workflows. Without this subscription, private repositories won't be accessible for these purposes.
-
Access to Private Repositories: GitHub Actions can only access private repositories if the necessary permissions are granted. This requires enabling GitHub Actions for a private repository and setting up authentication (like a Personal Access Token or SSH key) for accessing the private submodule.
Steps:
-
Generate a Personal Access Token (PAT) in your GitHub account
- Generate a Personal Access Token (PAT)
- Go to Settings → Developer settings → Personal access tokens (classic) → Generate new token
- Name it, set an expiration if desired, and select the repo scope (or more narrowly, the
repo:status, repo_deployment, public_repo, repo:invite, repo:readsubset) scope so it can check out private repos. - Click Generate token and copy it immediately.
-
In your main repository's settings:
- Secrets and variables → Actions → Add Repository Secrets
- Name:
CUSTOM_PAT - Value: your PAT
- Name:
- Actions → General → Workflow permissions → Configure Workflow Permissions
- Workflow permissions: Read and write
- Allow GitHub Actions to access: Only select repositories - now add each private repository you want to use as a submodule.
- Secrets and variables → Actions → Add Repository Secrets
GITHUB_TOKENis automatically available in public repos; no setup needed.CUSTOM_PAT(or your chosen name) must be added as a repository secret in each repo that uses private submodules.- The Actions workflow uses
${{ secrets.CUSTOM_PAT }}as the checkout token.
-
Use this template
On GitHub click Use this template, name your new repo (e.g.my-modular-project), then clone it:git clone [email protected]:YOUR-ORG/my-modular-project.git cd my-modular-project
-
Configure submodules
cp submodules.txt.example submodules.txt # Edit submodules.txt — one Git URL per line: # https://github.com/foo.git # https://github.com/bar.git
-
Bootstrap modules/
This will add the submodules to the repo, configure the
CUSTOM_PATsecret and grant the workflow its required permissions.chmod +x scripts/init-submodules.sh scripts/init-submodules.sh
After that, the built-in GitHub Action in
.github/workflows/update-submodules.ymlruns on its schedule without further configuration. -
Link to GitHub
if you already have a remote:
git remote add origin [email protected]:YOUR-ORG/my-modular-project.git git push -u origin main
Otherwise, create a new repo and link it:
gh repo create YOUR-ORG/my-modular-project \ --public \ --source=. \ --remote=origin
-
Verify CI
GitHub Actions will pick up.github/workflows/update-submodules.ymland run on the defined cron schedule, updating your submodule pointers automatically.
This workflow lives in .github/workflows/update-submodules.yml:
name: Pull Submodules & Repackage
on:
schedule:
- cron: '58 18 * * *' # adjust to your preferred UTC time
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout repo w/ submodules
uses: actions/checkout@v3
with:
submodules: recursive
token: ${{ secrets.CUSTOM_PAT || secrets.GITHUB_TOKEN }}
- name: Configure Git
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
- name: Pull main repo updates
run: git pull origin $(git branch --show-current)
- name: Update submodules
run: |
git submodule update --init --recursive --depth=1
git submodule foreach --recursive '
git fetch origin \
&& git reset --hard origin/$(git rev-parse --abbrev-ref HEAD) \
|| echo "Failed to reset submodule"'
- name: Restore sparse checkout configuration
run: |
SPARSE_CONFIG=".sparse-checkout-config"
if [[ -f "$SPARSE_CONFIG" ]]; then
echo "Restoring sparse checkout configuration..."
while IFS=':' read -r name paths; do
[[ -z "$name" ]] && continue
submodule_path="modules/$name"
if [[ -d "$submodule_path" ]]; then
echo " → Configuring sparse checkout for $name: $paths"
pushd "$submodule_path" > /dev/null
git sparse-checkout init --cone
IFS=',' read -ra path_array <<< "$paths"
git sparse-checkout set "${path_array[@]}"
popd > /dev/null
fi
done < "$SPARSE_CONFIG"
fi
- name: Commit changes in submodules
# ... (commits any local changes in submodules)
- name: Commit & push submodule pointer updates
# ... (commits updated submodule pointers)Note:
- Uses either
CUSTOM_PAT(for private submodules) or falls back toGITHUB_TOKEN.- To support private submodules, ensure your PAT is configured as a secret as described above.
Read more on actions/checkout here
- Submodule list: edit
submodules.txtand rerunscripts/init-submodules.sh. - Schedule: adjust the
cron:line in.github/workflows/update-submodules.yml. - CI steps: add build/test jobs after the update steps.
Instead of syncing entire repositories, you can choose to keep only specific directories or files from a submodule. This uses Git's sparse checkout feature.
In submodules.txt, append a colon and comma-separated paths to any repo URL:
# Full repo sync (default)
https://github.com/you/foo.git
# Selective sync - only specific directories/files
https://github.com/you/bar.git:src/,docs/,README.md
https://github.com/you/lib.git:lib/utils/,lib/helpers/,types.d.ts
- Directories should end with
/(e.g.,src/,docs/api/) - Files are specified by their path (e.g.,
README.md,src/index.ts) - Multiple paths are separated by commas (no spaces)
- Mode selection: Automatically uses cone mode (faster) for directory-only configs, or non-cone mode when individual files are specified
- The init script adds the submodule normally
- It then configures sparse checkout with your specified paths
- Only the specified directories/files are checked out to disk
- The GitHub Action preserves the sparse checkout configuration on updates
The sparse checkout settings are stored in .sparse-checkout-config at the repo root. This file is automatically generated and used by the GitHub Action to restore selective sync settings after updates.
# .sparse-checkout-config (auto-generated)
bar:src/,docs/,README.md
lib:lib/utils/,lib/helpers/,types.d.ts
- Only the
dist/folder from a dependency:https://github.com/lib/build.git:dist/ - Documentation only:
https://github.com/org/docs.git:docs/,README.md,CHANGELOG.md - Specific modules:
https://github.com/org/monorepo.git:packages/core/,packages/utils/
If you want interactive scaffolding, run:
pip install cookiecutter
cookiecutter https://github.com/your-org/your-repo --directory cookiecutter-templateThis will prompt for project_slug and submodules just as before.
- scripts/init-submodules.sh → single-command add/remove
- .github/workflows/update-submodules.yml → daily GitHub Actions automation
- cookiecutter-template/ → optional interactive scaffolding
Enjoy a modular, self-maintaining monorepo without the manual overhead!