Migration playbook · GitLab

How to migrate from GitLab to GitHub

Move GitLab projects to GitHub Enterprise Cloud with GitHub Enterprise Importer, keeping history, issues and merge requests, then convert GitLab CI to GitHub Actions and rebuild permissions.

Updated 4 Nov 20267 phases21 detailed steps11 official sources
Start here

Your migration plan

This moves GitLab projects (code, history, wiki, issues and merge requests) to GitHub Enterprise Cloud. GitHub Enterprise Importer (GEI) copies each project in a single run while work on it is frozen, then you finish what it does not move: people, large files, permissions and CI pipelines. Trial runs into a test organisation show how long each project takes, and converting GitLab CI to GitHub Actions is its own piece of work.

Recommended routeGitHub Enterprise Importer with the gh gl2gh extension

Moves issues and merge requests as well as code, and can script whole GitLab groups at once.

Other options and when to use them
  • GitHub Importer or a mirror push (code only): You only need code and history, have a handful of projects, or are moving to GitHub Enterprise Server.
  • GraphQL API migration: A platform team wants to build its own orchestration around the importer.
Order to migrate in, and why

Set up people and access first, prove the method in a trial, then move projects in waves, finishing each wave's people, permissions and CI before developers switch. GitLab is retired last, once every pipeline runs on GitHub.

  1. Inventory, sizes and the GitLab-to-GitHub mappingOversized and LFS projects need extra work, and the mapping decides which organisation and team each project lands in.
  2. GitHub accounts for every developer, and the migration tokensMannequins (placeholder users) can only be reclaimed to accounts that exist, and every run needs working tokens.
  3. Shrink oversized projects (Git LFS or history clean-up)GEI refuses repositories and files over its limits. Rewriting history before the move means everyone re-clones once, not twice.
  4. Trial into a sandbox organisationShows timings and problems without stopping anyone's work.
  5. Production waves of projects that build togetherThere is no top-up copy, so each wave is frozen while it runs, and projects that depend on each other must switch together.
  6. Per wave: mannequins, LFS objects, teams, rulesets and secretsWithout these, history shows placeholders, builds fail and the default branch accepts direct pushes.
  7. CI conversion per projectWorkflows can only be tested once the code is on GitHub. Keep GitLab runners until each workflow is green.
  8. Developers switch remotes, then GitLab is retiredDeleting GitLab cannot be undone and takes pipeline history, artifacts and snippets with it, so it goes last.
Settings checklist: decide these before you copy

Built-in and third-party tools treat these differently. Tick each one off once you have decided.

