Skip to content

Instantly share code, notes, and snippets.

@tamirs9876
Created November 3, 2024 09:23
Show Gist options
  • Save tamirs9876/4f5e7db9eb68058ba5bfdaadf9342d65 to your computer and use it in GitHub Desktop.
Save tamirs9876/4f5e7db9eb68058ba5bfdaadf9342d65 to your computer and use it in GitHub Desktop.
Exports Azure DevOps Pull Requests and Threads (comments) discussion.
<#
NOTES:
1. This script export *completed* pull requests and their comments while ignoring active/abandon pull requests
2. Pull request description is truncated after 400 symbols, as per Azure DevOps API documentation.
3. Linked Work Items are not exported.
4. The script uses Azure CLI to get the access token. It is also possible to use PAT from Azure DevOps.
#>
param (
[Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $OrganizationName,
[Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $RepositoryName,
[Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()] $DumpDir = "dump"
)
# Stop the script when a cmdlet or a native command fails
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$ApiVersion = "7.1"
$OrgUri = "https://dev.azure.com/${OrganizationName}/"
$BearerToken = $(az account get-access-token --query accessToken --output tsv)
$AzureDevOpsAuthenicationHeader = @{Authorization = "Bearer ${BearerToken}" }
function CallApi($Uri) {
for ($i = 0; $i -lt 3; $i++) {
try {
return (Invoke-RestMethod -Uri $Uri -Method get -Headers $AzureDevOpsAuthenicationHeader).value
}
catch {
if ($i -ge 2) {
Write-Output "Failed to call: ${Uri}"
Write-Output "Error message: $($_.Exception.Message)"
exit 1
}
Start-Sleep -Seconds 3
}
}
}
function GetFileNameForPullRequest($Pr) {
# Avoid potentially invalid file name characters
$Title = $Pr.title -replace "[^a-zA-Z0-9 ]", "."
$Id = $Pr.pullRequestId
$Date = $Pr.closedDate.ToString("yyyyMMdd")
return "${Date}-${Id}-${Title}"
}
# Get repo Id (required for the next API calls)
$GetRepoIdUri = "${OrgUri}_apis/git/repositories/?api-version=${ApiVersion}"
$RepositoryId = (CallApi $GetRepoIdUri | Where-Object { $_.name -ieq $RepositoryName })[0].id
$Top = 100
$Skip = 0
$Total = 0
while ($true) {
# Get completed pull requests
$GetPRsUri = "${OrgUri}_apis/git/repositories/${RepositoryId}/pullrequests/?`$top=${Top}&`$skip=${Skip}&searchCriteria.status=completed&api-version=${ApiVersion}"
$CompletedPullRequests = CallApi $GetPRsUri
if (-not (Test-Path $DumpDir)) {
New-Item -ItemType Directory -Path $DumpDir | Out-Null
}
foreach ($Pr in $CompletedPullRequests) {
$PrFileName = GetFileNameForPullRequest $Pr
$Pr | ConvertTo-Json -Depth 100 | Out-File -Path "${DumpDir}/${PrFileName}.json"
# Get PR comments
$GetCommentsUri = "${OrgUri}_apis/git/repositories/${RepositoryId}/pullRequests/$($Pr.pullRequestId)/threads?api-version=${ApiVersion}"
$Comments = CallApi $GetCommentsUri
$Comments | ConvertTo-Json -Depth 100 | Out-File -Path "${DumpDir}/${PrFileName}-comments.json"
}
Write-Verbose "Exporting $($CompletedPullRequests.Count) pull requests completed"
$Total += $CompletedPullRequests.Count
if ($CompletedPullRequests.Count -lt $Top) {
# Last batch processed
break
}
else {
$Skip += $Top
}
}
Write-Output "Total ${Total} pull requests exported to ${DumpDir}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment