Skip to main content

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 PatternGitHub 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 login with org admin access
  • Configure Actions Importer: gh actions-importer configure (set GitLab instance URL, tokens)

Organization Settingsโ€‹

  • Review publicala org settings on GitHub
  • Configure default repository permissions (read for members)
  • Create org-level ruleset for main branches (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 dev branches (ruleset #15112321):
    • No force pushes
    • No branch deletion (prevents dev being auto-deleted by delete_branch_on_merge after a dev to main PR merges)
    • No PR or status-check requirements (direct pushes to dev remain allowed)
    • Transitional. All GitHub-hosted repos are moving to trunk-based (main only); see Branch Strategy. This ruleset will be deleted after the last repo drops dev. Per-repo cutover runbook in Dropping the dev Branch.
  • Set up org-level secrets (migrated from GitLab group-level CI/CD variables):
    • CLOUDFLARE_ACCOUNT_ID
    • SINGLESTORE_LICENSE
    • SLACKWEBHOOK_ENG_DEPLOYS
    • SLACKWEBHOOK_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
Repos with >100MB historical blobs

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.

LFS push then refs: retry on connection drop

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: to concurrency: groups
  • Replace when: manual with the split-gate pattern (approval job + deploy job). See Deploy Approval Pattern
  • Update cache configuration (GitLab cache: to actions/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_HOOK on the staging environment (read by both the PR preview deploy and the main auto-deploy); LARAVEL_CLOUD_PRODUCTION_DEPLOY_HOOK on the production environment. The staging-review environment needs no deploy-hook secret; its only job is the approval gate. For Vapor repos: VAPOR_API_TOKEN at the repo level (a staging-scoped token, read by Job B and by deploy-staging via fallback), plus an env-scoped VAPOR_API_TOKEN on production (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 add build-js and (optionally) lint-js; Vapor repos optionally add phpcpd. 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โ€‹

warning

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_HOOK secrets

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 publishConfig in package.json to 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 pull and git push work

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)
Per-repo host switching during the transition

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/delfino to GitHub Packages
  • Update all consumer repos (farfalla, volpe, etc.) to pull from GitHub Packages
  • Verify npm install / yarn install resolves correctly
  • Remove GitLab Packages publishing from old CI
Documentation Hosting
  • Reconnect Cloudflare Pages docs project to use GitHub as source (currently connected to GitLab)
  • Set up Cloudflare Pages project for criceto test reports
  • Remove GitLab Pages configuration
Notifications
  • Evaluate if gitlab-to-slack-proxy is still needed
  • Set up GitHub native Slack integration for the publicala org
  • 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)
X

Graph View