Migrate GitLab to GitHub Enterprise Cloud with gh gl2gh
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:
gh gl2gh inventory-report \ --gitlab-server-url https://gitlab.com \ --gitlab-group acme-platformUse 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:
export GH_PAT="GITHUB_CLASSIC_TOKEN"export GITLAB_PAT="GITLAB_TOKEN"Before a production run, check these additional boundaries:
- Add the GitHub Enterprise Importer IP ranges to the destination organization’s IP allow list if the organization uses one.
- Add
Repository migrationsto the bypass list of destination rulesets that would reject imported history. This bypass is limited to the migration. - Decide whether the destination uses GitHub.com or a GHE.com data-residency subdomain, then prepare
TARGET_API_URLand—when using GitHub-owned storage on GHE.com—TARGET_UPLOADS_URL. - 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:
gh --versiongh extension install github/gh-gl2ghgh extension upgrade github/gh-gl2ghThe 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:
| Storage | CLI selection | What to check |
|---|---|---|
| GitHub-owned blob storage | --use-github-storage | No extra storage setup; GitHub deletes the archive after success or seven days after a failed migration. |
| AWS S3 | --aws-bucket-name | Provide the AWS region and credentials; GitHub does not delete the archive from your bucket. |
| Azure Blob Storage | AZURE_STORAGE_CONNECTION_STRING | Use 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:
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-storageThe 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:
gh gl2gh generate-script \ --gitlab-server-url https://gitlab.com \ --gitlab-group acme-platform \ --github-org acme-platform \ --output migration.ps1 \ --use-github-storageReview 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 automatically | Replacement work |
|---|---|
| Git LFS binary objects | Push 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 variables | Recreate 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 rules | Recreate teams, permissions, rulesets, and branch policies in the destination organization. |
| Webhooks, pipeline triggers, vulnerability reports, and other data absent from the export | Reconnect 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:
- Open the destination repository’s
Migration Logissue and review warnings, not just the final state. - Confirm the repository visibility and default branch before sharing the new URL.
- Check commits, tags, releases, issues, pull requests, attachments, and wiki pages against the source inventory.
- Push Git LFS objects and verify a representative large file from a clean clone.
- Recreate CI/CD workflows, variables, secrets, webhooks, and deployment environments. Do not assume
.gitlab-ci.ymlhas an equivalent GitHub Actions workflow waiting in the repository. - Reclaim mannequin identities and restore team access. Imported activity is attributed to placeholder users until an organization owner completes that step.
- 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
Report a typo or broken link, or suggest a related topic.