Download this checklist (Excel)
  • How each tool handles it
    Built-in methodGEI: migrates to GitHub Enterprise Cloud only.
    Mirror push (git push --mirror)Works with any GitHub, including Enterprise Server. A mirrored clone includes all branches and tags. GitHub's steps do not cover issues, merge requests or the wiki, so treat them as not copied.

    Tip: If you need issues and merge requests on Enterprise Server, check GitHub's current options before you plan the waves.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: 40 GiB of source code per repository (public preview) and 400 MiB per file during migration. After migration GitHub limits files to 100 MiB, and single commits and pushes to 2 GiB. GitLab.com limits its export archive, including metadata, to 40 GB.
    Git LFS (git lfs migrate)Moves large files already in history into Git LFS, or git filter-repo removes them. Both rewrite history, so every clone must be re-cloned.

    Tip: Run git-sizer on mirror clones of the biggest projects before you set wave dates.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: LFS pointer files migrate with the history, but the LFS objects do not. Builds fail until you push them.
    Mirror push (git push --mirror)Also leaves LFS objects behind. GitHub's steps fetch them with git lfs fetch --all and push them with git lfs push --all as separate commands.

    Tip: Find LFS projects by looking for filter=lfs in .gitattributes, and add the push to each wave's runsheet.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: issues and comments migrate, and threaded discussions become flat comments that keep the thread context. Merge requests become pull requests with reviewers, approvers and state events. Comments become review comments only when diff data is in the export, which holds only the latest diff.
    Mirror push (git push --mirror)Copies Git data only. Issues and merge requests stay in GitLab.

    Tip: In the trial, ask each owner to open a few busy merge requests on both sides and compare them.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: milestones, releases and release assets, uploads (attachments), timeline events and emoji reactions migrate. Labels are not listed as migrated.

    Tip: Compare labels after the trial and recreate any that are missing, before the production waves.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: the project wiki migrates. Snippets, issue boards, time-tracking data, design-management data and vulnerability reports do not.

    Tip: Export snippets you need to a repository, and rebuild boards as GitHub Projects if teams rely on them.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: project members arrive as mannequins (placeholder users). Content stays attributed to the mannequin, and may not show in searches for the real person, until you reclaim it. On self-managed GitLab, a token from a non-administrator gives incomplete exports and loses user attribution.

    Tip: Make sure every developer has a GitHub account in your enterprise before the first wave, and reclaim mannequins straight after each one.

    GitHub Docs: Reclaiming mannequins for GitHub Enterprise Importer
  • How each tool handles it
    Built-in methodGEI: group settings and group membership do not migrate. You create the teams, add members and grant access yourself.

    Tip: Export group members from GitLab before the waves, so you can rebuild teams the same day.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: repository policies such as required approvals, merge trains, pipeline gates and mirroring do not migrate. Webhooks are not in the GitLab export.

    Tip: Recreate protections as GitHub rulesets straight after each wave, before developers switch, or the default branch accepts direct pushes.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: pipelines and pipeline schedules do not migrate. CI/CD variables, job logs and artifacts, and pipeline triggers are not in the GitLab export at all.
    GitHub Actions ImporterConverts GitLab pipelines to workflows and opens a pull request with them. Masked variable values and artifact reports need manual work, and caching between workflows is not supported.

    Tip: Run the Actions Importer audit before you set dates, and take secret values from your secret store, not from GitLab.

    GitHub Docs: Understand migrations from GitLab
  • How each tool handles it
    Built-in methodGEI: no delta migrations (a top-up copy of only what changed). Anything pushed, commented or merged in GitLab during or after the run does not reach GitHub and must be moved by hand.

    Tip: Archive the projects, or reduce developers to read-only, at the start of each freeze.

    GitHub Docs: Migrate your repositories from GitLab
  • How each tool handles it
    Built-in methodGEI (gh gl2gh migrate-repo): new repositories are private unless you set --target-repo-visibility to public or internal.

    Tip: Record the visibility for each project in your mapping sheet, so nothing becomes public by mistake.

    GitHub Docs: Migrate your repositories from GitLab
  • How each tool handles it
    Built-in methodGEI: needs a GitHub classic personal access token (fine-grained tokens are not accepted) with repo, workflow and admin:org scopes (read:org for someone with the migrator role), authorised for single sign-on. The GitLab token needs api and read_repository scopes, and on self-managed GitLab it must belong to an administrator, with project export enabled.

    Tip: Set the tokens to expire just after your last wave.

    GitHub Docs: Manage access for a GitLab migration
  • How each tool handles it
    Built-in methodGEI: organisation rulesets, such as commit author email rules, can make a migration fail when old commits break them.

    Tip: Migrate into an organisation without blocking rulesets, or exclude the new repositories, and apply the rulesets afterwards.

    GitHub Docs: Understand migrations from GitLab
The plan, phase by phase
  1. 1

    Plan the move

    Before you start

    Watch out: GEI stops at 40 GiB of source code per repository and 400 MiB per file during migration. GitLab.com also limits its export archive, which includes issues and other metadata, to 40 GB.

    Ways around it

    • Run git-sizer on mirror clones of the biggest projects, and give oversized ones their own wave.

    Tools that can do it

    • Git LFS (git lfs migrate)Moves large files already in history into Git LFS, or git filter-repo removes them. Both rewrite history, so every clone must be re-cloned.

    Watch out: GEI only migrates to GitHub Enterprise Cloud, not GitHub Enterprise Server.

    Ways around it

    • For Enterprise Server, use a mirror push (git clone --mirror then git push --mirror), which moves code and history only.
    Show the detailed steps (3) →
  2. 2

    Set up access

    Watch out: On self-managed GitLab, a token from a non-administrator gives incomplete exports and loses who wrote what.

    Ways around it

    • Use an administrator's token, and check project export is enabled before the trial.

    Watch out: Organisation rulesets, such as commit author email rules, can make imports fail on old commits.

    Ways around it

    • Migrate into an organisation without blocking rulesets, or exclude the migrated repositories, then apply the rulesets afterwards.
    Show the detailed steps (3) →
  3. 3

    Run a trial

    Watch out: GitHub does not list labels among the things GEI migrates.

    Ways around it

    • Check labels after the trial and recreate any that are missing.
    Show the detailed steps (3) →
  4. 4

    Migrate production

    In each wave's freeze window

    Watch out: GEI has no delta migration (a top-up copy of only what changed), so anything changed in GitLab during the run does not reach GitHub.

    Ways around it

    • Freeze each wave's projects for the whole run, and tell developers to push nothing until they switch.
    Show the detailed steps (3) →
  5. 5

    Finish people and permissions

    Watch out: GEI migrates only LFS pointer files, so builds fail until the objects are pushed.

    Ways around it

    • Mirror-clone from GitLab, run git lfs fetch --all, then git lfs push --all to GitHub.

    Watch out: GEI does not move group membership, branch protections, approval rules, webhooks, snippets or issue boards.

    Ways around it

    • Export a list per project from GitLab and recreate them straight after each wave, before developers switch.

    Watch out: GitLab masked variable values are not exported.

    Ways around it

    • Take the values from your secret store when you create Actions secrets.
    Show the detailed steps (4) →
  6. 6

    Convert CI

    Watch out: GEI does not move pipelines, CI/CD variables, job artifacts or pipeline triggers.

    Ways around it

    • Run the audit before you set dates, and keep GitLab runners until each workflow is green.

    Tools that can do it

    • GitHub Actions ImporterConverts GitLab pipelines to workflows. Masked variables and artifact reports still need manual work, and caching between workflows is not supported.
    Show the detailed steps (3) →
  7. 7

    Switch and retire GitLab

    Watch out: Deleting GitLab projects or cancelling GitLab cannot be undone, and pipeline history, job artifacts, snippets and boards go with it.

    Ways around it

    • Keep GitLab read-only until every pipeline runs in Actions and the decommission checklist is signed off.
    • To roll back before then, unarchive GitLab and push back any commits made on GitHub.

    Turn this phase into dated tasks with the cutover planner

    Show the detailed steps (2) →
