How to migrate from Azure DevOps to GitHub
Move Azure Repos to GitHub Enterprise Cloud with GitHub Enterprise Importer, keep Azure Boards and Pipelines connected or convert pipelines to GitHub Actions, then lock the old repositories.
Your migration plan
This moves Azure Repos Git repositories, with their history and pull requests, from Azure DevOps to GitHub Enterprise Cloud. GitHub Enterprise Importer (GEI) copies each repository in a single run while work on it is frozen. Azure Boards and Azure Pipelines can stay where they are and connect to GitHub, so you convert pipelines to GitHub Actions later, team by team. Migration time depends mostly on how many pull requests each repository has, and trial runs show you the real figure.
The smallest change for developers on day one: generate-script --all rewires pipelines to GitHub and connects Azure Boards for you.
Other options and when to use them
- GEI plus GitHub Actions Importer in the same move: Teams want to leave Azure Pipelines as part of the move and can accept a longer cutover.
- Mirror push (code only): Your source is Azure DevOps Server or your destination is GitHub Enterprise Server, or you have only a few repositories.
Set up access first, prove the method in a trial, then move repositories in waves. Pipelines and Boards stay connected on day one, so developers change only where their code lives. Pipeline conversion comes later, team by team, and old repositories are disabled last.
- Source check, tokens and the inventory reportConfirms GEI can read your source at all, and the pull request counts in the report decide how long each repository takes.
- GitHub organisation, accounts and the repository mappingNames, teams and accounts must exist before the script runs and before mannequins can be reclaimed.
- Shrink oversized repositoriesGEI refuses repositories and files over its limits. Rewriting history before the move means everyone re-clones once.
- Trial of one repository into a sandbox organisationShows timings and follow-up work without stopping anyone's work.
- Production waves, busiest repositories in their own windowThere is no top-up copy, so each wave is frozen while it runs. Repositories with many pull requests take longest.
- Per wave: mannequins, LFS objects, team members and Boards linksUntil these are done, history shows placeholders, builds that need large files fail and nobody but admins has access.
- Pipelines: rewired first, converted to GitHub Actions laterRewiring keeps builds running on day one. Converting team by team keeps each change small and testable.
- Developers switch remotes, then old repositories are disabledRewired pipelines and rollback both need the old repositories readable until GitHub is proven.
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 only from Azure DevOps Services, not Azure DevOps Server. TFVC repositories (Team Foundation Version Control, the older non-Git kind) do not migrate: convert them to Git first.Mirror push (git push --mirror)Works from any Git server, including Azure DevOps Server. A mirrored clone includes all branches and tags, and nothing else.Tip: Run gh ado2gh inventory-report per organisation and mark any TFVC repositories straight away.
GitHub Docs: Understand migrations from Azure DevOps to GitHubHow 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.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 the largest repositories before the trial.
GitHub Docs: Understand migrations from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: pull requests migrate with their user history, files attached to them and links to work items. Migration time depends largely on how many pull requests a repository has.Mirror push (git push --mirror)Copies Git data only. Pull requests stay in Azure Repos.Tip: Sort repos.csv by pull request count and give the busiest repositories their own window.
GitHub Docs: Understand migrations from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: repository branch policies migrate, except user-scoped and cross-repository policies.Tip: List user-scoped and cross-repository policies in the trial and recreate them as GitHub rulesets after each wave.
GitHub Docs: Understand migrations from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: work items stay in Azure Boards, and links between pull requests and work items migrate. The generated script with --all also configures the Azure Boards connection. AB# links (AB#123 in a commit or pull request) go to the wrong work item if a repository is connected to more than one Azure DevOps organisation.Tip: Connect each GitHub repository to one Azure DevOps organisation only.
GitHub Docs: Migrate repositories from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: pipelines do not migrate. The generated script with --all rewires existing Azure Pipelines to build from the new GitHub repositories.GitHub Actions ImporterConverts Azure Pipelines to GitHub Actions workflows and opens a pull request. Secrets, service connections, unknown tasks and some triggers and gates still need manual work.Tip: Convert simple build pipelines first, and leave release pipelines with gates and approvals until last.
GitHub Docs: Understand migrations from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: repository permissions do not migrate. The generated script with --all creates teams, which start empty.Tip: Export who has access to each team project before the wave, so you can add members the same day.
GitHub Docs: Migrate repositories from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: pull requests and comments are tied to mannequins (placeholder users). Content linked to a mannequin may not show in searches until you reclaim it.Tip: Make sure every developer has a GitHub account in your enterprise before the first wave.
GitHub Docs: Reclaiming mannequins for GitHub Enterprise ImporterHow each tool handles it
Built-in methodGEI: migrates repositories that use LFS, but not the LFS objects themselves. Push them as a follow-up task.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 repositories by looking for filter=lfs in .gitattributes.
GitHub Docs: Understand migrations from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: no delta migrations (a top-up copy of only what changed). Changes made during or after the run must be moved by hand. GitHub recommends, but does not require, halting work during the production run.Tip: Remove push rights for the freeze, and disable a repository only after its migration succeeded and pipelines build from GitHub, because a disabled repository cannot be read.
GitHub Docs: Migrate repositories from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: by default the script gives the new repository the same visibility as the source repository. The --target-repo-visibility option changes it.Tip: Check the script before you run it, so nothing becomes public by mistake.
GitHub Docs: Migrate repositories from Azure DevOps to GitHubHow each tool handles it
Built-in methodGEI: accepts only classic personal access tokens, not fine-grained ones. A GitHub IP allow list must include GEI's address ranges, and an identity provider IP allow list (such as a Microsoft Entra Conditional Access policy) can block it.Tip: Set both tokens to expire just after your last wave, and agree allow list handling with your identity team before the trial.
GitHub Docs: Managing access for a migration from Azure DevOpsHow each tool handles it
Built-in methodGEI: organisation rulesets can make a migration fail when commits break them, for example commit author emails outside a required domain.Tip: Migrate where blocking rulesets do not apply to the new repositories, and turn them on afterwards.
GitHub Docs: Understand migrations from Azure DevOps to GitHub
- 1
Get ready
Before you startWatch out: GEI does not migrate from Azure DevOps Server.
Ways around it
- Move to Azure DevOps Services first, or use a mirror push for code only.
Watch out: GEI accepts only classic tokens, and an identity provider IP allow list (such as an Entra Conditional Access policy) can block it.
Ways around it
- Allow classic tokens for the migration, add GEI's ranges to any GitHub IP allow list, and agree allow list handling with your identity team.
Show the detailed steps (2) →Watch out: TFVC repositories (Team Foundation Version Control, the older non-Git kind) do not migrate.
Ways around it
- Convert them to Git first. GitHub documents a TFVC import route.
- 2
Plan the waves
Watch out: GEI stops at 40 GiB of source code per repository and 400 MiB per file during migration.
Ways around it
- Run git-sizer on the largest repositories before the trial.
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.
Show the detailed steps (3) →Watch out: Organisation rulesets such as commit author email rules can make migrations fail.
Ways around it
- Migrate where blocking rulesets do not apply to the new repositories, and turn them on afterwards.
- 3
- 4
Migrate production
In each wave's freeze windowWatch out: GEI has no delta migration (a top-up copy of only what changed), so changes made during or after the run must be moved by hand.
Ways around it
- Lock each wave's repositories for the whole run.
Watch out: A disabled repository cannot be read, so pipelines that still need it fail.
Ways around it
- Remove push rights during the run, and disable the repository only after a successful migration.
Show the detailed steps (4) →Watch out: GEI migrates branch policies, except user-scoped and cross-repository ones.
Ways around it
- Recreate them as GitHub rulesets.
- 5
Finish people and Boards
Watch out: Repository permissions do not migrate, and the teams the CLI creates start empty.
Ways around it
- Export who has access to each team project and add them, or link the teams to Entra ID groups.
Watch out: GEI migrates the repository but not its LFS objects, so builds that need them fail.
Ways around it
- Mirror-clone from Azure Repos, run git lfs fetch --all, then git lfs push --all to GitHub.
Show the detailed steps (4) →Watch out: Work items stay in Azure Boards, and AB# links go wrong if a repository is connected to more than one Azure DevOps organisation.
Ways around it
- Keep Boards connected to GitHub, with each repository connected to one organisation only.
- 6
Sort out pipelines
Show the detailed steps (4) →Watch out: GEI does not migrate pipelines.
Ways around it
- Keep Azure Pipelines rewired to GitHub for the first waves.
- Convert simple pipelines first, and leave release pipelines with gates and approvals until last.
Tools that can do it
- GitHub Actions ImporterConverts Azure Pipelines to workflows. Secrets, service connections, unknown tasks and some triggers and gates still need manual work.
- 7
Switch and disable
Watch out: Deleting Azure Repos repositories, pipelines (with their run history) or the Azure DevOps organisation cannot be undone.
Ways around it
- Disable rather than delete: a disabled repository can be re-enabled.
- To roll back, re-enable Azure Repos and push back any commits made on GitHub.
Turn this phase into dated tasks with the cutover planner
Show the detailed steps (2) →
- Branch, tag and commit counts match, and the default branch head is the same commit on both sides.
- Pull request counts match repos.csv, and no current staff remain as unclaimed mannequins.
- Pipelines or workflows build from GitHub, and AB# links show on work items.
- Direct pushes to the default branch are refused, and the old repositories show as disabled.
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.
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) moves Azure Repos Git repositories from Azure DevOps Services to GitHub Enterprise Cloud, with commit history and pull requests. Work items stay in Azure Boards, and pipelines stay in Azure Pipelines unless you convert them. Many teams keep Boards and Pipelines running against GitHub for a while, then move CI to GitHub Actions with GitHub Actions Importer.
The order of work: inventory and plan, set up access and the CLI, trial in a sandbox organisation, migrate production with work halted, then do the follow-up tasks (people, LFS, teams, Boards, Pipelines) and disable the old repositories. For the same job from GitLab, see the GitLab to GitHub guide.
1What you need before step one
| Need | Details | Where to get it |
|---|---|---|
| Azure DevOps Services | GEI migrates from Azure DevOps Services only, not Azure DevOps Server. | Move Azure DevOps Server to Services first using Microsoft's migration guidance |
| GitHub Enterprise Cloud | An enterprise account on GitHub.com or GHE.com with the destination organisation. | GitHub sales or your partner; check current pricing |
| GitHub role and token | Organisation owner, or migrator role granted by an owner. Classic token: owner repo, workflow, admin:org; migrator repo, workflow, read:org. | GitHub > Settings > Developer settings > Personal access tokens > Tokens (classic) |
| Azure DevOps token | Work Items (Read), Code (Read) and Identity (Read) at minimum; GitHub recommends full access for inventory-report, pipeline rewiring and Boards. Allow access to all organisations if you migrate several. | Azure DevOps (https://dev.azure.com/contoso) > User settings > Personal access tokens |
| GitHub CLI | Version 2.4.0 or later with the ADO2GH extension. | https://cli.github.com |
| PowerShell | The generated migration script is PowerShell; macOS and Linux need PowerShell 7. | https://learn.microsoft.com/powershell/scripting/install/installing-powershell |
| Boards connection rights | Project Collection Administrator in Azure DevOps and admin or owner on the GitHub repositories. | Azure DevOps organisation settings and GitHub repository settings |
| Docker (for CI conversion) | GitHub Actions Importer runs in a container. | https://www.docker.com |
2Inventory and plan3 steps
| Azure DevOps | Moves with GEI? | What to do |
|---|---|---|
| Git source and history | Yes | Check size limits |
| Pull requests with user history, work item links and attachments | Yes | Complete or abandon stale pull requests first |
| Branch policies | Yes, except user-scoped and cross-repository policies | Recreate the exceptions as rulesets |
| Repository permissions | No | Teams on GitHub (CLI creates Maintainers and Admins teams) |
| Work items (Azure Boards) | No | Keep Boards and connect it to GitHub |
| Pipelines | No (can be rewired) | Rewire to GitHub, then convert with Actions Importer |
| Git LFS objects | No | Push with git lfs after migration |
| TFVC repositories | No | Convert to Git first (GitHub documents a TFVC import route) |
- 1
Run the inventory report
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installedAfter installing the extension and setting
ADO_PAT(Step 2), run the inventory report for each Azure DevOps organisation. It writes several CSV files;repos.csvlists each repository with its pull request count.gh ado2gh inventory-report --ado-org contosoYou should see: repos.csv and the other CSV files appear in the current folder, with one row per repository in repos.csv. - 2
Map organisations, projects and teams
Where: Your Identity and licence mapping sheetMap each Azure DevOps organisation to one GitHub organisation, each repository to a GitHub name (team project prefix if names clash), and each team project's permission groups to GitHub teams.
You should see: Every repository has a target organisation, name and owning team. - 3
Plan waves and freeze windows
Where: Your Migration wave planGroup repositories by team project or by teams that release together. Give each wave a trial date, a production date with a freeze window, and a decision for its pipelines: rewire to GitHub, or convert to Actions now.
You should see: The wave plan is signed off by each team lead.
3Set up access and the CLI2 steps
- 1
Create the tokens
Where: GitHub > Settings > Developer settings > Personal access tokens > Tokens (classic), and Azure DevOps (https://dev.azure.com/contoso) > User settings > Personal access tokens > New TokenCreate a GitHub classic token with
repo,workflowandadmin:org(owner) orrepo,workflowandread:org(migrator), and authorise it for SAML single sign-on if used. In Azure DevOps create a token with at least Work Items (Read), Code (Read) and Identity (Read), or full access as GitHub recommends, with an expiry just after your last wave.You should see: Two tokens stored in your password manager. - 2
Install the ADO2GH extension and set the tokens
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installedCheck
gh --versionis 2.4.0 or later, install the extension (GitHub updates it weekly, so upgrade before each wave) and set the tokens. For GHE.com, also setTARGET_API_URLtohttps://api.SUBDOMAIN.ghe.com.gh extension install github/gh-ado2gh gh extension upgrade github/gh-ado2gh # Terminal (macOS or Linux) export GH_PAT="ghp_your_github_classic_token" export ADO_PAT="your_azure_devops_token" # PowerShell $env:GH_PAT="ghp_your_github_classic_token" $env:ADO_PAT="your_azure_devops_token"You should see: gh ado2gh --help lists generate-script, migrate-repo and inventory-report.
4Run trial migrations3 steps
- 1
Create a sandbox organisation
Where: GitHub > your enterprise > Organizations > New organizationCreate an organisation whose name ends in
-sandbox, for examplecontoso-sandbox, so its purpose is clear. You can delete it after the trials.You should see: The sandbox organisation exists and you are an owner. - 2
Migrate a single repository
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installedMigrate one repository into the sandbox. Work does not need to stop for a trial.
gh ado2gh migrate-repo --ado-org contoso --ado-team-project Payments --ado-repo payments-api --github-org contoso-sandbox --github-repo payments-apiYou should see: The command reports success, and contoso-sandbox/payments-api has the code, branches and pull requests, plus a Migration Log issue. - 3
Validate the trial with users
Where: The sandbox repository on GitHub, with the repository ownerAsk the owner to check history, pull requests, branch policies and work item links. Confirm you can do the follow-up tasks (mannequins, LFS, teams, pipelines). Record timings and issues in the Pilot test plan, fix them, then delete the sandbox if you no longer need it.
You should see: The owner signs off the trial and the wave timings are updated.
5Migrate production repositories4 steps
- 1
Freeze the source repositories
Where: Azure DevOps (https://dev.azure.com/contoso) > the team project > Project settings > Repos > Repositories > select the repositoryAt the start of the freeze window, set the repository's permissions so contributors cannot push, or disable it (needs the Delete repository permission). A disabled repository stays in the list but cannot be read or updated, so only disable it after a successful migration if pipelines still need to read it during the run.
You should see: Pushes to the wave's repositories are refused. - 2
Generate the migration script
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installedGenerate a script for the organisation.
--alladds pipeline rewiring, team creation and Azure Boards integration;--download-migration-logsdownloads each repository's log. Open the script, delete or comment out repositories outside this wave, and edit--target-repoand--target-repo-visibilityif needed (visibility matches the source by default). For GHE.com add--target-api-url.gh ado2gh generate-script --ado-org contoso --github-org contoso --output migrate-wave1.ps1 --all --download-migration-logsYou should see: migrate-wave1.ps1 contains migration steps only for the repositories in this wave, plus the team, pipeline and Boards lines. - 3
Run the script
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installedRun the script and keep the console output.
./migrate-wave1.ps1 # Terminal (PowerShell 7 installed as pwsh) .\migrate-wave1.ps1 # PowerShellYou should see: Each repository reports success and a log file is downloaded for each. - 4
Read the migration logs
Where: The downloaded log files, and the Migration Log issue in each destination repositoryCheck each log for warnings and skipped items, for example branch policies that could not be migrated.
You should see: No errors, and warnings only for items you listed as not migrating.
6Finish people, LFS, teams and Boards4 steps
- 1
Reclaim mannequins
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installed, signed in with an organisation owner's tokenGenerate the mannequin CSV, add each person's GitHub username, and reclaim in bulk. Each person gets an email invitation and attribution moves when they accept (organisations using Enterprise Managed Users can add
--skip-invitation).gh ado2gh generate-mannequin-csv --github-org contoso --output mannequins.csv gh ado2gh reclaim-mannequin --github-org contoso --csv mannequins.csvYou should see: People accept and their pull request history shows under their GitHub accounts. - 2
Push Git LFS objects
Where: Terminal on your migration machine with Git LFS installed (https://git-lfs.com)For each repository that uses LFS, mirror-clone it from Azure Repos, fetch every LFS object and push them to GitHub.
git clone --mirror https://dev.azure.com/contoso/Payments/_git/payments-api cd payments-api.git git lfs fetch --all git lfs push --all https://github.com/contoso/payments-api.gitYou should see: A fresh clone from GitHub downloads LFS files with their content. - 3
Fill the teams
Where: GitHub > your organisation > Teams > TEAM-PROJECT-Maintainers and TEAM-PROJECT-AdminsAdd members to the teams the script created, or link them to Entra ID groups with team synchronisation. Create extra teams with Read or Write roles for contributors.
You should see: Each developer has the same level of access they had in the team project. - 4
Check the Azure Boards connection
Where: Azure DevOps (https://dev.azure.com/contoso) > the team project > Project settings > GitHub connectionsConfirm the migrated repositories are listed. To add one by hand select New connection, sign in with GitHub, choose the repositories and select Approve, Install, & Authorize for the Azure Boards app. One connection can hold up to 2,000 repositories. Ask developers to mention work items as
AB#123in commits and pull requests.You should see: A test pull request mentioning AB# with a work item ID shows a link on the work item in Azure Boards.
7Keep Azure Pipelines or convert to GitHub Actions4 steps
- 1
Confirm rewired pipelines build from GitHub
Where: Azure DevOps (https://dev.azure.com/contoso) > the team project > Pipelines > select each pipeline > Run pipelineIf you used
--all, the script pointed each pipeline at its new GitHub repository. Queue a run from the main branch and open a test pull request on GitHub.You should see: The pipeline runs from the GitHub repository, and the pull request on GitHub shows the pipeline check. - 2
Install and configure GitHub Actions Importer
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installed, with Docker runningInstall the extension, update the container and run
configure. Choose Azure DevOps and enter a GitHub classic token withworkflow, and an Azure DevOps token with read access to Agent Pools, Build, Code, Release, Service Connections, Task Groups and Variable Groups. Actions Importer works with Azure DevOps Services or Azure DevOps Server 2019 and later.gh extension install github/gh-actions-importer gh actions-importer update gh actions-importer configureYou should see: gh actions-importer -h lists audit, forecast, dry-run and migrate. - 3
Audit, forecast and dry-run
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installedAudit the organisation to see what converts, forecast runner usage, then dry-run each pipeline by its ID (from the pipeline URL). Use
releaseinstead ofpipelinefor classic release pipelines. Secrets, service connections, unknown tasks, self-hosted agents, environments and approvals need manual work.gh actions-importer audit azure-devops --output-dir tmp/audit gh actions-importer forecast azure-devops --output-dir tmp/forecast_reports gh actions-importer dry-run azure-devops pipeline --pipeline-id 42 --output-dir tmp/dry-runYou should see: tmp/audit holds the audit summary and tmp/dry-run holds the converted workflow for review. - 4
Open the workflow pull request
Where: Terminal or PowerShell on your migration machine, with the GitHub CLI and the ADO2GH extension installedRun
migratefor each pipeline. It opens a pull request with the workflow in the GitHub repository. Add secrets under the repository's Settings > Secrets and variables > Actions, fix the manual items, and merge when a run passes. Then disable the Azure pipeline so it does not run twice.gh actions-importer migrate azure-devops pipeline --pipeline-id 42 --target-url https://github.com/contoso/payments-api --output-dir tmp/migrateYou should see: The workflow runs green in the repository's Actions tab and the old pipeline is disabled.
8Switch developers and disable Azure Repos2 steps
- 1
Point local clones at GitHub
Where: Each developer's terminal, inside their existing cloneChange the remote rather than recloning, and update IDE connections and scripts that clone from Azure Repos.
git remote set-url origin https://github.com/contoso/payments-api.git git fetch origin git remote -vYou should see: git remote -v shows the github.com URL and git fetch succeeds. - 2
Disable the old repositories
Where: Azure DevOps (https://dev.azure.com/contoso) > the team project > Project settings > Repos > Repositories > select the repositoryOnce pipelines build from GitHub, disable each migrated repository. It stays listed, shows a disabled message and cannot be read or updated. Users with the Delete repository permission can re-enable it.
You should see: Opening the repository in Azure Repos shows that it is disabled.
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
GEI for repos, keep Boards and Pipelines
Medium effortBest for: Most organisations: move code and pull requests now, keep planning and CI stable
- generate-script --all rewires pipelines and connects Boards
- Smallest change for developers on day one
- Two platforms to run and licence for a while
- Work items stay in Azure Boards
GEI plus GitHub Actions Importer
High effortBest for: Teams that want to leave Azure Pipelines as part of the move
- Ends up with code and CI on GitHub
- Audit and forecast show the work before you commit
- Secrets, service connections, agents, environments and approvals need manual work
- Longer per-team cutover
Mirror push (code only)
Low effortBest for: A few repositories, Azure DevOps Server sources, or a GitHub Enterprise Server destination
- Works from any Git host to any GitHub product
- Keeps full history, branches and tags
- No pull requests, policies or work item links
- Teams, pipelines and LFS still need follow-up
Use GEI with generate-script --all and keep Azure Boards and Pipelines connected for the first waves; convert pipelines to Actions team by team once the repositories have settled. Use a mirror push only when GEI cannot reach your source or destination, such as Azure DevOps Server or GitHub Enterprise Server.
Size waves by pull requests, not repository countSaves time
GitHub says migration time is largely based on the number of pull requests in a repository. A small repository with years of pull requests can take longer than a large one with few.
Run gh ado2gh inventory-report (Step 1) and sort repos.csv by pull request count. Put the busiest repositories in their own trial and give them the longest freeze window.
Map one Azure DevOps organisation to one GitHub organisationCost lever
Azure DevOps nests organisation, team project and repository; GitHub has enterprise, organisation and repository. GitHub advises against creating a GitHub organisation per team project, which fragments permissions, policies and billing.
Map each Azure DevOps organisation to one GitHub organisation. Represent team projects with teams and a repository naming rule, for example a project prefix.
Halt work during the production run, there is no deltaReduces risk
GitHub says the importer does not support delta migrations: changes made during or after the migration must be moved by hand.
Agree a freeze window per wave, complete or abandon open pull requests you do not want copied, and lock or disable the source repositories for the window (Step 4).
Recreate permissions as teamsUser impact
GEI does not migrate repository permissions from Azure DevOps. The ADO2GH CLI creates two teams per team project (TEAM-PROJECT-Maintainers and TEAM-PROJECT-Admins) but leaves their membership to you.
Export who has access to each team project, add members to the created teams on GitHub, or link the teams to Entra ID groups if your enterprise uses team synchronisation. Add Read and Write teams for people who were contributors.
Keep Boards and Pipelines for now, convert CI laterSaves time
Work items and pipelines are not migrated, but GitHub says both Azure Boards and Azure Pipelines can be fully integrated with GitHub. Converting every pipeline before the repository move turns one project into two.
Use generate-script --all so the script rewires pipelines to the GitHub repositories and sets up Boards integration. Convert pipelines to GitHub Actions per team afterwards with GitHub Actions Importer.
GEI does not migrate pipelines. GitHub Actions Importer converts Azure Pipelines into GitHub Actions workflows and opens a pull request with them, listing the manual steps left.
- GitHub Actions ImporterSecrets, service connections, unknown tasks and some triggers and gates still need manual work.
Push Git LFS objects separatelyReduces risk
GEI migrates repositories that use Git LFS, but not the LFS objects. Builds that need those files fail after the move.
List repositories with filter=lfs in .gitattributes and push their LFS objects after migration with the commands in Step 5.
Check size limits and rulesets before the trialSaves 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. Organisation rulesets such as commit author email rules can make migrations fail.
Run git-sizer (https://github.com/github/git-sizer) on the largest repositories. Migrate into an organisation where blocking rulesets do not apply to the new repositories, and turn them on afterwards.
Rewriting history to move large files into Git LFS, or remove them, brings a repository under GEI's 400 MiB per-file limit before the trial.
- Git LFS (git lfs migrate)GitHub documents
git lfs migrateto move large files already in history into Git LFS, orgit filter-repoto remove them. Both rewrite history, so every clone must be re-cloned.
Use classic tokens and plan around IP restrictionsReduces risk
GEI accepts only GitHub personal access tokens (classic), and does not work if the organisation restricts classic tokens. An identity provider IP allow list such as an Entra Conditional Access policy can block the importer.
Allow classic tokens for the migration period, add GitHub's GEI ranges to any GitHub IP allow list, and agree with your identity team how to handle IdP allow lists during the migration, as GitHub describes.
Size the pipeline conversion with an audit firstCost lever
Actions Importer converts much of Azure Pipelines YAML and classic task groups, but secrets, service connections, unknown tasks, self-hosted agents, environments and approvals need manual work, and some triggers and gates are unsupported.
Run gh actions-importer audit azure-devops and forecast before you commit to dates. Convert simple pipelines first and leave release pipelines with gates and approvals until last.
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.
| Check | How to check | What good looks like |
|---|---|---|
| Branches, tags and commits match | In fresh clones from Azure Repos and GitHub run | Counts are identical and the default branch head commit hash matches. |
| Pull requests arrived | Compare the pull request count per repository in repos.csv with | Counts match and sampled pull requests keep comments, reviewers and work item links. |
| Migration logs are clean | Read the downloaded logs and the Migration Log issue in each repository. | No errors; warnings only for branch policies and items you expected not to migrate. |
| Branch protection is in place | Try a direct push to the default branch from a developer account, and check the repository's Settings > Branches and Rules pages. | The push is refused and required reviews match the Azure DevOps branch policies. |
| People and teams are right | Check the organisation's mannequins and each team's members and repository roles. | No unclaimed mannequins for current staff, and each team has the access in your mapping sheet. |
| Boards links work | Create a test commit with | The work item shows the GitHub commit in its Development section. |
| Pipelines or workflows build from GitHub | Queue each rewired Azure pipeline, or check the Actions tab for converted workflows, and open a test pull request. | Builds pass from the GitHub repository and pull requests show the check. |
| Azure Repos is no longer written to | Open each migrated repository in Azure Repos. | It shows as disabled (or read-only), with no pushes 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
- Rewired pipelines cannot build or deploy from GitHub and the release cannot wait
- Developers cannot sign in to GitHub (SSO, tokens or managed users) on the switch day
- Pull request history or branch policies are missing in a way the owners will not accept
Point of no return
GEI copies from Azure Repos and leaves the repositories in place, so while developers have not pushed to GitHub you can re-enable Azure Repos and carry on. After people commit and merge on GitHub, that work exists only there and must be pushed back by hand. Deleting Azure Repos repositories, Azure pipelines (and their run history) or the Azure DevOps organisation cannot be undone from GitHub.
- 1
Re-enable the source repositories
Where: Azure DevOps (https://dev.azure.com/contoso) > the team project > Project settings > Repos > Repositories > select the repositoryRe-enable each disabled repository (needs the Delete repository permission) and restore contributors' push rights.
You should see: Developers can push to Azure Repos again. - 2
Push back any commits made on GitHub
Where: Terminal on your migration machineMirror-clone each GitHub repository where work happened and push its branches and tags to Azure Repos. Recreate any pull requests opened on GitHub.
git clone --mirror https://github.com/contoso/payments-api.git cd payments-api.git git push --all https://dev.azure.com/contoso/Payments/_git/payments-api git push --tags https://dev.azure.com/contoso/Payments/_git/payments-apiYou should see: Azure Repos shows every branch and tag created on GitHub. - 3
Point pipelines back at Azure Repos
Where: Azure DevOps (https://dev.azure.com/contoso) > Pipelines > select the pipeline > Edit > Triggers > YAML > Get sourcesChange each rewired pipeline's source back to the Azure Repos repository and branch, save, and queue a run. Disable any GitHub Actions workflow that would also deploy.
You should see: The pipeline runs from Azure Repos and nothing deploys twice. - 4
Archive the GitHub copies
Where: GitHub > each repository > Settings > Danger Zone > Archive this repositoryArchive the migrated repositories so they become read-only, or delete them if you will rerun the migration with the same names.
You should see: The repositories show as archived. - 5
Revert developer remotes
Where: Each developer's cloneRun
git remote set-url originwith the Azure Repos URL and fetch.You should see: git remote -v shows dev.azure.com and git fetch succeeds.
Agree the triggers and who decides before the go / no-go meeting. Use the rollback plan template.
Common problems and fixes7 errors
You can only use GitHub Enterprise Importer to migrate from Azure DevOps CloudCause: The source is Azure DevOps Server.
Fix: Migrate the collection to Azure DevOps Services first, or use a mirror push for code only.
pack exceeds maximum allowed sizeCause: A single commit or push is over GitHub's 2 GiB limit.
Fix: Remove or split the oversized commit in the source, then migrate again.
Token rejected or 401 from GitHubCause: A fine-grained token was used, a scope is missing, SSO is not authorised, or the organisation restricts classic tokens.
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 rulesetCause: An organisation ruleset such as a commit author email rule rejects old commits.
Fix: Exclude the new repositories from the ruleset during migration and apply it afterwards.
inventory-report fails or returns partial dataCause: The Azure DevOps token lacks scopes or access to all organisations.
Fix: Create the ADO token with full access, as GitHub recommends for inventory-report, and allow all accessible organisations.
Builds fail after migration: LFS files are pointersCause: LFS objects are not migrated.
Fix: Run git lfs fetch --all from Azure Repos and git lfs push --all to GitHub.
AB# mentions link to the wrong work itemCause: The GitHub repository is connected to projects in more than one Azure DevOps organisation; work item IDs are not unique across organisations.
Fix: Connect each GitHub repository to one Azure DevOps organisation only.
Frequently asked questions
Does GitHub Enterprise Importer migrate Azure Boards work items?
No. It migrates Git history, pull requests and work item links on pull requests. Keep work items in Azure Boards and connect Boards to GitHub.
Can I migrate from Azure DevOps Server?
Not with GEI. Move to Azure DevOps Services first. GitHub Actions Importer does support Azure DevOps Server 2019 and later for pipeline conversion.
Will my Azure Pipelines keep working?
Yes, if you rewire them to the GitHub repositories. generate-script --all adds the rewiring to the migration script.
How do I convert Azure Pipelines to GitHub Actions?
Use GitHub Actions Importer: audit, forecast, dry-run and migrate. It opens a pull request with the converted workflow, and you finish the items it marks as manual.
Are repository permissions migrated?
No. The CLI creates Maintainers and Admins teams per team project; you add the members or link them to identity provider groups.
Can I sync changes after the first migration?
No. GEI has no delta migration, so halt work in each repository during its production run.
Official sources
11 official pages this guide is based on
Vendors change limits and features, so confirm details on the source before you rely on them. Last checked 4 Nov 2026.