Trigger deploys on Netlify with submodules
By Flavio
Tagged: Git
Learn how a GitHub Action updates the parent submodule pointer so Netlify deploys the new commit, and how a build hook starts a rebuild.
I wanted to rebuild my site on Netlify any time I committed in a Git submodule. Netlify watches the site repository, so a push to the submodule never starts that build on its own.
A submodule is a pointer. The site repo stores the submodule URL and one commit SHA. On each build, Netlify clones the submodule at that SHA. A new commit in the submodule repo leaves the pointer where it was.
Pushing an updated pointer starts the Netlify build when the site repo is linked to Netlify. A build hook starts a build and leaves the pointer where it is.
Start a build with a hook
A build hook is a URL. Send it a POST request and Netlify starts a deploy of the branch you chose for that hook.
Netlify calls sites projects now. I opened Project configuration, then Developer settings, then Continuous deployment. Under Build hooks I clicked “Add build hook”. The screenshot below is from 2022, so the UI looks a bit different today. You still set a hook name and a branch.

Saving this gave me a unique URL I could call, something like https://api.netlify.com/build_hooks/UNIQUE_STRING.
Treat that URL like a password. Anyone who has it can start builds of your site. Builds also have to be active. A paused site ignores the hook.
Then I created a GitHub Action in the submodule repository. How to use GitHub Actions for CI/CD is the longer walkthrough if this is your first workflow.
Save the file as .github/workflows/netlify.yml in the submodule repo:
name: Trigger redeploy on Netlify
on:
push:
branches: [ "main" ]
jobs:
build:
name: Make POST request
runs-on: ubuntu-latest
steps:
- name: Curl request
shell: bash
env:
UNIQUE_STRING: ${{ secrets.NETLIFY_BUILD_HOOK_UNIQUE_STRING }}
run: curl -X POST -d {} https://api.netlify.com/build_hooks/$UNIQUE_STRING
Copy the unique string from the hook URL into a repository secret named NETLIFY_BUILD_HOOK_UNIQUE_STRING. In the repo, open Settings, then Secrets and variables, then Actions. The workflow builds the URL from that secret, so the full URL stays out of the file.
The step posts to the hook with curl. The curl guide shows what -X POST and -d do. The body becomes the INCOMING_HOOK_BODY environment variable inside the Netlify build. {} is enough. Netlify does not require a particular payload.
curl exits 0 when Netlify answers with an error. Change the run line if you want a 404 or a 500 to fail the job:
curl --fail -X POST -d '{}' "https://api.netlify.com/build_hooks/$UNIQUE_STRING"
Commit the action. A push to main in the submodule starts a deploy, and the deploy list shows the hook name:

That deploy still checks out the old submodule SHA. The hook rebuilt the site repo at the commit the pointer already stored.
Update the pointer
To publish the new submodule commit, point the site repo at it and push. Netlify sees a push to the site repo and builds that commit. Calling the hook on that same push builds the site twice, because the pointer push already started a build.
The workflow lives in the submodule repo, and it has to push to a different repository. The default GITHUB_TOKEN cannot do that. Create a personal access token with write access to the site repo, and save it as SITE_REPO_TOKEN in the submodule’s Actions secrets.
In the site repo, tell Git which branch --remote should follow. Run this once, from the site repo, and commit the .gitmodules change:
git submodule set-branch --branch main content/handbook
content/handbook is the folder where the submodule sits. Use your own path.
Then add .github/workflows/update-site.yml in the submodule repo:
name: Update the site submodule pointer
on:
push:
branches: [main]
jobs:
bump:
runs-on: ubuntu-latest
steps:
- name: Checkout the site repo
uses: actions/checkout@v7
with:
repository: flaviocopes/site
token: ${{ secrets.SITE_REPO_TOKEN }}
path: site
- name: Point the submodule at the remote branch
working-directory: site
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git submodule update --init --remote content/handbook
git add content/handbook
if git diff --cached --quiet; then
exit 0
fi
git commit -m "Update handbook submodule"
git push
Replace flaviocopes/site with your site repository. actions/checkout clones the default branch of that repo. That branch has to be the one Netlify builds. If you deploy main and the default branch is master, the push updates the wrong branch. Pass ref: main on the checkout step in that case.
git submodule update --init --remote clones the submodule if needed and checks out the latest commit on the branch you set. The next lines commit that SHA in the site repo and push it. The if skips the commit when the site repo already points at that commit.
A public submodule should use an https URL in .gitmodules. The action can clone that URL, and Netlify can too, with no deploy key. A private submodule needs a token that can read it, and Netlify needs the site’s deploy key on that repo. Generate the key under Project configuration, Developer settings, Continuous deployment, Deploy key, and add the public half to the submodule repo. An ssh URL needs that key as well.
Use the hook when the site repo did not change and you still want a rebuild. A CMS write, or a post dated in the future, is that case. For a new submodule commit, update the pointer.
If you need a starting point for other deploy workflows, I built a free GitHub Actions deploy workflow generator that writes the YAML for the most common targets.
Want me to talk about your product? You can sponsor this site.
Related posts about git: