Mirroring third-party repositories for resilience Jump to heading
Every submodule, pinned Git dependency and vendored upstream remote is a promise from someone else: that the repository will keep existing, under the same name, with the commits you depend on still reachable. Upstreams break that promise more often than people expect. A maintainer force-pushes away a branch you pinned. A project is renamed, transferred or deleted. A hosting provider has an outage the morning of your release. A mirror β a copy of the upstream repository on infrastructure you control, refreshed on a schedule and never pruned β turns all of those into non-events. This page sets one up correctly, which mostly means making sure the mirror keeps what upstream deletes. It belongs with submodule and dependency integrity.
When to use this approach Jump to heading
- Your builds fetch code from Git repositories you do not control, via submodules, Git dependencies or subtree remotes.
- A build has failed because an upstream commit, tag or repository disappeared.
- Releases must be reproducible years later, as in reproducing a release build from a tag.
- You want a single place to scan third-party code before it reaches developers.
Step 1 β Create a mirror that never prunes Jump to heading
git clone --mirror copies every ref, and git remote update refreshes them. The default refresh also deletes refs that upstream deleted β exactly the commits you wanted to keep. Disable pruning, and keep a dated snapshot of upstreamβs refs so nothing is lost when upstream rewrites a branch.
cd /srv/mirrors
git clone --mirror https://github.com/example/fast-parser.git fast-parser.git
cd fast-parser.git
git config remote.origin.prune false
git config remote.origin.pruneTags false
git config gc.reflogExpire never
git config gc.reflogExpireUnreachable never Disabling reflog expiry keeps old values of each ref recoverable even after upstream force-pushes. Combined with no pruning, nothing upstream does can make a previously mirrored commit unreachable in your copy.
Step 2 β Archive every ref value you have ever seen Jump to heading
Reflogs are per-repository state and easy to lose in a migration. A sturdier approach copies each upstream ref into a dated namespace on every refresh that changes it. Those refs are ordinary Git refs, so they survive clones of the mirror and keep commits reachable indefinitely.
#!/bin/sh
# refresh-mirror.sh <mirror.git>
set -eu
cd "$1"
stamp=$(date +%Y%m%d%H%M)
git for-each-ref --format='%(objectname) %(refname)' refs/heads refs/tags > /tmp/before
git fetch --quiet origin '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
git for-each-ref --format='%(objectname) %(refname)' refs/heads refs/tags > /tmp/after
# Any "sha ref" line that is no longer present means the ref moved or vanished:
# keep its old value under refs/archive/
sort /tmp/before > /tmp/before.s; sort /tmp/after > /tmp/after.s
comm -23 /tmp/before.s /tmp/after.s | while read -r sha ref; do
git update-ref "refs/archive/$stamp/${ref#refs/}" "$sha"
done # Verification: simulate an upstream force-push and confirm the old commit is archived
git for-each-ref refs/archive --format='%(refname) %(objectname:short)' | tail Step 3 β Refresh on a schedule and alert on rewrites Jump to heading
Run the refresh every hour or so. Any archive ref created means upstream rewrote or deleted something you had seen, which is worth a human glance: it might be routine branch cleanup, or it might be a maintainer removing a malicious commit.
# .github/workflows/refresh-mirrors.yml (on the mirror host's runner)
on: { schedule: [{ cron: "23 * * * *" }] }
jobs:
refresh:
runs-on: [self-hosted, mirror-host]
steps:
- run: |
for m in /srv/mirrors/*.git; do
before=$(git -C "$m" for-each-ref refs/archive | wc -l)
./refresh-mirror.sh "$m"
after=$(git -C "$m" for-each-ref refs/archive | wc -l)
[ "$after" -gt "$before" ] && echo "::warning::$(basename "$m"): upstream rewrote $((after-before)) ref(s)"
done Step 4 β Point builds at the mirror Jump to heading
Change submodule URLs, Git dependency URLs and subtree remotes to the mirror. For submodules, update .gitmodules in a reviewed change and add the mirror URL to the allow-list described in detecting submodule URL tampering in pull requests.
git config -f .gitmodules submodule.vendor/fast-parser.url https://git.example.com/mirrors/fast-parser.git
git submodule sync --recursive
git commit -am "Fetch fast-parser from internal mirror" For builds you cannot easily edit, a URL rewrite on CI runners redirects without touching manifests:
git config --global url."https://git.example.com/mirrors/".insteadOf "https://github.com/example/" Rewrites are invisible in the repository, so prefer explicit URLs and use rewrites only as a transition, as discussed in rewriting remote URLs with insteadOf.
Step 5 β Treat the mirror as part of your supply chain Jump to heading
The mirror is now a single place every build trusts. Restrict who can push to it β only the refresh job β and protect it like any build input.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Wonβt the mirror grow forever? Jump to heading
Yes, slowly. Third-party libraries are usually small, and archived refs usually point at commits that share most of their objects with current history. Monitor disk use; if one mirror grows sharply, investigate why upstream is rewriting so much.
Is a package registry proxy a substitute? Jump to heading
For registry packages, a caching proxy does the same job. Git dependencies, submodules and subtree remotes bypass registries entirely, which is why they need Git mirrors.
Should we scan mirrored code for malware or secrets? Jump to heading
The mirror is a natural choke point for it: scan new commits on each refresh before builds can use them. Even a basic check β new binaries, new install scripts β catches a surprising amount.
Related Jump to heading
- Submodule & Dependency Integrity β the parent topic.
- Verifying Submodule Commits Are Reachable Upstream β the check a mirror makes reliable.
- Reusing a Git Mirror on Self-Hosted Runners β mirrors for speed rather than resilience.
- Pinning Git Dependencies to Commit SHAs β the pins a mirror keeps satisfiable.