GitLab to GitHub Migration Guide
Step-by-step checklist for migrating repositories from GitLab to GitHub. Each repo follows the same per-repo process; cross-cutting tasks are handled once after all repos are moved.
For full repo details, see the Repository Inventory. For CI translation patterns, see the CI/CD Translation Reference. The target branch model post-migration is trunk-based (main only); see Branch Strategy. Repos that ship with a dev branch drop it during or after the migration; see Dropping the dev Branch.
Naming Conventionโ
GitHub repos use flat namespace. For nested GitLab subgroup repos:
| GitLab Pattern | GitHub Pattern |
|---|---|
publicala/<name> | <name> (as-is) |
publicala/fenice/dev/<name> | fenice-<name> |
publicala/fenice/dep/env/<name> | fenice-<name> |
publicala/fenice/dep/dependencies/<name> | fenice-<name> |
publicala/fenice-legacy-archived/<name> | <name> (already prefixed) |
publicala_exercises/<name> | <name> |
Phase 1: Setup (one-time)โ
Toolsโ
- Install GitHub CLI:
brew install gh - Install Actions Importer:
gh extension install github/gh-actions-importer - Verify:
gh actions-importer version(expect v1.3.6+) - Authenticate:
gh auth loginwith org admin access - Configure Actions Importer:
gh actions-importer configure(set GitLab instance URL, tokens)
Organization Settingsโ
- Review
publicalaorg settings on GitHub - Configure default repository permissions (read for members)
- Create org-level ruleset for
mainbranches (ruleset #14411941):- Require pull request reviews (1 approval)
- No force pushes
- No branch deletion
- Status checks: configured per-repo (job names vary)
- Create org-level ruleset for
devbranches (ruleset #15112321):- No force pushes
- No branch deletion (prevents
devbeing auto-deleted bydelete_branch_on_mergeafter adevtomainPR merges) - No PR or status-check requirements (direct pushes to
devremain allowed) - Transitional. All GitHub-hosted repos are moving to trunk-based (
mainonly); see Branch Strategy. This ruleset will be deleted after the last repo dropsdev. Per-repo cutover runbook in Dropping thedevBranch.
- Set up org-level secrets (migrated from GitLab group-level CI/CD variables):
CLOUDFLARE_ACCOUNT_IDSINGLESTORE_LICENSESLACKWEBHOOK_ENG_DEPLOYSSLACKWEBHOOK_ENG_PRODUCTION_DEPLOYS
Team Membersโ
The publicala GitHub Organization (Enterprise Cloud, via GitHub for Startups) was provisioned 2026-03 with all current engineers invited and added to the Core Team. Onboarding for new hires and the canonical add-member / grant-repo commands live in Access Management. Members are invited to the organization, not the Enterprise account โ Enterprise seats follow automatically.
Third-Party Runners (Depot)โ
- Sign up for Depot Startup plan
- Connect GitHub org to Depot
- Configure runner labels in workflows per the runners reference (default label, sizing policy, PHP setup tiers). Background: CI Runner Strategy.
- Test with a pilot repo before Tier 4
Phase 2: Per-Repo Migration Checklistโ
Repeat for each repository. Copy this checklist as a template.
2.1 Mirror the Repositoryโ
Commands: clone, create, push
# Clone all refs from GitLab
git clone --mirror git@gitlab.com:publicala/<repo>.git
cd <repo>.git
# For LFS repos (castoro, volpe): fetch LFS objects
git lfs fetch --all
# Create the GitHub repo
gh repo create publicala/<github-name> --private
# Push everything to GitHub
git push --mirror git@github.com:publicala/<github-name>.git
# For LFS repos: push LFS objects
git lfs push --all git@github.com:publicala/<github-name>.git
GitHub's pre-receive hook rejects the entire mirror push if any blob in any commit exceeds 100MB, even on stale branches. A repo that adopted LFS partway through its history can hit this on pre-LFS commits. Rewrite the history before pushing:
git lfs migrate import --everything --above=100MB
This rewrites every commit SHA from the first offending blob forward. Coordinate the cutover with the team so local clones don't carry orphaned branches. Validated on castoro (526 commits rewritten); volpe likely needs this too.
For large LFS pushes (>1GB), the SSH connection sometimes drops after LFS objects upload but before refs land. The push reports success on LFS but the GitHub repo shows no branches. Re-running git push --mirror lands the refs cleanly โ LFS objects are already on the server, so the retry is fast.
2.2 Rename Default Branch to mainโ
Skip if already on main. For repos on master, production, or other branches.
Commands: rename branch
# Rename the existing branch in place (preserves history, refs, and PR base).
# Org rulesets that protect `main` activate automatically once the default
# changes, so no extra setup is needed.
gh api repos/publicala/<github-name>/branches/master/rename \
-X POST -f new_name=main
This rewrites the ref on GitHub and re-points the default branch in one call. Avoid the older "delete master, push main" pattern โ it severs any open PRs and leaves a window where the repo has no default branch.
2.3 Verify the Mirrorโ
- Compare branch count: GitLab vs GitHub
- Compare tag count: GitLab vs GitHub
- Verify latest commit SHA matches on default branch
- For LFS repos: verify LFS objects are accessible (
git lfs ls-files)
2.4 Migrate CI/CD (repos with .gitlab-ci.yml)โ
Commands: actions-importer dry-run
# Dry-run: preview the translation
gh actions-importer dry-run gitlab \
--output-dir output/<repo> \
--namespace publicala \
--project <repo>
# Review the generated workflow files in output/<repo>/.github/workflows/
# Manually adjust as needed (see CI/CD Translation Reference)
Common adjustments after dry-run:
- Replace
services: [docker:dind]with appropriate Docker setup - Replace GitLab-specific variables (
CI_JOB_TOKEN,CI_REGISTRY, etc.) - Convert
resource_group:toconcurrency:groups - Replace
when: manualwith the split-gate pattern (approval job + deploy job). See Deploy Approval Pattern - Update cache configuration (GitLab
cache:toactions/cache) - Replace GitLab Pages deployment with Cloudflare Pages or GitHub Pages action
- Update Sentry release/deploy notification commands
Commit the workflow files to .github/workflows/ in the repo.
2.5 Configure Environments and Secretsโ
GitHub has three secret scopes: organization, repository, and environment. Deploy-related secrets (deploy hooks, API tokens) should be scoped to their environment so they're only exposed to jobs that declare that environment.
Use the three-environment pattern (staging, staging-review, production) for repos with deploy gates. See Deploy Approval Pattern for the full setup (environments, reviewers, secrets, workflow examples). Summary checklist:
- Create GitHub Environments matching workflow
environment:declarations. For deploy-gated repos:staging(no reviewer),staging-review(Core Team reviewer, PR previews),production(Core Team reviewer) - Add deploy secrets at the narrowest scope that every reader can reach. For Cloud repos:
LARAVEL_CLOUD_STAGING_DEPLOY_HOOKon thestagingenvironment (read by both the PR preview deploy and the main auto-deploy);LARAVEL_CLOUD_PRODUCTION_DEPLOY_HOOKon theproductionenvironment. Thestaging-reviewenvironment needs no deploy-hook secret; its only job is the approval gate. For Vapor repos:VAPOR_API_TOKENat the repo level (a staging-scoped token, read by Job B and bydeploy-stagingvia fallback), plus an env-scopedVAPOR_API_TOKENonproduction(a distinct production-scoped token for defense-in-depth). Full scoping table and rationale in Deploy Approval Pattern ยง2. - Use org-level secrets for shared values:
SINGLESTORE_LICENSE,CLOUDFLARE_ACCOUNT_ID,SLACKWEBHOOK_ENG_DEPLOYS,SLACKWEBHOOK_ENG_PRODUCTION_DEPLOYS - Use repository secrets for repo-specific values that aren't tied to a deploy environment
- Verify secret names match workflow references
2.6 Set Branch Protectionโ
- Apply branch protection rules to
main - Configure required status checks. Migrated Laravel repos converged on these job names:
build,lint,phpstan,test. Repos with a JS build addbuild-jsand (optionally)lint-js; Vapor repos optionally addphpcpd. Use these names verbatim in new workflows so status-check configuration is copy-pasteable across repos - Verify auto-delete head branches is enabled (automatically deletes the PR source branch after merge). Applied org-wide on 2026-04-24 to all 28 active repos; GitHub offers no org-level default, so confirm the toggle is still on for the repo being migrated and for any newly created repo
2.7 Update Deploy Integrationsโ
Laravel Cloud deploy hooks are async. The curl returns immediately; deployment happens in the background. Remove any post-deploy health check scripts that expect the deploy to be done.
Per-target deploy integration steps
Laravel Cloud:
- Update Laravel Cloud environment to connect to GitHub repo
- Regenerate deploy hook URLs if needed
- Update
LARAVEL_CLOUD_*_DEPLOY_HOOKsecrets
Laravel Vapor:
- Continue using deploy hooks from GitHub Actions (Vapor does not support GitHub as a source)
Cloudflare Workers/Pages:
- Update Cloudflare Pages project to connect to GitHub repo (if using Cloudflare Git integration)
- Or keep Wrangler CLI deploy from GitHub Actions
npm Registry (Delfino):
- Configure GitHub Packages authentication in workflow
- Update
publishConfiginpackage.jsonto point to GitHub Packages - Update consumer repos to pull from new registry
2.8 Verify End-to-Endโ
- Push a test commit to a feature branch
- Verify CI runs successfully
- Create and merge a PR
- Verify deployment triggers correctly (if applicable)
2.9 Update Local Developmentโ
- Update remote URL:
git remote set-url origin git@github.com:publicala/<github-name>.git - If the default branch was renamed (e.g.
masterโmain), rename the local branch and reset upstream:git branch -m master main
git fetch origin
git branch -u origin/main main
git remote set-head origin -a - Verify
git pullandgit pushwork
2.10 Archive on GitLabโ
- Mark the GitLab repo as archived (read-only)
- Add a note to the GitLab repo description:
Migrated to GitHub: github.com/publicala/<github-name>
Phase 3: Cross-Cutting Updatesโ
After all repos (or a significant batch) are migrated:
Laravel Cloud
- Reconnect all Laravel Cloud environments to GitHub repos
- Verify deploy hooks trigger from GitHub Actions
- Test staging and production deploys for each Cloud service
Sentry
- Update Sentry project integrations to watch GitHub repos
- Update deploy notification webhooks in GitHub Actions
- Verify source map uploads reference correct repo
Local Development (zoo)
- Update
zoo/docker-compose files to reference GitHub remotes - Update any git clone/pull scripts in zoo
- Update developer onboarding documentation
- Notify team to update local clones (see step 2.9)
While the cutover is in progress, zoo coexists with both hosts. Rather than flipping a global flag, scripts/functions.sh carries a github_projects array; cloneProject looks up each repo name and picks github.com or gitlab.com accordingly. Each migration PR appends one line to the array:
export github_projects=(
"castoro"
"coniglio"
# ...
)
This keeps the diff per migration to a single line and avoids a flag-day cutover for zoo.
npm Registry (Delfino)
- Publish
@publicala/delfinoto GitHub Packages - Update all consumer repos (
farfalla,volpe, etc.) to pull from GitHub Packages - Verify
npm install/yarn installresolves correctly - Remove GitLab Packages publishing from old CI
Documentation Hosting
- Reconnect Cloudflare Pages
docsproject to use GitHub as source (currently connected to GitLab) - Set up Cloudflare Pages project for
cricetotest reports - Remove GitLab Pages configuration
Notifications
- Evaluate if
gitlab-to-slack-proxyis still needed - Set up GitHub native Slack integration for the
publicalaorg - Configure notification channels per repo/event type
Cleanup
- Verify all 33 active repos are on GitHub and functional
- Verify all GitLab repos are archived
- Update CLAUDE.md files across all projects
- Update this migration documentation with final status
- Remove GitLab-specific tooling and scripts (glab, GitLab tokens in CI)