You are finished when
  • Branch, tag and commit counts match, and the default branch head is the same commit on both sides.
  • Issue and pull request counts match, and no current staff remain as unclaimed mannequins.
  • LFS files download with their content, and each workflow runs green in Actions.
  • Direct pushes to the default branch are rejected, and nothing has been pushed to GitLab since the freeze.

Tools named in this plan are independent third-party products, not endorsed by the platform vendor and not paid placements. Check the tool's own documentation and run a pilot first.

Click by click

Detailed steps

Open a part when you reach it in the plan. Each step says where to go, what to do and what you should see.

About this migrationWhat it covers, limits at a glance

GitHub Enterprise Importer (GEI) now migrates GitLab projects to GitHub Enterprise Cloud with the gh gl2gh extension. It exports each project from GitLab, uploads the archive to blob storage and imports code, wiki, issues, merge requests (as pull requests), milestones and releases. It does not move pipelines, so GitLab CI is converted separately with GitHub Actions Importer.

The order of work: plan the organisation structure and trial runs, set up access and the CLI, migrate a sandbox, migrate production with work halted, then do the follow-up tasks (people, LFS, permissions, CI). If you only need code and history, GitHub Importer or a mirror push is simpler. For the same job from Azure DevOps, see the Azure DevOps to GitHub guide.

Native toolGitHub Enterprise Importer with the GL2GH extension of the GitHub CLI (gh gl2gh), generally available since August 2026
SourceGitLab.com and currently maintained GitLab Self-Managed versions
DestinationGitHub Enterprise Cloud on GitHub.com or GHE.com; GitHub Enterprise Server is not supported
MigratedGit history and wiki, issues, merge requests as pull requests, milestones, releases, comments
Not migratedCI/CD pipelines, Git LFS objects, group membership, repository policies, webhooks, CI/CD variables
Size limit40 GiB per repository (public preview); 400 MiB per file during migration
1What you need before step one
NeedDetailsWhere to get it
GitHub Enterprise CloudAn enterprise account on GitHub.com or GHE.com with the destination organisation. GEI does not migrate GitLab to GitHub Enterprise Server.GitHub sales or your partner; check current pricing
GitHub roleOrganisation owner, or a user granted the migrator role by an owner.GitHub organisation settings
GitHub tokenPersonal access token (classic), not fine-grained. Owner: repo, workflow, admin:org. Migrator: repo, workflow, read:org.GitHub > Settings > Developer settings > Personal access tokens > Tokens (classic)
GitLab tokenPersonal access token with api and read_repository. On self-managed GitLab, an administrator's token.GitLab > your avatar > Edit profile > Access tokens
GitHub CLIVersion 2.4.0 or later, with the GL2GH extension.https://cli.github.com
PowerShellThe generated migration script is PowerShell, so macOS and Linux need PowerShell 7 installed.https://learn.microsoft.com/powershell/scripting/install/installing-powershell
Docker (for CI)GitHub Actions Importer runs in a container.https://www.docker.com
IP allow listIf your organisation uses GitHub's IP allow list, add GEI's ranges as listed by GitHub.GitHub Docs: Manage access (GitLab)
2Plan the structure, waves and trial3 steps
GitLabMoves with GEI?What to do
Git history, branches, tags, wikiYesCheck size limits first
Issues and comments, milestonesYesIssue boards do not move
Merge requestsYes, as pull requests with comments, reviewers, approvers and stateClose stale merge requests first
Releases and assets, uploads, reactionsYesSpot-check after the trial
MembersAs mannequinsReclaim after each wave
LabelsNot listed by GitHub as migratedCheck after the trial and recreate if missing
Pipelines, schedules, CI/CD variablesNoGitHub Actions Importer and Actions secrets
Git LFS objectsNo (pointers only)Push with git lfs after migration
Groups, group membership, protections, webhooks, snippetsNoRecreate as teams, rulesets and webhooks
  1. 1

    Inventory the GitLab projects

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed, or the GitLab web UI for each group

    List every project with its group path, size, open merge requests, whether it uses LFS and CI, and its owner. The GL2GH extension has an inventory-report command in recent releases; run gh gl2gh inventory-report --help for its options. Record the results in your Application inventory and dependency map.

    You should see: One row per project with size, LFS and CI flags, owner and a target GitHub organisation and name.
  2. 2

    Map groups to organisations and teams

    Where: Your Identity and licence mapping sheet

    Decide which GitHub organisation each top-level GitLab group lands in, and which GitHub team replaces each subgroup or permission set. Nested GitLab paths become flat repository names, so agree a naming rule (for example payments-api for platform/payments/payments-api).

    You should see: Every project has a target organisation, repository name and owning team.
  3. 3

    Set the waves and freeze windows

    Where: Your Migration wave plan

    Group projects that build together into the same wave. Give each wave a trial date, a production date with a freeze window, and a CI conversion owner.

    You should see: The wave plan is signed off by each team lead.
