Skip to content

Instantly share code, notes, and snippets.

@johannesprinz
Last active June 13, 2024 06:55
Show Gist options
  • Save johannesprinz/d60e764890a588c0cccdfeca61a06747 to your computer and use it in GitHub Desktop.
Save johannesprinz/d60e764890a588c0cccdfeca61a06747 to your computer and use it in GitHub Desktop.
Migrating GitLab issues + history to Azure DevOps

Migrate-GitlabToDevops

Dependencies

You will need an access token from azure devops. Get yours here. This script will need contributor access to the project in question.

How to run

# populate user map in Map-IssueToWorkitem
# Load all functions in functions.ps1 into scope
# Setup global variables as per below
$DevOpsAccessToken = 'get yours here https://dev.azure.com/datainsight/_usersSettings/tokens'
$GitLabAccessToken = 'get yours here https://gitlab.com/profile/personal_access_tokens'
$DevOpsOrganization = 'this is the path segmment right after https://dev.azure.com/ you use to access your devops instance ie: myorg'
$DevOpsProjectId = 'get yours here https://dev.azure.com/$DevOpsOrganization/_apis/projects'
$GitlabContainerPath = 'either "groups/groupID" get yours here https://gitlab.com/api/v4/groups or "projects/projectId" get yours here https://gitlab.com/api/v4/projects;'
# Now run all the steps one by one in process.ps1
function Get-GitlabIssues {
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.String]
$AccessToken,
[ValidateNotNullOrEmpty()]
[System.String]
$ContainerPath)
Begin {
$issues = $null;
$result = $null;
$page = 1;
Write-Progress -Activity "Get-GitlabIssues" -Status "0 Complete:" -PercentComplete 0;
}
Process {
$total = (Invoke-RestMethod -Method Get -Uri "https://gitlab.com/api/v4/$ContainerPath/issues_statistics" -Headers @{ 'Authorization' = "Bearer $AccessToken" }).statistics.counts.all;
while($result -ne 0) {
$result = Invoke-RestMethod -Method Get -Uri "https://gitlab.com/api/v4/$ContainerPath/issues?page=$page&per_page=100" -Headers @{ 'Authorization' = "Bearer $AccessToken" }
$issues += $result;
if($issues.Count -eq 0) {
$count = 1;
} else {
$count = $issues.Count;
}
$percent = $count/$total*100;
$page ++;
Write-Progress -Activity "Get-GitlabIssues" -Status "$percent% Complete:" -PercentComplete $percent;
}
}
End {
return $issues;
}
}
function Get-GitlabComments {
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.String]
$AccessToken,
[parameter(Mandatory=$true, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[System.String]
$NotesLink)
Begin {
}
Process {
try{
Invoke-RestMethod -Method Get -Uri "$NotesLink`?page=$page&per_page=100" -Headers @{ 'Authorization' = "Bearer $AccessToken" };
} catch {
$issueIid = $NotesLink.Split("/") | Select-Object -Last 2 | Select-Object -First 1
Write-Error -Message "Failed to get comments for issue: $issueIid" -ErrorId 500 -TargetObject $issueIid -Category InvalidResult;
}
}
End {
}
}
function Get-GitLabAttachment {
[CmdletBinding(
SupportsShouldProcess,
ConfirmImpact="Medium"
)]
param (
[parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[System.String]
$AccessToken,
[parameter(Mandatory)]
[System.String]
$Root,
[parameter(Mandatory)]
[System.String]
$Issue,
[parameter(Mandatory)]
[System.String]
$File)
Begin {
}
Process {
$path = Join-Path -Path $Root -ChildPath $Issue | Join-Path -ChildPath "Attachments";
New-Item -Path $path -Type Directory -Force | Out-Null;
$path = $path | Join-Path -ChildPath ("~"+([string]::Join("~", ($File.Split("/") | Select-Object -Last 3))));
if(-Not (Test-Path -Path $path)) {
Try {
Invoke-WebRequest -OutFile $path -Uri $File -Headers @{ 'Authorization' = "Bearer $AccessToken" };
} Catch {
Write-Error -Message "Failed to get attachment: $File for issue: $Issue" -ErrorId 500 -TargetObject $Issue -Category InvalidResult;
}
}
}
End {
}
}
function Add-DevopsWorkItem {
[CmdletBinding(
SupportsShouldProcess=$true,
ConfirmImpact="Medium"
)]
param (
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.String]
$AccessToken,
[ValidateNotNullOrEmpty()]
[System.String]
$Organization,
[ValidateNotNullOrEmpty()]
[System.String]
$Project,
[ValidateNotNullOrEmpty()]
[System.String]
[ValidateSet("Epic", "Feature", "User Story", "Bug", "Issue", "Task")]
$Type,
[System.Int32]
$Id,
[parameter(Mandatory=$true,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[System.String]
$Data,
[Switch]
$Test)
Begin {
$basicAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f '', $AccessToken)))
$uri = "https://dev.azure.com/$Organization/$Project/_apis/wit/workitems/`$$($Type)?api-version=5.1&bypassRules=true&suppressNotifications=true";
}
Process {
if ($PSCmdlet.ShouldProcess($Data)) {
Try {
if($Test) {
Invoke-RestMethod -Method Patch -Uri $uri+"&validateOnly=true" -Headers @{ 'Authorization' = "Basic $basicAuth" } -ContentType 'application/json-patch+json' -Body $Data
} else {
Invoke-RestMethod -Method Patch -Uri $uri -Headers @{ 'Authorization' = "Basic $basicAuth" } -ContentType 'application/json-patch+json' -Body $Data
}
} Catch {
Write-Error -Message "Failed to add issue for issue: $($Id)" -ErrorId 500 -TargetObject $Data -Category InvalidResult;
}
}
}
End {
}
}
function Add-DevopsComment {
[CmdletBinding(
SupportsShouldProcess=$true,
ConfirmImpact="Medium"
)]
param (
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.String]
$AccessToken,
[ValidateNotNullOrEmpty()]
[System.String]
$Organization,
[ValidateNotNullOrEmpty()]
[System.String]
$Project,
[ValidateNotNullOrEmpty()]
[System.Int32]
$Id,
[parameter(Mandatory=$true,ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[System.String]
$Data)
Begin {
$basicAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f '', $AccessToken)))
$uri = "https://dev.azure.com/$Organization/$Project/_apis/wit/workItems/$Id/comments?api-version=5.1-preview.3";
}
Process {
if ($PSCmdlet.ShouldProcess($Data)) {
Try{
Invoke-RestMethod -Method Post -Uri $uri -Headers @{ 'Authorization' = "Basic $basicAuth" } -ContentType 'application/json' -Body $Data
} Catch {
Write-Error -Message "Failed to add comments for issue: $($Id)" -ErrorId 500 -TargetObject $Id -Category InvalidResult;
}
}
}
End {
}
}
function Add-DevopsAttachment {
[CmdletBinding(
SupportsShouldProcess=$true,
ConfirmImpact="Medium"
)]
param (
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.String]
$AccessToken,
[ValidateNotNullOrEmpty()]
[System.String]
$Organization,
[ValidateNotNullOrEmpty()]
[System.String]
$Project,
[parameter(Mandatory=$true,ValueFromPipeline)]
[System.IO.FileInfo]
$File)
Begin {
$basicAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f '', $AccessToken)));
$uri = "https://dev.azure.com/$Organization/$Project/_apis/wit/attachments?api-version=5.0";
}
Process {
if ($PSCmdlet.ShouldProcess($FileName)) {
Try{
$image = [System.IO.File]::ReadAllBytes($File.FullName);
Invoke-RestMethod -Uri $uri+"&fileName=$([System.Web.HttpUtility]::UrlEncode($File.Name))" -Method Post -Headers @{ Authorization=("Basic {0}" -f $basicAuth) } -ContentType application/json -Body $image;
} Catch {
Write-Error -Message "Failed to add attachments for issue: $($File.FullName)" -ErrorId 500 -TargetObject $File -Category InvalidResult;
}
}
}
End {
}
}
function Convert-IssueToWorkItem {
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '', Scope='Function')]
[CmdletBinding()]
[OutputType([string])]
param (
[parameter(Mandatory=$true)]
$RootIterationPath,
[parameter(Mandatory=$true,ValueFromPipeline)]
$Issue
)
Begin {
Import-Module MarkdownToHtml -Force;
$userMap = @{
#https://gitlab.com/api/v4/groups/mygroupid/members GitLabUserUserId="AzureDevOpsDisplayName",
123 ="John Doe";
};
}
Process {
if([String]::IsNullOrWhiteSpace($Issue.description)) {
$content = "`" `"";
} else {
$content = ((Convert-MarkdownToHTMLFragment -Markdown ($Issue.description | Format-BadCharacters)).HtmlFragment).Replace("₹", "₹").Replace("'", "'") | ConvertTo-Json;
}
$title = [System.Web.HttpUtility]::HtmlEncode(($Issue.title | Format-BadCharacters));
if($title.Length -gt 255){$title = $title.Substring(0,247);}
$author = $($userMap.GetEnumerator() | Where-Object{$_.Name -eq $Issue.author.id}).Value;
$assignee = ($userMap.GetEnumerator() | Where-Object{$_.Name -eq $Issue.assignee.id}).Value;
$closedby = ($userMap.GetEnumerator() | Where-Object{$_.Name -eq $Issue.closed_by.id}).Value;
$tags = [String]::Join(";", $Issue.labels+$Issue.state);
if($Issue.milestone) {
$iteration = $RootIterationPath+"\\"+$Issue.milestone.title.Replace("?", "").Replace("&", "and");
} else {
$iteration = $RootIterationPath;
}
if($null -ne $Issue.assignee){
$user = ($userMap.GetEnumerator() | Where-Object{$_.Name -eq $Issue.assignee.id}).Value;
$assignee = ",
{
`"op`": `"add`",
`"path`": `"/fields/System.AssignedTo`",
`"from`": null,
`"value`": `"$($user)`"
}"
} else {
$assignee = "";
}
$type = @("User Story", "Bug")[$Issue.labels.Contains("Bug")];
if($type -eq "Bug") {
$body = ",
{
`"op`": `"add`",
`"path`": `"/fields/Microsoft.VSTS.TCM.ReproSteps`",
`"from`": null,
`"value`": $content
}"
} else {
$body = ",
{
`"op`": `"add`",
`"path`": `"/fields/System.Description`",
`"from`": null,
`"value`": $content
}"
}
if($Issue.state -eq "closed"){
$closed = ",
{
`"op`": `"add`",
`"path`": `"/fields/Microsoft.VSTS.Common.ClosedBy`",
`"from`": null,
`"value`": `"$($closedby)`"
},
{
`"op`": `"add`",
`"path`": `"/fields/Microsoft.VSTS.Common.ClosedDate`",
`"from`": null,
`"value`": `"$($Issue.closed_at)`"
}";
} else {
$closed = "";
}
return "[
{
`"op`": `"add`",
`"path`": `"/fields/System.IterationPath`",
`"from`": null,
`"value`": `"$($iteration)`"
},
{
`"op`": `"add`",
`"path`": `"/fields/System.Title`",
`"from`": null,
`"value`": `"$($Issue.iid): $($title)`"
}$($body),
{
`"op`": `"add`",
`"path`": `"/fields/System.State`",
`"from`": null,
`"value`": `"$(@{"opened"="New"; "closed" = "New"}[$Issue.state])`"
},
{
`"op`": `"add`",
`"path`": `"/fields/System.Tags`",
`"from`": null,
`"value`": `"$($tags)`"
},
{
`"op`": `"add`",
`"path`": `"/fields/System.CreatedBy`",
`"from`": null,
`"value`": `"$($author)`"
},
{
`"op`": `"add`",
`"path`": `"/fields/System.CreatedDate`",
`"from`": null,
`"value`": `"$($Issue.created_at)`"
}$($assignee)$($closed)
]";
}
End {
}
}
function Convert-NoteToComment {
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '', Scope='Function')]
[CmdletBinding()]
[OutputType([string])]
param(
[parameter(Mandatory,ValueFromPipeline)]
$Note
)
Begin {
Import-Module MarkdownToHtml -Force;
}
Process {
$header = "<p><b>$($Note.created_at.ToLocalTime().ToString('yyyy-MM-dd HH:mm:ss')) by $($Note.author.name)</b></p>";
if([String]::IsNullOrWhiteSpace($Note.body)) {
$content = "`" `"";
} else {
$content = ($header + (Convert-MarkdownToHTMLFragment -Markdown ($Note.body | Format-BadCharacters)).HtmlFragment).Replace("₹", "&#x20b9;") | ConvertTo-Json;
}
return "{ `"text`": $content }";
}
End {
}
}
function Format-BadCharacters {
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseBOMForUnicodeEncodedFile', '', Scope='Function')]
[CmdletBinding()]
[OutputType([string])]
param (
[parameter(
Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName,
Position = 0)]
[string] $Input
)
Process {
return $Input.Replace("`“", "`"").Replace("`”", "`"").Replace(" ", " ").Replace("ä", "a").Replace("ë", "e");
}
}
#Requires -PSEdition Core
#Requires -Modules @{ ModuleName="GitLab"; ModuleVersion="0.0"; }, @{ ModuleName="AzureDevops"; ModuleVersion="0.0"; }, @{ ModuleName="GitLabDevOpsMigration"; ModuleVersion="0.0"; }, @{ ModuleName="MarkdownToHtml"; ModuleVersion="2.0.0"}
throw "This is purely here so you dont accidentaly trigger this script, it is not designed to run end to end in one step, Please read the instructions"
### Setup working directory
$root = Join-Path -Path $(Get-Location) -ChildPath "MigrationWorkSpace"
New-Item -Path $root -Type Directory -Force;
### Step 1 - Download all GitLab Issues
$issues = Get-GitlabIssues -AccessToken $GitLabAccessToken -ContainerPath $GitlabContainerPath;
### Step 2 - Had to run this a few times so I always kept track of migrated ids in this file
$MigratedIssueSucessPath = $root | Join-Path -ChildPath "GtDIssueSucess.txt";
if(Test-Path -Path $MigratedIssueSucessPath) {
$migratedIssueIds = Get-Content $MigratedIssueSucessPath | ForEach-Object{[int64]::Parse($_)}
} else { $migratedIssueIds = @()}
$issuesToMigrate = $issues | Where-Object{ -not $migratedIssueIds.Contains($_.id)}
### Step 3 - Save all issues into a folder
$issuesToMigrate | ForEach-Object {
$folder = Join-Path -Path $root -ChildPath $_.id;
New-Item -Path $folder -Type Directory -Force | Out-Null;
$_ | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $folder -ChildPath issue.json) -Encoding utf8 -Force};
### Step 4 - Get Unique Milestones and their status
### These need to be created as sprints in devops manualy, I only had to create about a dozen so was easy enough
$issuesToMigrate | Select-Object -ExpandProperty milestone | Select-Object title, state -Unique | Sort-Object state, name
### Step 5 - Download Comments
$total = $issuesToMigrate.Count;
$counter = 1;
$issuesToMigrate | Sort-Object id | ForEach-Object -Begin {
Write-Progress -Activity "Get-GitlabComments" -Status "0% Complete:" -PercentComplete 0;
} -Process {
$percent = $counter/$total*100;
$counter++;
Write-Progress -Activity "Get-GitlabComments" -Status "$percent% Complete: $($_.id)" -PercentComplete $percent;
$folder = Join-Path -Path $root -ChildPath $_.id;
Get-GitlabComments -AccessToken $GitLabAccessToken -NotesLink $_._links.notes |
ConvertTo-Json |
Out-File -FilePath (Join-Path -Path $folder -ChildPath notes.json) -Encoding utf8 -Force;
} -End {
Write-Progress -Activity "Get-GitlabComments" -Status "100% Complete:" -PercentComplete 100 -Completed
}
### Step 6 - Get all attachment links
$files = @();
#### Get attachment links from issues
Get-ChildItem -Path $root |
ForEach-Object { $id = $_.Name;
Get-ChildItem -Path $_.FullName -Filter issue.json |
Get-Content -Raw |
ConvertFrom-Json |
Where-Object { -not [String]::IsNullOrWhiteSpace($_.description) } |
ForEach-Object {
$urlParts = $_.web_url.Split("/");
$rootPath = [string]::Join("/", ($urlParts | Select-Object -First ($urlParts.Count-2)));
$_.description.Split("`n") |
ForEach-Object { [regex]::Match($_, "\]\((/uploads/.+)\)")} |
Where-Object { $_.Success } |
Select-Object -ExpandProperty Groups |
Where-Object { -Not $_.Value.StartsWith("]")} |
Select-Object -ExpandProperty Value |
ForEach-Object {
$files+=New-Object PSObject -Property @{
issue=$id;
file=$rootPath+$_;}
}
}
}
#### Get attachment links from comments
Get-ChildItem -Path $root |
ForEach-Object {
$id = $_.Name;
$urlParts = (Get-ChildItem -Path $_.FullName -Filter issue.json |
Get-Content -Raw |
ConvertFrom-Json).web_url.Split("/");
$rootPath = [string]::Join("/", ($urlParts | Select-Object -First ($urlParts.Count-2)));
Get-ChildItem -Path $_.FullName -Filter notes.json |
Get-Content -Raw |
ConvertFrom-Json |
Where-Object { -not [String]::IsNullOrWhiteSpace($_.body) } |
ForEach-Object { $_.body.Split("`n") } |
ForEach-Object { [regex]::Match($_, "\]\((/uploads/.+)\)")} |
Where-Object { $_.Success } |
Select-Object -ExpandProperty Groups |
Where-Object { -Not $_.Value.StartsWith("]")} |
Select-Object -ExpandProperty Value |
ForEach-Object {
$files+=New-Object PSObject -Property @{
issue=$id;
file=$rootPath+$_;} }
}
### Step 7 - Downlaod the attachments from gitlab NOTE: This can take while
$total = $files.Count;
$counter = 1;
$files | ForEach-Object -Begin {
Write-Progress -Activity "Get-GitlabAttachments" -Status "0% Complete:" -PercentComplete 0;
} -Process {
$percent = $counter/$total*100;
$counter++;
Write-Progress -Activity "Get-GitlabAttachments" -Status "$percent% Complete: $($_.issue)" -PercentComplete $percent;
Get-GitLabAttachment -AccessToken $GitLabAccessToken -Root $root -Issue $_.Issue -File $_.File;
} -End {
Write-Progress -Activity "Get-GitlabAttachments" -Status "100% Complete:" -PercentComplete 100 -Completed
}
### Step 8 - Upload and map attachments to offlined notes and comments
$total = (Get-ChildItem -Path $root).Count;
$counter = 1;
Get-ChildItem -Path $root |
ForEach-Object -Begin {
Write-Progress -Activity "Map-Attachments" -Status "0% Complete:" -PercentComplete 0;
} -Process {
$percent = $counter/$total*100;
$counter++;
Write-Progress -Activity "Map-Attachments" -Status "$percent% Complete: $($_.Name)" -PercentComplete $percent;
Push-Location -Path $_.FullName;
if(Test-Path -Path Attachments) {
$replace = Get-ChildItem -Path Attachments | ForEach-Object {
$name = $_ | Select-Object -Property @{Name="Name";Expression={$_.Name.Replace("~","/")}} | Select-Object -ExpandProperty Name;
$upld = Add-DevopsAttachment -AccessToken $DevOpsAccessToken -Organization $DevOpsOrganization -Project $DevOpsProjectId -File $_;
Write-Verbose "$($name) $($upld.url)" -Verbose;
@{Old=$name; New=$upld.url};
}
$replace | ForEach-Object {
$old = $_.Old;
$new = $_.New;
Get-ChildItem -File | ForEach-Object {
$fileName = $_.FullName;
$content = Get-Content $_ | ForEach-Object {
$_.Replace($old, $new);
};
$content | Out-File -FilePath $fileName -Encoding utf8 -Force;
}
}
}
Pop-Location;
} -End {
Write-Progress -Activity "Map-Attachments" -Status "100% Complete:" -PercentComplete 100 -Completed
}
###
# At this stage I had to do some manual fixes sometimes, important at this stage to re-load all the issues and commments from the filesystem as they have the updated data after uploading all the attachments
###
$issuesToMigrate = Get-ChildItem -Path $root -Filter issue.json -Recurse | Get-Content -Raw | ConvertFrom-Json
### Step 9 - Issue + Comment Upload
$failed = @();
$total = $issuesToMigrate.Count;
$test=$true; # This will allow you to validate before import, set to false for actual import
$counter = 1;
#### The Sort helped me judge how far into the process I was
$issuesToMigrate | Sort-Object id |
ForEach-Object -Begin {
Write-Progress -Activity "Create Work Items and comments" -Status "0% Complete:" -PercentComplete 0;
} -Process {
$percent = $counter/$total*100;
$counter++;
$id = $_.id;
### Note the new bug template needs to map the content to another field other than description, think it was called reposteps or similar
$type = @("User Story", "Bug")[$_.labels.Contains("Bug")];
Write-Progress -Activity "Create Work Items and comments" -Status "$percent% Complete: $($id)" -PercentComplete $percent;
$_ | Convert-IssueToWorkItem -RootIterationPath "Squad.Umenit" | ForEach-Object{
$workItem = $null;
$workItem = Add-DevopsWorkItem -Test:$test -AccessToken $DevOpsAccessToken -Organization $DevOpsOrganization -Project $DevOpsProjectId -Id $id -Type $type -Data $_;
if($null -eq $workItem){
$failed += @{$id=$_}
} else {
if(-not $test) {
$comments = Get-Content -Path ($root | Join-Path -ChildPath $id | Join-Path -ChildPath "notes.json") -Raw | ConvertFrom-Json | Sort-Object -Property created_at;
$comments.GetEnumerator() | Convert-NoteToComment | Add-DevopsComment -AccessToken $DevOpsAccessToken -Organization $DevOpsOrganization -Project $DevOpsProjectId -Id $workItem.id | Out-Null;
}
}
}
} -End {
Write-Progress -Activity "Create Work Items and comments" -Status "100% Complete:" -PercentComplete 100 -Completed
}
@NielsSkytte
Copy link

