Migrate your packages from MyGet to Azure Artifacts

I recently migrated NuGet packages from a private MyGet repository to Azure DevOps Artifacts because it is actually cheaper and if you already have all your build pipelines in DevOps, why not use the artifacts too. Good thing there, you can also include the usual NuGet feeds like NuGet Gallery or npmjs, that way you only need your feed in Visual Studio or your pipelines for restoring all the packages.

So I used the guideline from Microsoft to migrate all my packages, let’s say it wasn’t as easy as I thought.

I ran into multiple issues and the Powershell module wasn’t working at all. Let’s see how I fixed it.

So apparently all you need to do is follow the requirements, import the Powershell module and run these two commands:

$password = ConvertTo-SecureString -String '<your password here>' -AsPlainText -Force

# Migrate packages from a private source feed.
Move-MyGetNuGetPackages -SourceIndexUrl '<your source index url here>' -DestinationIndexUrl '<your destination index url here>' -DestinationPAT '<your destination PAT string here>' -DestinationFeedName '<your destination feed name>' -SourceUsername '<username for source feed>' -SourcePassword $password -Verbose

Well no, that didn’t work at all with the current version. First it wasn’t doing anything, then it gave me a null error and then a 401 unauthorized.

So I downloaded the source from Github and debugged it.

Deadlock

So the first issue I ran into was actually a deadlock while calling the nuget.exe.

The first usage of nuget.exe in the script is trying to add your feed to nuget, so it is trying to run: nuget.exe sources add …

But it didn’t do anything, the script was running and running, not logging any verbose message. First I thought it just takes a while because I have many packages and the whole thing could run for ages but while debugging a quickly saw that it was stuck on trying to add the feed which is a really quick call. I actually ran the command myself and it returned quickly and fine.

So what was going on?

Basically the problem is this:

$process.Start() | Out-Null
$process.WaitForExit()

$return = [pscustomobject]@{
StdOut = $process.StandardOutput.ReadToEnd()
StdErr = $process.StandardError.ReadToEnd()
ExitCode = $process.ExitCode}

You should not call WaitForExit() before StandardOutput.ReadToEnd() because that might cause a deadlock, basically what happened to me. See the Microsoft documentation on this.

So I flipped the code around:

$process.Start() | Out-Null

$output = $process.StandardOutput.ReadToEnd();
$error = $process.StandardError.ReadToEnd();

$process.WaitForExit()

$return = [pscustomobject]@{
StdOut = $output
StdErr = $error
ExitCode = $process.ExitCode
}

Problem solved.

Null error

The second issue I ran into was an null error on this:

$sourceCredential = New-Object -TypeName pscredential -ArgumentList $SourceUsername, $secureSourcePassword

Basically $secureSourcePassword was null, but I did set all the parameters needed in the command for the module. So what was wrong? Let’s have a look at the whole code snippet:

if ($SourcePassword)
{
$sourceCredential = New-Object -TypeName pscredential -ArgumentList $SourceUsername, $secureSourcePassword
}

So it is checking $SourcePassword which is actually the correct parameter, but it is using $secureSourcePassword, which basically is null and never assigned to anything in the whole module. So this is just a bug and shows me that it got never really tested, where for the deadlock it might have worked with luck.

So I also fixed that to:

$sourceCredential = New-Object -TypeName pscredential -ArgumentList $SourceUsername, $SourcePassword

401 – unauthorized

So I got the script running and it did a good job until I saw some packages missing and was checking the logs again. That is when I found a few 401 errors returning from the MyGet feed. I double checked the feed and it was all fine. So I debugged the code again and followed where it was going wrong for those packages throwing the error.

It was failing here:

$null = $result.AddRange((Read-CatalogUrl -RegistrationUrl $catalogUrl))

So it was trying to read the catalog of the package, which I checked worked but what was different. Let’s have a look at the whole code snipped again.

The error was in Read-CatalogEntry:

if ($itemType -eq 'catalog:CatalogPage' -and $null -eq $item.items)
{
$catalogUrl = $Item.'@id'
$null = $result.AddRange((Read-CatalogUrl -RegistrationUrl $catalogUrl))
}
elseif ($itemType -eq 'catalog:CatalogPage')
{
foreach ($subItem in $Item.items)
{
$null = $result.AddRange((Read-CatalogEntry -Item $subItem))
}
}

As you can see it is actually the first if that got hit, so the items where actually null and there were multiple pages for this packages. So why was it giving a 401 when it got a valid return on the first Read-CatalogURL? Well simple if you look at the first call:

$versions = Read-CatalogUrl -RegistrationUrl $registrationUrl -Credential $Credential

Can you spot the difference? Correct this call is using the $Credential where the other one is not, thats why its failing.

So I had to adjust the code to also use the $Credential in the failing call.

$null = $result.AddRange((Read-CatalogEntry -Item $item -Credential $Credential))

After this change it was running fine and I had all packages.

I have created a PR for my changes but so far they didn’t merge it in, so if you want to run the working module you can just use my fork: https://github.com/Marcele1987/azure-artifacts-migration

Leave a Reply

Your email address will not be published. Required fields are marked *