3Set up access and the CLI3 steps
  1. 1

    Create the tokens

    Where: GitHub > Settings > Developer settings > Personal access tokens > Tokens (classic), and GitLab > Edit profile > Access tokens

    Create a GitHub classic token with repo, workflow and admin:org (organisation owner) or repo, workflow and read:org (migrator). If your organisation uses SAML single sign-on, authorise the token for it. Create a GitLab token with api and read_repository, as an administrator on self-managed GitLab.

    You should see: Two tokens stored in your password manager, each with an expiry after your last wave.
  2. 2

    Grant the migrator role if needed

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed, signed in with an organisation owner's token

    If the person running the migration is not an organisation owner, an owner grants them the migrator role. The GL2GH extension includes a grant-migrator-role command; run gh gl2gh grant-migrator-role --help for its arguments, or follow GitHub's "Grant the migrator role" page.

    You should see: The migrator can run migrations into the destination organisation.
  3. 3

    Install the GL2GH extension and set the tokens

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed

    Check gh --version is 2.4.0 or later, install the extension, then set the tokens as environment variables. For GHE.com (data residency), also set TARGET_API_URL to https://api.SUBDOMAIN.ghe.com.

    gh extension install github/gh-gl2gh
    gh extension upgrade github/gh-gl2gh
    
    # Terminal (macOS or Linux)
    export GH_PAT="ghp_your_github_classic_token"
    export GITLAB_PAT="glpat-your_gitlab_token"
    
    # PowerShell
    $env:GH_PAT="ghp_your_github_classic_token"
    $env:GITLAB_PAT="glpat-your_gitlab_token"
    You should see: gh gl2gh --help lists migrate-repo and generate-script.
4Migrate a trial into a sandbox organisation3 steps
  1. 1

    Create the sandbox organisation

    Where: GitHub > your enterprise > Organizations > New organization

    Create an organisation such as contoso-sandbox without blocking rulesets.

    You should see: The sandbox organisation exists and you are an owner.
  2. 2

    Migrate one project

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed

    Run migrate-repo with the full group path (for subgroups, parent-group/subgroup). --use-github-storage uses GitHub-owned blob storage; if you use your own storage, follow GitHub's "Configure blob storage" page. Add --target-repo-visibility to set private (default), internal or public. Use --no-ssl-verify only for a self-signed certificate on GitLab.

    gh gl2gh migrate-repo \
      --gitlab-server-url https://gitlab.contoso.com \
      --gitlab-group platform/payments \
      --gitlab-project payments-api \
      --github-org contoso-sandbox \
      --github-repo payments-api \
      --use-github-storage
    You should see: The command reports the migration succeeded, and contoso-sandbox/payments-api shows the code, wiki, issues and pull requests.
  3. 3

    Check the trial with the team

    Where: The sandbox repository on GitHub, with the project owner

    Compare branch and tag counts, open and closed issues, merge requests (now pull requests), releases and the wiki with GitLab. Check labels, which GitHub does not list as migrated. Record the elapsed time for wave sizing, and fix anything in GitLab (oversized files, broken refs) before production.

    You should see: The owner signs off the trial in your Pilot test plan, with any gaps listed.
