Monorepo Migration Case Study: Moving 8 Repos Into One Without Losing History
Why They Migrated
The team had 8 microservices in separate repositories: auth-service, user-service, payment-service, notification-service, api-gateway, shared-lib, admin-portal, worker-service.
Problems:
- Making a change that touched 3 services required 3 PRs, 3 review cycles, 3 deploys, and careful coordination of merge order
shared-libversion bumps required updating 7 downstream repos, often causing conflicts weeks later- CI configuration was duplicated across 8 repos with drift over time
- New engineers had to find 8 repos and understand how they connected
The decision: migrate to a monorepo where all services live under /services/<name> and shared code lives under /packages/.
---
The Challenge: Preserving History
Simply copying files into a new repo loses all history. Blame, log, and bisect stop working. The engineering team insisted on preserving history — git log services/auth-service/src/ should show commits going back 3 years.
The tool for this: git filter-repo (preferred) or git subtree.
---
Step 1: Prepare Each Service Repo
For each service, rewrite its history so all files appear under services/<name>/ instead of the repo root:
# Clone auth-service fresh (filter-repo modifies in place)
git clone https://github.com/company/auth-service.git /tmp/auth-service-prep
cd /tmp/auth-service-prep
# Rewrite history to move all files into services/auth-service/
git filter-repo --to-subdirectory-filter services/auth-service
After this, the repo's history looks as if all commits always had files under services/auth-service/.
---
Step 2: Create the Monorepo
mkdir /tmp/monorepo && cd /tmp/monorepo
git init
git commit --allow-empty -m "init: create monorepo"
---
Step 3: Merge Each Service Repo
for service in auth-service user-service payment-service notification-service api-gateway shared-lib admin-portal worker-service; do
echo "==> Merging $service..."
# Add the prepared repo as a remote
git remote add "$service" "/tmp/${service}-prep"
git fetch "$service" --tags
# Merge its history into the monorepo (allow unrelated histories)
git merge --allow-unrelated-histories -m "merge: import $service history" "$service/main"
# Remove the temporary remote
git remote remove "$service"
done
After this loop, the monorepo has the complete history of all 8 services with each file correctly attributed.
---
Step 4: Add Shared Configuration
mkdir -p packages packages/shared-types packages/config-common
# Root-level CI, linting, etc.
cat > .github/workflows/ci.yml << 'EOF'
on: [push, pull_request]
jobs:
changed-services:
# detect which services changed, only run their tests
EOF
cat > package.json << 'EOF'
{
"name": "company-monorepo",
"workspaces": ["services/*", "packages/*"]
}
EOF
git add .
git commit -m "chore: add monorepo root configuration"
---
Step 5: Validate History
# Check that a file's history is intact
git log --oneline services/auth-service/src/handlers/login.js
# Check that blame works
git blame services/payment-service/src/processor.js | head -20
# Check tags from original repos were preserved
git tag | grep auth-service
---
Step 6: CI/CD Changes
The CI pipeline needed to detect which services changed and only run tests for those:
# Get changed services from the diff
CHANGED=$(git diff --name-only origin/main...HEAD | grep "^services/" | cut -d/ -f2 | sort -u)
for service in $CHANGED; do
echo "Running tests for $service"
cd "services/$service" && npm test && cd ../..
done
---
The Cutover
The team used a 2-week transition period:
- Week 1: Monorepo live, all 8 original repos set to archived (read-only) on GitHub. Engineers continued using original repos.
- Week 2: New PRs must go to the monorepo. CI in original repos posted comments pointing to the monorepo.
- End of Week 2: Original repos fully archived, monorepo is the single source of truth.
---
What Improved
After migration:
- Cross-service changes: 1 PR, 1 review cycle, coordinated deploy
shared-libchanges: visible to all services in the same diff- New engineer onboarding: clone 1 repo, read 1 README
- CI configuration: maintained in one place, changes apply to all services immediately
What got harder: CI runs are larger (though affected-service detection mitigates this), the repo is larger to clone (shallow clone + sparse checkout help).
Practice Git collaboration in the ShellGenius Git Labs — the git-branch-merge and git-staging challenges use real multi-branch scenarios.