Hi, trying this script for migrating from Gitlab to Devobs. Got the first parts working I think, but you write
#populate user map in Map-IssueToWorkitem
I'm guessing its Convert-IssueToWorkitem you mean? And what exactly do I need to change?

Its
$userMap = @{
#https://gitlab.com/api/v4/groups/mygroupid/members GitLabUserUserId="AzureDevOpsDisplayName",
123 ="John Doe";
};

so in my case I would replace the gitlab.com with our own and have set the group id where the project is in mygroupid. But I'm unsure about the GitLabUserUserID and why it says AzureDevOpsDisplayName? and what about the 123 John Doe, what should that be replaced with?

Hope you can help :-)

@johannesprinz
Copy link
Author

johannesprinz commented Apr 21, 2020 via email

@NielsSkytte
Copy link

That makes sense :-) If you don't map the users, would it still work? I've having trouble creating the issues in DevObs, when I run step 8 it just says 'Failed to add issue: xxxx' for all the issues that I've downloaded. It might be how 'bugs' are setup i Devobs, haven't looked at it yet so much.....

@johannesprinz
Copy link
Author

johannesprinz commented Apr 23, 2020 via email

@NielsSkytte
Copy link

Was away from this task for a while......having issues with step 7 and step 9 hope you can help :-)