5Migrate production repositories3 steps
  1. 1

    Freeze the source projects

    Where: GitLab > each project > Settings > General > Advanced > Archive project (or reduce developers to read-only access)

    At the start of the freeze window, make the wave's projects read-only so nothing changes during the run. Tell developers the freeze has started.

    You should see: Pushes and new merge requests are refused on the frozen projects.
  2. 2

    Generate the migration script

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed

    Generate a script for the group. By default it includes every project the token can see; narrow it with --gitlab-group, or --gitlab-group plus --gitlab-project for one project. Open the script, delete or comment out projects outside this wave, and edit --github-repo and --target-repo-visibility to match your naming rule.

    gh gl2gh generate-script \
      --gitlab-server-url https://gitlab.contoso.com \
      --gitlab-group platform \
      --github-org contoso \
      --output migrate-platform.ps1 \
      --use-github-storage
    
    # Review and edit the script, then run it
    ./migrate-platform.ps1      # Terminal (needs PowerShell 7 installed as pwsh)
    .\migrate-platform.ps1     # PowerShell
    You should see: migrate-platform.ps1 lists one migrate-repo line per project in the wave, with the target names you chose.
  3. 3

    Run the script and read the logs

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed

    Run the script and keep the console output. Download the migration log for each repository (the extension has a download-logs command; see gh gl2gh download-logs --help) and look for warnings about skipped items.

    You should see: Every project in the wave reports success, and the logs show no unexpected skipped items.
