Adding repo to team fails with "Unauthorized. Please check your token and try again" even when the token has repo permissions
Description
I am using gh ado2gh to test migration of our repos from DevOps to GHE. But the migration script fails with error 401 on step, where the migrated repository is added to created team. I have used generate-script with --sequential to make the migration sequential for purpose of the test.
Reproduction Steps
Used script:
#!/usr/bin/env pwsh
# =========== Created with CLI version 1.17.0 ===========
function Exec {
param (
[scriptblock]$ScriptBlock
)
& @ScriptBlock
if ($lastexitcode -ne 0) {
exit $lastexitcode
}
}
if (-not $env:ADO_PAT) {
Write-Error "ADO_PAT environment variable must be set to a valid Azure DevOps Personal Access Token with the appropriate scopes. For more information see https://docs.github.com/en/migrations/using-github-enterprise-importer/preparing-to-migrate-with-github-enterprise-importer/managing-access-for-github-enterprise-importer#personal-access-tokens-for-azure-devops"
exit 1
}
else {
Write-Host "ADO_PAT environment variable is set and will be used to authenticate to Azure DevOps."
}
if (-not $env:GH_PAT) {
Write-Error "GH_PAT environment variable must be set to a valid GitHub Personal Access Token with the appropriate scopes. For more information see https://docs.github.com/en/migrations/using-github-enterprise-importer/preparing-to-migrate-with-github-enterprise-importer/managing-access-for-github-enterprise-importer#creating-a-personal-access-token-for-github-enterprise-importer"
exit 1
}
else {
Write-Host "GH_PAT environment variable is set and will be used to authenticate to GitHub."
}
# =========== Organization: XXX===========
# === Team Project: XXX/JJL Test ===
Exec { gh ado2gh create-team --target-api-url "https://api.XXX.ghe.com" --github-org "TestMigration3" --team-name "JJL-Test-Maintainers" }
Exec { gh ado2gh create-team --target-api-url "https://api.XXX.ghe.com" --github-org "TestMigration3" --team-name "JJL-Test-Admins" }
Exec { gh ado2gh migrate-repo --target-api-url "https://api.XXX.ghe.com" --ado-org "XXX" --ado-team-project "JJL Test" --ado-repo "JJL Test" --github-org "TestMigration3" --github-repo "JJL-Test-JJL-Test" --target-repo-visibility private }
Exec { gh ado2gh add-team-to-repo --github-org "TestMigration3" --github-repo "JJL-Test-JJL-Test" --team "JJL-Test-Maintainers" --role "maintain" }
Exec { gh ado2gh add-team-to-repo --github-org "TestMigration3" --github-repo "JJL-Test-JJL-Test" --team "JJL-Test-Admins" --role "admin" }
It fails on the gh ado2gh add-team-to-repo step.
When I try directly the failing command (gh ado2gh add-team-to-repo) with --verbose, I get this output:
gh ado2gh add-team-to-repo --github-org "TestMigration3" --github-repo "JJL-Test-JJL-Test" --team "JJL-Test-Maintainers" --role "maintain" --verbose
[2025-07-28 08:02:30] [INFO] You are running an up-to-date version of the ado2gh CLI [v1.17.0]
[2025-07-28 08:02:30] [INFO] GITHUB ORG: TestMigration3
[2025-07-28 08:02:30] [INFO] GITHUB REPO: JJL-Test-JJL-Test
[2025-07-28 08:02:30] [INFO] TEAM: JJL-Test-Maintainers
[2025-07-28 08:02:30] [INFO] ROLE: maintain
[2025-07-28 08:02:30] [INFO] VERBOSE: true
[2025-07-28 08:02:30] [INFO] Adding team to repo...
[2025-07-28 08:02:30] [DEBUG] HTTP GET: https://api.github.com/orgs/TestMigration3/teams
[2025-07-28 08:02:30] [DEBUG] GITHUB REQUEST ID: DAA0:E89A9:7825072:7C8FE6C:68871276
[2025-07-28 08:02:30] [DEBUG] RESPONSE (Unauthorized): {"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}
[2025-07-28 08:02:30] [DEBUG] [HTTP ERROR 401] System.Net.Http.HttpRequestException: GitHub API error: {"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}
---> System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at OctoshiftCLI.Services.GithubClient.SendAsync(HttpMethod httpMethod, String url, Object body, HttpStatusCode expectedStatus, Dictionary`2 customHeaders)
--- End of inner exception stack trace ---
at OctoshiftCLI.Services.GithubClient.SendAsync(HttpMethod httpMethod, String url, Object body, HttpStatusCode expectedStatus, Dictionary`2 customHeaders)
at OctoshiftCLI.Services.GithubClient.<>c__DisplayClass20_0.<<GetWithRetry>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Polly.Retry.AsyncRetryEngine.ImplementationAsync[TResult](Func`3 action, Context context, CancellationToken cancellationToken, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Func`5 onRetryAsync, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider, Boolean continueOnCapturedContext)
[2025-07-28 08:02:30] [ERROR] OctoshiftCLI.OctoshiftCliException: Unauthorized. Please check your token and try again
---> System.Net.Http.HttpRequestException: GitHub API error: {"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}
---> System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at OctoshiftCLI.Services.GithubClient.SendAsync(HttpMethod httpMethod, String url, Object body, HttpStatusCode expectedStatus, Dictionary`2 customHeaders)
--- End of inner exception stack trace ---
at OctoshiftCLI.Services.GithubClient.SendAsync(HttpMethod httpMethod, String url, Object body, HttpStatusCode expectedStatus, Dictionary`2 customHeaders)
at OctoshiftCLI.Services.GithubClient.<>c__DisplayClass20_0.<<GetWithRetry>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Polly.Retry.AsyncRetryEngine.ImplementationAsync[TResult](Func`3 action, Context context, CancellationToken cancellationToken, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Func`5 onRetryAsync, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider, Boolean continueOnCapturedContext)
--- End of inner exception stack trace ---
at OctoshiftCLI.RetryPolicy.<CreateRetryPolicyForException>b__8_1[TException](Exception ex, TimeSpan ts, Context ctx)
at Polly.AsyncRetrySyntax.<>c__DisplayClass22_0.<<WaitAndRetryAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at Polly.Retry.AsyncRetryEngine.ImplementationAsync[TResult](Func`3 action, Context context, CancellationToken cancellationToken, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Func`5 onRetryAsync, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider, Boolean continueOnCapturedContext)
at Polly.AsyncPolicy.ExecuteAsync[TResult](Func`3 action, Context context, CancellationToken cancellationToken, Boolean continueOnCapturedContext)
at OctoshiftCLI.RetryPolicy.Retry[T](Func`1 func)
at OctoshiftCLI.Services.GithubClient.GetWithRetry(String url, Dictionary`2 customHeaders, HttpStatusCode expectedStatus)
at OctoshiftCLI.Services.GithubClient.GetAllAsync(String url, Func`2 resultCollectionSelector, Dictionary`2 customHeaders)+MoveNext()
at OctoshiftCLI.Services.GithubClient.GetAllAsync(String url, Func`2 resultCollectionSelector, Dictionary`2 customHeaders)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
at System.Linq.AsyncEnumerable.<SingleAsync>g__Core|335_0[TSource](IAsyncEnumerable`1 source, Func`2 predicate, CancellationToken cancellationToken) in /_/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Single.cs:line 82
at System.Linq.AsyncEnumerable.<SingleAsync>g__Core|335_0[TSource](IAsyncEnumerable`1 source, Func`2 predicate, CancellationToken cancellationToken) in /_/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Single.cs:line 82
at OctoshiftCLI.Services.GithubApi.GetTeamSlug(String org, String teamName)
at OctoshiftCLI.AdoToGithub.Commands.AddTeamToRepo.AddTeamToRepoCommandHandler.Handle(AddTeamToRepoCommandArgs args)
at OctoshiftCLI.Extensions.CommandExtensions.RunHandler[TArgs,THandler](TArgs args, ServiceProvider sp, CommandBase`2 command)
at OctoshiftCLI.Extensions.CommandExtensions.<>c__DisplayClass1_0`3.<<ConfigureCommand>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Invocation.AnonymousCommandHandler.InvokeAsync(InvocationContext)
at System.CommandLine.Invocation.InvocationPipeline.<>c__DisplayClass4_0.<<BuildInvocationChain>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Builder.CommandLineBuilderExtensions.<>c__DisplayClass17_0.<<UseParseErrorReporting>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Builder.CommandLineBuilderExtensions.<>c__DisplayClass12_0.<<UseHelp>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Builder.CommandLineBuilderExtensions.<>c__DisplayClass22_0.<<UseVersionOption>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Builder.CommandLineBuilderExtensions.<>c__DisplayClass19_0.<<UseTypoCorrections>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Builder.CommandLineBuilderExtensions.<>c.<<UseSuggestDirective>b__18_0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Builder.CommandLineBuilderExtensions.<>c__DisplayClass16_0.<<UseParseDirective>b__0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Builder.CommandLineBuilderExtensions.<>c.<<RegisterWithDotnetSuggest>b__5_0>d.MoveNext()
--- End of stack trace from previous location ---
at System.CommandLine.Builder.CommandLineBuilderExtensions.<>c__DisplayClass8_0.<<UseExceptionHandler>b__0>d.MoveNext()
The token has these permissions:
Based on the verbose output, it seems that this command doesn't use the --target-api-url parameter and is calling the api.github.com instead api.xxx.ghe.com. It is needed to extend it in same way as done in #1365
I am checking the code and I do not understand why the AddTeamToRepo function is part of the ado2gh (ado2gh/Commands). I see it as generic tool, which should be on same level as CreateTeam function (which is in Octoshift/Commands).
Your token is going to need the repo scope. The docs give more info.
It is there.. check the screenshot again. It is checked, just grayed out. If it is not checked, I will not be able to migrate the repo, but the repo is migrated successfully.
The problem is in line:
[2025-07-28 08:02:30] [DEBUG] HTTP GET: https://api.github.com/orgs/TestMigration3/teams
I am using xxx.ghe.com as target (GitHub with data residency).
I'm seeing the same issue - I did find it strange that selecting the admin:org scope caused the repo scope(s) to become inactive but also checked, I think mostly due to the lack of contrast as it is hard to see that it was still checked