Step 7 downloading attachments works okay, but the actual images being downloaded are all 13 KB and are all invalid when I try to open them locally on my pc (e.g. before uploading).
Step 9 creating the actual issues in DevOps fails for all issues.

For Step 7 the attachments seems to be invalid bitmaps, both on my local pc and the version uploaded to DevOps if I try to download that directly.

For Step 9 I can't seem to find the problem. Looked at the code and wondered a bit about this line 170 in process.ps:
$_ | Convert-IssueToWorkItem -RootIterationPath "Squad.Umenit" | ForEach-Object{

what is "Squad.Umenit" guessing its something hardcoded? From the code I would guess its related to Iteration/sprint, but not quite sure how.?

Your help is much appreciated as I can't find any alternative to doing this kind of migration :-)

@johannesprinz
Copy link
Author

johannesprinz commented Jun 25, 2020 via email

@NielsSkytte
Copy link

Super, I'll check the api version and update ps core.

I'm using the Agile template, should be okay as I see it, but I'll have a look again.

I'll give an update later :-)

@phpai
Copy link

phpai commented Jul 1, 2020

thanks, worked like a charm.
maybe someone else encounters this problem, for migrating german text, i had to encode the $Data like this $body = [System.Text.Encoding]::UTF8.GetBytes($Data) in Add-DevopsWorkItem and Add-DevopsComment

@electrominium
Copy link

@johannesprinz thanks for this wonderful post.

I did't run into any issues until stage 9 or I should say did not see any errors. One thing I did have to change was the GitLab URI. I updated it to the following (added projects in the URI path) as the above one did not work. For group, should probably use "groups".

https://gitlab.com/api/v4/projects/$ContainerPath/issues_statistics

I am using Agile and copying 3 test issues from GitLab. When I run the 9th stage, it just says "Failed to create an issue". Not sure how to troubleshoot this issue. Also, if I am reading this right (I am using powershell for the first time as I am a linux user), its trying to write this issue as a "User Story" as its the only workitemtype defined apart from "bug". Can I get some help in this?

@jwebru
Copy link

jwebru commented May 14, 2021

PowerShell is not my programming language, so the questions arose:

  1. Where can I get the "GitLabDevOpsMigration" module (PowerShell 7.1)?
  2. What does it mean to run a script step by step? Move steps from a file into separate scripts?

@johannesprinz
Copy link
Author

@jwebru,

  1. You can get PowerShell core from https://github.com/PowerShell/PowerShell this is different to the native PowerShell that comes with windows, it is important to use the core version as it uses consistent file encoding when working with json files.
    • You know you have it right when you use pwsh --version and get PowerShell 7.0.3 or better.
  2. paste the process commands into a terminal one by one
    • To fully understand what it does
    • and to fix any intermittent failures

This was a one off process for me so many hard coded elements, once I was done there was no need to maintain and make more generic, but I had backed it up in GIST for that "just in case" moment and turns out it was useful for a few other user. Yay :D

@MieMieMieeeee
Copy link

@johannesprinz
Hi, I'm interested in using the code you've shared. It works wells in my situation and I want to share my code with other coworks in my company. However, I noticed that there is no explicit license mentioned in the Gist.

Could you please clarify the licensing terms for your code? I want to make sure I respect your rights as the original author and abide by any conditions you may have regarding the use of your code.
Thank you for your time, and I appreciate your work!

@johannesprinz
Copy link
Author

@MieMieMieeeee this is open to use on which ever way it helps. Feel free to modify and share as required.

@zaakiy
Copy link

zaakiy commented Apr 25, 2024

@johannesprinz If I change the URL from GitLab.com to my self-hosted GitLab, will it still work?

@johannesprinz
Copy link
Author

johannesprinz commented Apr 25, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment