1455 words
7 minutes

Migrate GitLab to GitHub Enterprise Cloud with gh gl2gh

2026-08-06
DevOps
GitHub
/
GitLab
/
DevOps
/
Migration
/
CLI

GitHub announced general availability for migrations from GitLab.com and maintained self-managed GitLab instances on August 3, 2026. The workflow uses the gh gl2gh extension of GitHub CLI and targets GitHub Enterprise Cloud on GitHub.com or GHE.com—not GitHub Enterprise Server. GitHub’s announcement has the short version; the migration guide fills in the operational details.

The important planning boundary is easy to miss: GitHub Enterprise Importer can move repository history and collaboration data, but it does not turn GitLab CI/CD into GitHub Actions, migrate Git LFS objects, or recreate GitLab permissions. Treat the move as a staged migration with a trial run and a post-migration worklist.

Start with an inventory, not a migration command#

GitLab projects and GitHub repositories do not map one-to-one at the organization level. GitLab uses an instance, groups, nested subgroups, and projects; GitHub uses an enterprise, organizations, and repositories. GitHub does not have an equivalent to nested subgroups, so decide where each top-level group and its teams will live before generating a batch.

The GL2GH inventory command creates CSV reports for the groups and projects that a GitLab token can access. It also includes the number of merge requests, which GitHub identifies as a major factor in migration planning:

Terminal window
gh gl2gh inventory-report \
--gitlab-server-url https://gitlab.com \
--gitlab-group acme-platform

Use the report to record at least:

  • source group and project path
  • intended GitHub organization and repository name
  • repository visibility after review
  • merge request volume and any unusually large repository
  • whether the project uses LFS, webhooks, protected rules, or CI/CD variables
  • the owner for recreating permissions, secrets, and deployment workflows

For a first batch, choose a repository that exercises the team’s normal issues, merge requests, releases, and build process without being the most operationally sensitive project.

Check access and export prerequisites#

The importer needs credentials for both ends of the migration. GitHub’s current access guide requires a personal access token (classic) for the destination; a fine-grained token is not supported for this workflow. The exact GitHub scopes depend on whether you are an organization owner or a delegated migrator, but a repository migration needs repo, workflow, and either admin:org for an owner or read:org for a migrator.

The GitLab token needs the api and read_repository scopes. For a self-managed instance, GitHub’s guide says the token must belong to an administrator so the export is complete and user attribution can be preserved. The projects must also have exports enabled.

Set the tokens as environment variables instead of putting them into the migration command:

Terminal window
export GH_PAT="GITHUB_CLASSIC_TOKEN"
export GITLAB_PAT="GITLAB_TOKEN"

Before a production run, check these additional boundaries:

  1. Add the GitHub Enterprise Importer IP ranges to the destination organization’s IP allow list if the organization uses one.
  2. Add Repository migrations to the bypass list of destination rulesets that would reject imported history. This bypass is limited to the migration.
  3. Decide whether the destination uses GitHub.com or a GHE.com data-residency subdomain, then prepare TARGET_API_URL and—when using GitHub-owned storage on GHE.com—TARGET_UPLOADS_URL.
  4. Agree on a change freeze. The importer does not support delta migrations, so changes made while a production migration runs will need to be moved manually.

Install the GL2GH extension#

Check that GitHub CLI is installed, then install and update the official extension:

Terminal window
gh --version
gh extension install github/gh-gl2gh
gh extension upgrade github/gh-gl2gh

The extension is a wrapper around GitHub’s migration APIs. It exports each GitLab project to a .tar.gz archive, stages that archive, uploads it to a blob store GitHub can read, and imports the result into the destination organization. The archive step is why source GitLab export settings, temporary storage, and cleanup need to be part of the plan.

Choose where migration archives are staged#

For most migrations, GitHub recommends its own blob storage. The following choice affects cleanup and network policy:

StorageCLI selectionWhat to check
GitHub-owned blob storage--use-github-storageNo extra storage setup; GitHub deletes the archive after success or seven days after a failed migration.
AWS S3--aws-bucket-nameProvide the AWS region and credentials; GitHub does not delete the archive from your bucket.
Azure Blob StorageAZURE_STORAGE_CONNECTION_STRINGUse a storage-account access-key connection string, not SAS; GitHub does not delete the archive.

Use an external bucket when the organization requires a particular network boundary or needs to retain archives. In that case, add a lifecycle rule yourself. Retaining migration archives without an expiration policy creates an avoidable copy of repository data.

Run a trial before production#

GitHub recommends a trial migration in a test organization. The trial answers three questions that a successful CLI exit cannot answer on its own:

  • Can the repository export and import complete with your rulesets and network controls?
  • Can the team finish the follow-up work, including mannequin reclamation and CI/CD recreation?
  • How long does this repository take to migrate?

For a single repository, the production-shaped command is:

Terminal window
gh gl2gh migrate-repo \
--gitlab-server-url https://gitlab.example.com \
--gitlab-group platform \
--gitlab-project docs-site \
--github-org acme-platform \
--github-repo docs-site \
--use-github-storage

The command defaults the new repository to private. If you need a different visibility, make it explicit with --target-repo-visibility public, private, or internal and still review the result after the import.

For several repositories, use the inventory to generate a script:

Terminal window
gh gl2gh generate-script \
--gitlab-server-url https://gitlab.com \
--gitlab-group acme-platform \
--github-org acme-platform \
--output migration.ps1 \
--use-github-storage

Review the generated file before running it. Remove projects that are out of scope, correct destination names, and set visibility per repository. Treat the script as a migration proposal that needs review, not as an unattended bulk delete-and-copy operation.

Know what still needs manual work#

The importer moves the Git source and history, wiki, issues and comments, merge requests as pull requests, milestones, timeline events, reactions, uploads, and releases when those records exist in the export archive. It does not migrate several pieces that teams often assume are part of a repository move:

Not migrated automaticallyReplacement work
Git LFS binary objectsPush the LFS objects to the destination after the repository import. Pointer files in history are not enough.
.gitlab-ci.yml, pipeline schedules, job traces, artifacts, and variablesRecreate the build and deployment workflow in GitHub Actions or another system. Existing GitHub Actions input checks are useful when the replacement workflow has manual deploy gates.
GitLab group settings, group membership, repository policies, and protected-branch rulesRecreate teams, permissions, rulesets, and branch policies in the destination organization.
Webhooks, pipeline triggers, vulnerability reports, and other data absent from the exportReconnect integrations and decide which historical data is worth preserving separately.

Merge request discussions also have a boundary: threaded discussions are imported as flat comments with context, and review comments are preserved as review comments only when the export contains diff data. Plan a review pass for projects where the exact discussion shape is important.

Verify the destination before switching users over#

The CLI reports success or failure when the process completes. If you queue a migration with --queue-only, use the extension’s wait-for-migration command or the migration log instead of treating the queue response as completion.

After each migration:

  1. Open the destination repository’s Migration Log issue and review warnings, not just the final state.
  2. Confirm the repository visibility and default branch before sharing the new URL.
  3. Check commits, tags, releases, issues, pull requests, attachments, and wiki pages against the source inventory.
  4. Push Git LFS objects and verify a representative large file from a clean clone.
  5. Recreate CI/CD workflows, variables, secrets, webhooks, and deployment environments. Do not assume .gitlab-ci.yml has an equivalent GitHub Actions workflow waiting in the repository.
  6. Reclaim mannequin identities and restore team access. Imported activity is attributed to placeholder users until an organization owner completes that step.
  7. Re-index or wait before judging code search results; GitHub documents that code search can take a few hours to settle after a migration.

Keep the source project available until the destination has passed these checks and the team has a rollback decision. The importer moves a snapshot, not a continuously synchronized mirror.

The practical sequence is inventory → access and policy checks → trial → production freeze → migration → follow-up verification. That order exposes missing permissions and unmigrated systems before a GitLab shutdown turns them into an incident.

FAQ#

Q: Does GitLab CI/CD migrate to GitHub Actions with gh gl2gh?#

A: No. GitHub lists CI/CD pipelines and schedules, .gitlab-ci.yml, variables, job traces, artifacts, and pipeline triggers among the data that is not migrated automatically. Recreate and test the replacement workflow separately.

Q: Can this importer target GitHub Enterprise Server?#

A: No. The current GitHub announcement supports GitHub Enterprise Cloud on GitHub.com and GHE.com. It explicitly excludes migrations from GitLab to GitHub Enterprise Server.

Q: Are Git LFS objects included in the migration archive?#

A: No. Git LFS pointer files travel with the Git history, but the binary LFS objects must be pushed to the destination as a follow-up task.

Q: Can I use a fine-grained GitHub personal access token?#

A: Not for this workflow. GitHub’s access guide currently requires a personal access token (classic), with scopes determined by whether the operator is an organization owner or a migrator.

References:

GitHub Changelog: Migrate from GitLab to GitHub with GitHub Enterprise Importer

GitHub Docs: Understand migrations from GitLab to GitHub

GitHub Docs: Plan your migration from GitLab to GitHub

GitHub Docs: Manage access for a migration from GitLab to GitHub

GitHub Docs: Migrate your repositories from GitLab to GitHub

GitHub Docs: Follow-up tasks

Migrate GitLab to GitHub Enterprise Cloud with gh gl2gh
https://laplusda.com/en/posts/gitlab-to-github-enterprise-importer/
Author
Zero
Published at
2026-08-06
License
CC BY-NC-SA 4.0
Was this article useful?

Report a typo or broken link, or suggest a related topic.