6Finish people, LFS and permissions4 steps
  1. 1

    Reclaim mannequins

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed, signed in with an organisation owner's token

    Generate a CSV of mannequins, fill in each person's GitHub username, and reclaim them in bulk. By default each person gets an email invitation and attribution moves when they accept. The commands follow the same pattern as the other GEI extensions; check gh gl2gh generate-mannequin-csv --help for the exact arguments.

    gh gl2gh generate-mannequin-csv --github-org contoso --output mannequins.csv
    gh gl2gh reclaim-mannequin --github-org contoso --csv mannequins.csv
    You should see: People accept the invitations and their GitLab history shows under their GitHub accounts.
  2. 2

    Push Git LFS objects

    Where: Terminal on your migration machine with Git LFS installed (https://git-lfs.com)

    For each project that uses LFS, mirror-clone it from GitLab, fetch every LFS object and push them to the new GitHub repository.

    git clone --mirror https://gitlab.contoso.com/platform/payments/payments-api.git
    cd payments-api.git
    git lfs fetch --all
    git lfs push --all https://github.com/contoso/payments-api.git
    You should see: git lfs ls-files in a fresh clone from GitHub lists the files, and they download with content rather than pointers.
  3. 3

    Create teams and grant access

    Where: GitHub > your organisation > Teams > New team, then each repository > Settings > Collaborators and teams

    Create the teams from your mapping sheet, add members, and give each team its role (Read, Triage, Write, Maintain or Admin) on its repositories. Link teams to identity provider groups if your enterprise uses team synchronisation.

    You should see: Each developer can push to the repositories their GitLab role allowed, and no further.
  4. 4

    Recreate protections, webhooks and secrets

    Where: GitHub > each repository (or organisation) > Settings > Rules, Webhooks, and Secrets and variables > Actions

    Recreate protected branches and approval rules as rulesets, add the webhooks and apps your GitLab projects used, and add CI/CD variables as Actions secrets or variables. GitLab masked variable values are not exported, so take them from your secret store.

    You should see: The default branch rejects direct pushes, webhooks deliver test events, and secrets are listed for each repository.
7Convert GitLab CI to GitHub Actions3 steps
  1. 1

    Install and configure GitHub Actions Importer

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed, with Docker running

    Install the extension, update the container and run configure. Choose GitLab, enter a GitHub classic token with the workflow scope, a GitLab token with read_api, and your GitLab URL.

    gh extension install github/gh-actions-importer
    gh actions-importer update
    gh actions-importer configure
    You should see: The configure prompts finish and gh actions-importer -h lists audit, forecast, dry-run and migrate.
  2. 2

    Audit, forecast and dry-run

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed

    Run the audit for the namespace to see which jobs convert fully or partly, the forecast to estimate runner usage, and a dry-run per project to review the generated workflow. script, stages, needs, image, variables and schedules convert; rules, include, services and parallel are partial; masked variables, artifact reports and cross-workflow caching are manual.

    gh actions-importer audit gitlab --output-dir tmp/audit --namespace platform
    gh actions-importer forecast gitlab --output-dir tmp/forecast --namespace platform
    gh actions-importer dry-run gitlab --output-dir tmp/dry-run --namespace platform --project payments-api
    You should see: tmp/audit holds an audit summary, and tmp/dry-run holds a workflow YAML per project for review.
  3. 3

    Open the workflow pull request

    Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the GL2GH extension installed

    Run migrate for each project. It opens a pull request with the converted workflow in the GitHub repository. Review it with the project owner, fix the manual items, and merge when a run is green.

    gh actions-importer migrate gitlab --target-url https://github.com/contoso/payments-api --output-dir tmp/migrate --namespace platform --project payments-api
    You should see: A pull request with the workflow appears in the repository, and its Actions run passes.
8Switch developers and retire GitLab2 steps
  1. 1

    Point local clones at GitHub

    Where: Each developer's terminal, inside their existing clone

    Change the remote URL rather than recloning, then fetch to confirm. Update IDE integrations, deploy keys and any scripts that clone from GitLab.

    git remote set-url origin https://github.com/contoso/payments-api.git
    git fetch origin
    git remote -v
    You should see: git remote -v shows the github.com URL and git fetch succeeds.
  2. 2

    Keep GitLab read-only, then retire it

    Where: GitLab admin area, and your Decommission checklist

    Leave migrated projects archived for an agreed period so links in tickets and documents still resolve. Keep GitLab runners until every pipeline runs in Actions. Then export what your records policy requires, and decommission or cancel GitLab.

    You should see: The decommission checklist is signed off and no build or deploy still references GitLab.
Worth knowing

Expert notes

The detail behind the plan: options compared, and what experienced engineers check first. Tap any item.

Compare all the ways to do this3 options

GitHub Enterprise Importer (gh gl2gh)

Medium effort

Best for: Organisations moving to GitHub Enterprise Cloud that want issues, merge requests and history

  • Migrates code, wiki, issues, merge requests, milestones and releases
  • Scriptable across whole groups with generate-script
  • GitHub Enterprise Cloud only, not GitHub Enterprise Server
  • No delta: work halts during each run
  • CI, LFS objects and group membership need follow-up work
Read more →

GitHub Importer or mirror push (code only)

Low effort

Best for: A few projects, a GitHub Enterprise Server destination, or teams that keep issues elsewhere

  • Simple: keeps full Git history, branches and tags
  • Works for any GitHub product
  • No issues, merge requests, wiki pages or releases
  • Still needs LFS, CI and permission work
Read more →

GraphQL API migration

High effort

Best for: Platform teams building their own orchestration around GEI

  • Full control over export, upload and import
  • Same importer as the CLI
  • You write and maintain the tooling
  • Archives over 100 MiB need your own blob storage
Read more →
Our advice

Use GitHub Enterprise Importer with gh gl2gh when you are moving to GitHub Enterprise Cloud and want issues and merge requests to come with the code. Use a mirror push for a handful of projects or for GitHub Enterprise Server, and the GraphQL API only if you need your own orchestration. Whichever you choose, plan GitLab CI conversion with GitHub Actions Importer as its own workstream.

Run a trial into a sandbox organisation firstReduces risk

GitHub recommends trial runs to see whether each repository migrates, whether it can be returned to a workable state, and how long it takes. Trial runs do not need work to stop.

What to do

Create a test organisation with a name ending in -sandbox, migrate a mix of small, large and busy projects, and time them. Use the timings to size the production waves, then delete the sandbox.

GitHub Docs: Migrate your repositories from GitLab
Halt work during the production run, there is no deltaReduces risk

GitHub says the importer does not support delta migrations: anything pushed, commented or merged in GitLab during the migration does not reach GitHub.

What to do

Agree a freeze window per wave, merge or close open merge requests you do not want carried across, and make the GitLab projects read-only for the window (Step 5). Developers push nothing to GitLab until they switch remotes.

GitHub Docs: Migrate your repositories from GitLab
Find large repositories and files before the wavesSaves time

GEI is limited to 40 GiB per repository (public preview) and 400 MiB per file during migration; GitHub limits files to 100 MiB after migration, commits and pushes to 2 GiB, and refs to 255 bytes. GitLab.com limits the export archive to 40 GB including metadata.

What to do

Run git-sizer (https://github.com/github/git-sizer) on a mirror clone of the biggest projects. Move large binaries to Git LFS or clean history before the production run, and give oversized projects their own wave.

Tools that can get around this

Rewriting history to move large files into Git LFS, or remove them, brings a project under GEI's 400 MiB per-file limit before the production run.

  • Git LFS (git lfs migrate)GitHub documents git lfs migrate to move large files already in history into Git LFS, or git filter-repo to remove them. Both rewrite history, so every clone must be re-cloned.
Third-party tools, not endorsed by the platform vendor. Confirm in the tool's documentation and run a pilot first.
GitHub Docs: Understand migrations from GitLab
Push Git LFS objects separatelyReduces risk

LFS pointer files migrate, but the binary objects do not. A repository that looks complete fails its first build when the LFS files are missing.

What to do

List projects that use LFS (look for .gitattributes with filter=lfs). After each migration, fetch and push the LFS objects with the commands in Step 6.

GitHub Docs: Understand migrations from GitLab
Use an administrator token on self-managed GitLabReduces risk

On self-managed instances the GitLab token must belong to an administrator so exports are complete and user attribution is kept. Projects must also be enabled for export.

What to do

Create the GitLab personal access token as an instance administrator with the api and read_repository scopes, and check project export is enabled in your instance's import and export settings before the trial.

GitHub Docs: Manage access for a GitLab migration
Plan people: mannequins and teamsUser impact

Project members arrive as mannequins (placeholder users), and group settings and group membership are not migrated. Until mannequins are reclaimed, history is attributed to placeholders and content may not show in searches for the real user.

What to do

Make sure every developer has a GitHub account in your enterprise (or managed user) before the waves. Map GitLab groups to GitHub teams in your Identity and licence mapping sheet, create the teams, and reclaim mannequins after each wave.

GitHub Docs: Reclaiming mannequins for GitHub Enterprise Importer
Budget real effort for CI, it does not migrateCost lever

GitHub states that .gitlab-ci.yml has no automatic GitHub Actions equivalent in the importer, and CI/CD variables, job artifacts and pipeline triggers are not in the GitLab export. Actions Importer converts much of the YAML, but masked variables, artifact reports and cross-workflow caching need manual work.

What to do

Run gh actions-importer audit gitlab before you set dates. Its report shows what converts fully, partially or not at all, so you can size the CI work per project. Keep GitLab runners until each project's Actions workflow is green.

Tools that can get around this

GitHub Actions Importer converts GitLab pipelines into GitHub Actions workflows and opens a pull request with them, with a list of manual steps such as creating secrets.

  • GitHub Actions ImporterMasked variable values and artifact reports still need manual work, and caching between jobs of different workflows is not supported.
Third-party tools, not endorsed by the platform vendor. Confirm in the tool's documentation and run a pilot first.
GitHub Docs: Migrating from GitLab with GitHub Actions Importer
Recreate protections and integrations by handCompliance

Repository policies (required approvals, merge trains, pipeline gates, mirroring), webhooks, snippets, issue boards and vulnerability reports are not migrated. A repository without branch protection on day one accepts direct pushes to main.

What to do

Export a list of protected branches, approval rules, webhooks and integrations per project from GitLab. Recreate them as GitHub rulesets or branch protection, webhooks and apps straight after each wave, before developers switch.

GitHub Docs: Understand migrations from GitLab
Check organisation rulesets will not block the importSaves time

GitHub notes that organisation rulesets, such as commit author email rules, can make GEI migrations fail when old commits break them.

What to do

Migrate into an organisation without blocking rulesets, or exclude the migrated repositories from them, then apply the rulesets after the migration.

GitHub Docs: Understand migrations from Azure DevOps to GitHub
Finish safely

Check it worked, or roll back

Full validation checklist8 checks

Run these before you declare the migration complete. Record the results in the data validation report.

CheckHow to checkWhat good looks like
Branches, tags and commits match

In a fresh clone from each side run git branch -r | wc -l, git tag | wc -l and git rev-list --all --count, and compare.

Counts are identical, and the default branch head commit hash is the same in GitLab and GitHub.
Issues and pull requests arrived

Compare open and closed issue counts and merge request counts in GitLab with issues and pull requests on GitHub, using gh issue list --state all --limit 5000 | wc -l and gh pr list --state all --limit 5000 | wc -l in the repository.

Counts match, and sampled pull requests keep their comments, reviewers and approvals.
Migration logs are clean

Read the migration log for each repository downloaded in Step 4.

No errors, and any warnings are for items you listed as not migrating.
LFS content is real

Clone the repository from GitHub and run git lfs ls-files, then open a few of the listed files.

Files download with their content, not as text pointer files.
People and permissions are right

Check the organisation's mannequin list (People > Mannequins) and each repository's Collaborators and teams page.

No unclaimed mannequins for current staff, and each team has the role in your mapping sheet.
CI runs in Actions

Open the repository's Actions tab after merging the converted workflow, and trigger a run on a test branch.

The workflow runs green on push and on pull request, and deployments reach the same targets as in GitLab.
Protections and webhooks are in place

Try a direct push to the default branch from a developer account, and send a test delivery from each webhook.

The push is rejected by the ruleset, and each webhook shows a successful delivery.
GitLab is no longer written to

In GitLab, check the last activity date on each migrated project.

No pushes or merge requests after the freeze started.
Rollback planIf it goes wrong

Roll back if

  • The production run fails for key repositories and cannot be rerun inside the freeze window
  • Builds or deployments cannot run in GitHub Actions and the GitLab pipelines are still needed to ship
  • History or pull requests are missing in a way the project owner will not accept
  • Developers cannot authenticate to GitHub (SSO, tokens, managed users) on the switch day

Point of no return

GEI copies from GitLab and leaves the projects in place, so until developers push to GitHub you can unarchive GitLab and carry on. Once people commit, open pull requests and run releases on GitHub, that work exists only there and has to be pushed back by hand. Deleting GitLab projects, removing runners or cancelling GitLab cannot be undone, and GitLab-only data (pipeline history, job artifacts, snippets, issue boards) goes with it.

  1. 1

    Unfreeze the GitLab projects

    Where: GitLab > each project > Settings > General > Advanced > Unarchive project (or restore developer access)

    Unarchive the wave's projects and tell developers to keep working in GitLab.

    You should see: Developers can push and open merge requests in GitLab again.
  2. 2

    Push back any commits made on GitHub

    Where: Terminal on your migration machine

    For each repository where people pushed to GitHub, fetch from GitHub and push the new branches and tags to GitLab. Recreate any pull requests opened on GitHub as merge requests.

    git clone --mirror https://github.com/contoso/payments-api.git
    cd payments-api.git
    git push --all https://gitlab.contoso.com/platform/payments/payments-api.git
    git push --tags https://gitlab.contoso.com/platform/payments/payments-api.git
    You should see: GitLab shows every branch and tag that was created on GitHub.
  3. 3

    Archive the GitHub copies

    Where: GitHub > each repository > Settings > Danger Zone > Archive this repository

    Archive the migrated repositories so nobody keeps using them, or delete them if you will rerun the migration (a rerun needs the target name free).

    You should see: The repositories show as archived and read-only.
  4. 4

    Revert remotes and pipelines

    Where: Each developer's clone, and your CI settings

    Set each clone's origin back to the GitLab URL with git remote set-url origin, and re-enable any GitLab pipelines or runners you paused.

    You should see: git remote -v shows GitLab and pipelines run there again.

Agree the triggers and who decides before the go / no-go meeting. Use the rollback plan template.

Common problems and fixes6 errors
Migration fails: repository or archive too large

Cause: The repository is over the 40 GiB GEI limit, a file is over 400 MiB, or the GitLab.com export archive is over 40 GB.

Fix: Clean history or move large files to LFS in GitLab, then migrate again.

pack exceeds maximum allowed size

Cause: A single push or commit is over GitHub's 2 GiB limit.

Fix: Split the history or remove the large commit before migrating.

Export incomplete or users missing (self-managed)

Cause: The GitLab token does not belong to an administrator, or project export is disabled.

Fix: Use an administrator's token with api and read_repository, enable project export, and migrate again.

Token rejected by GitHub

Cause: A fine-grained token was used, a scope is missing, or the organisation restricts classic tokens or needs SAML authorisation.

Fix: Use a classic token with the documented scopes, authorise it for SSO, and allow classic tokens for the migration.

Migration fails on an organisation ruleset

Cause: An organisation ruleset (for example commit author email rules) rejects old commits.

Fix: Exclude the migrated repositories from the ruleset during migration, then apply it afterwards.

Build fails after migration: LFS files are pointers

Cause: LFS objects are not migrated.

Fix: Run git lfs fetch --all from GitLab and git lfs push --all to GitHub.

Search the full error library →

FAQ

Frequently asked questions

Does GitHub Enterprise Importer support GitLab?

Yes. GitHub made GitLab migrations with the gh gl2gh extension generally available in August 2026, from GitLab.com and currently maintained GitLab Self-Managed versions to GitHub Enterprise Cloud.

Are merge requests and issues migrated?

Yes. Issues migrate with comments, state and milestone events, and merge requests become pull requests with comments, reviewers, approvers and state events.

Can I migrate GitLab to GitHub Enterprise Server?

Not with GEI. GitHub says GitLab migrations to GitHub Enterprise Server are not supported; for code only, use a mirror push.

Does GitLab CI move to GitHub Actions?

Not with GEI. Use GitHub Actions Importer to convert .gitlab-ci.yml into workflows, then finish the parts it marks as partial or manual.

Can I migrate in stages and sync changes later?

No. The importer has no delta migration, so halt work in each project during its production run.

What if I only need the code?

Use GitHub Importer or git clone --mirror and git push --mirror. Both keep history but not issues or merge requests.

Sources

Official sources

11 official pages this guide is based on