Last active
August 21, 2023 20:57
-
-
Save fluffy-cakes/d6cd86d734ce99837551a80e79c6543b to your computer and use it in GitHub Desktop.
azdo_prStats
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
An easy text-based view of how quickly PRs are being approved. | |
.DESCRIPTION | |
This script was born out of the need to help a client understand how quickly we are progressing with PR approvals. | |
It's a handy way to view how a particular repo, or all the repos within an Azure DevOps project is progressing for | |
PR approvals. | |
It will find the longest running PR and set it's text-based graphical view at 100% noted with the "-" for each 1%. | |
All other PRs will be compared against it and show how they fare in % approval reaction time. It will sort the | |
PRs via PR number, which gives representation of progress over time. | |
You can either scope it to the whole project by not defining a particular repo, or if you define one it will scope | |
the view to only that repo. A Days parameter is provided if you want to scope it to show within the las x-days, else | |
it will show by default the last 9999 days. The script will output the required values to a JSON file which can be | |
used in conjunction with PowerBI (great graphic stats are shown using this), else it will output a text file named | |
accordingly with the results. | |
.PARAMETER days | |
(Optional) | |
How many days to pull the stats from. By default it is 9999 days, else you can pass in your own value. Note; it will | |
count the days backwards from exactly the time you ran it. So at 5:00pm minus 2 days will show stats from 2 days ago | |
starting from 5:00pm. It won't start from the begining of the day (something to script in later, perhaps). | |
.PARAMETER includeOld | |
(Optional) | |
Our client had renamed some repos with "zzz_" when they were discontinued. This was included to show stats from these | |
repos, but can be easily changed to a value you desire in the code, or omitted. | |
.PARAMETER organization | |
The organization for which Azure DevOps stats will be pulled from. | |
.PARAMETER project | |
The project within the organization for which Azure DevOps stats will be pulled from. | |
.PARAMETER repoName | |
(Optional) | |
A particular repository for which Azure DevOps stats will be pulled from. | |
.PARAMETER token | |
The Azure DevOps Personal Access Token used to pull the stats. Only required "Code (read)". | |
#> | |
[CmdletBinding()] | |
param ( | |
[int] $days = 9999, | |
[switch]$includeOld, | |
[ValidateNotNullOrEmpty()] | |
[string]$organization, | |
[ValidateNotNullOrEmpty()] | |
[string]$project, | |
[string]$repoName, | |
[ValidateNotNullOrEmpty()] | |
[string]$token | |
) | |
$base64token = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(":${token}")) | |
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" | |
$headers.Add("Authorization", "Basic ${base64token}") | |
$headers.Add("Content-Type", "application/json") | |
$url = "https://dev.azure.com/${organization}/${project}/_apis/git/repositories?api-version=7.1-preview.1" | |
$response = Invoke-RestMethod -Uri $url -Method "GET" -Headers $headers | |
$allRepos = @() | |
$statuses = @( | |
"Active" | |
"Completed" | |
) | |
foreach($repo in ($response.value.name | Sort-Object)) { | |
if($repoName -and ($repo -ne $repoName)) { continue } # if passed in repo name to check, skip those that don't match | |
if((-not $includeOld) -and ($repo -match "^zzz_")) { continue } # if option to include old is not check, then skip those matching zzz_ | |
$thisRepo = [PSObject]@{ | |
"repo" = $repo | |
} | |
foreach($status in $statuses) { | |
$repoMultiArray = @() | |
$url = "https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repo}/pullrequests?searchCriteria.status=${status}&api-version=7.1-preview.1" | |
$response = Invoke-RestMethod -Uri $url -Method "GET" -Headers $headers | |
foreach($pr in ($response.value | Sort-Object -Property "pullRequestId")) { | |
Write-Host "${repo} $($pr.pullRequestId): $($status.ToUpper())" | |
if($pr.isDraft) { | |
Write-Host "`t^ `"isDraft`", skipping" | |
continue | |
} | |
if(($pr.closedDate -lt (Get-Date).AddDays(-${days})) -and ($status -eq "Completed")) { | |
continue | |
} | |
$prObject = [PSObject]@{ | |
"repo" = $repo | |
"id" = $pr.pullRequestId | |
"created" = $pr.creationDate | |
} | |
switch($status) { | |
"Active" { | |
$prObject.Add("closed", $pr.closedDate) | |
$prObject.Add("hours" , ([math]::Round((New-TimeSpan -Start $pr.creationDate -End (Get-Date)).TotalHours))) | |
} | |
"Completed" { | |
$prObject.Add("closed", $pr.closedDate) | |
$prObject.Add("hours" , ([math]::Round((New-TimeSpan -Start $pr.creationDate -End $pr.closedDate).TotalHours))) | |
} | |
} | |
# Required reviewers | |
$url = "https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repo}/pullRequests/$($pr.pullRequestId)/reviewers?api-version=7.1-preview.1" | |
$response = Invoke-RestMethod -Uri $url -Method "GET" -Headers $headers | |
$requirement = $response.value | Where-Object { $_.isRequired -eq $true } | |
$reviewers = @() | |
foreach($reviewer in $requirement) { | |
$thisPerson = $reviewer.displayName.Replace("[${project}]\", "") | |
if($thisPerson -notin $reviewers) { | |
$reviewers += $thisPerson | |
} | |
} | |
$prObject | Add-Member -NotePropertyName "reviewers" -NotePropertyValue $reviewers | |
# Comment count per PR | |
$url = "https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repo}/pullRequests/$($pr.pullRequestId)/threads?api-version=7.1-preview.1" | |
$response = Invoke-RestMethod -Uri $url -Method "GET" -Headers $headers | |
$comments = $response.value | Where-Object { $_.comments[0].commentType -eq "text" } | |
$stats = @() | |
foreach($comment in $comments) { | |
foreach($subcomment in $comment.comments) { | |
if ($subcomment.isDeleted) { continue } # skip deleted comments | |
$user = $subcomment.author.displayName | |
if ($stats.Count -eq 0) { | |
$stats += ,@($user, 1) | |
} else { | |
foreach ($s in $stats) { | |
$statsValue = 0 | |
if ($s[0] -eq $user) { | |
$s[1]++ | |
break | |
} else { | |
$statsValue = 1 | |
} | |
} | |
if ($statsValue -eq 1) { | |
$stats += ,@($user, 1) | |
$statsValue = 0 | |
} | |
} | |
} | |
} | |
$statArray = @() | |
foreach($stat in $stats) { | |
$obj = [PSObject]@{ | |
$stat[0] = $stat[1] | |
} | |
$statArray += $obj | |
} | |
$prObject | Add-Member -NotePropertyName "comments" -NotePropertyValue $statArray | |
$repoMultiArray += $prObject | |
} | |
$thisStatus = [PSObject]@{ | |
"pr_count" = $repoMultiArray.Count | |
"pr_average" = ([math]::Round(($repoMultiArray.hours | Measure-Object -Average).Average)) | |
"prs" = $repoMultiArray | |
} | |
$thisRepo | Add-Member -NotePropertyName "pr_${status}" -NotePropertyValue $thisStatus | |
} | |
$allRepos += $thisRepo | |
} | |
if($repoName) { | |
$projectFile = "${project}_${repoName}" | |
} | |
else { | |
$projectFile = "${project}_all" | |
} | |
## | |
# Output results into JSON file that can be used for PowerBI | |
## | |
$allRepos | ConvertTo-Json -Depth 100 | Out-File -FilePath "./${projectFile}.json" -Force | |
## | |
# Output results into text file | |
## | |
$fileName = "prTime_${projectFile}.txt" | |
Write-Output "$(Get-Date -Format "dddd yyyy/MM/dd HH:mm") $((Get-TimeZone).Id)" | Out-File $fileName | |
if($repoName) { | |
Write-Output "`n$($repoName.ToUpper())" | Out-File -Append $fileName | |
} | |
Write-Output "Check for the last ${days} days`n" | Out-File -Append $fileName | |
foreach($type in "pr_Active","pr_Completed") { | |
Write-Output "`n$($type.ToUpper())" | Out-File -Append $fileName | |
$max = ($allRepos.${type}.prs.hours | Measure-Object -Maximum).Maximum | |
foreach($pr in ($allRepos.${type}.prs | Sort-Object -Property "id")) { | |
if($pr.hours -ne 0.0) { | |
$percent = [math]::Round(($pr.hours/$max)*100, 2) | |
$percentString = "$("-"*$percent)" | |
} | |
else { | |
$percentString = "" | |
} | |
$arrayThis = @($pr.repo.PadRight(32, " "), $pr.id.ToString().PadRight(6, " "), $percentString, "$($pr.hours) hrs") | |
$output = $arrayThis -join " " | |
Write-Output $output | Out-File -Append $fileName | |
} | |
} | |
foreach($repo in $allRepos) { | |
Write-Output "`n$($repo.repo)" | Out-File -Append $fileName | |
Write-Output "`tActive Count : $($repo.pr_Active.pr_count)" | Out-File -Append $fileName | |
Write-Output "`tActive Avg Hrs : $($repo.pr_Active.pr_average)" | Out-File -Append $fileName | |
Write-Output "`t ~~~" | Out-File -Append $fileName | |
Write-Output "`tCompleted Count : $($repo.pr_Completed.pr_count)" | Out-File -Append $fileName | |
Write-Output "`tCompleted Avg Hrs: $($repo.pr_Completed.pr_average)`n" | Out-File -Append $fileName | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Friday 2022/04/22 08:31 GMT Standard Time | |
THISREPO1 | |
Check for the last 9999 days | |
PR_ACTIVE | |
thisRepo1 11618 ---------------------------------------------------------------------------------------------------- 549 hrs | |
thisRepo1 11759 ------------------------------------------------------------------------- 403 hrs | |
thisRepo1 14336 ------- 41 hrs | |
PR_COMPLETED | |
thisRepo1 10665 ------------------------------------------------- 166 hrs | |
thisRepo1 10954 ------ 19 hrs | |
thisRepo1 11164 - 3 hrs | |
thisRepo1 11240 - 3 hrs | |
thisRepo1 11252 0 hrs | |
thisRepo1 11255 0 hrs | |
thisRepo1 11399 ----- 18 hrs | |
thisRepo1 11440 1 hrs | |
thisRepo1 11447 ------- 24 hrs | |
thisRepo1 11521 0 hrs | |
thisRepo1 11602 ----- 17 hrs | |
thisRepo1 11604 ------------- 45 hrs | |
thisRepo1 11637 0 hrs | |
thisRepo1 11653 ---------------------------- 96 hrs | |
thisRepo1 11668 ----------------------------- 99 hrs | |
thisRepo1 11723 - 2 hrs | |
thisRepo1 11767 ---------------------------------------------------------------------------------------------------- 337 hrs | |
thisRepo1 11908 ------------------------------------------ 142 hrs | |
thisRepo1 12276 0 hrs | |
thisRepo1 12288 ------ 19 hrs | |
thisRepo1 12325 0 hrs | |
thisRepo1 | |
Active Count : 3 | |
Active Avg Hrs : 331 | |
~~~ | |
Completed Count : 21 | |
Completed Avg Hrs: 47 | |
i needed to switch around the condition from "if Active or Completed was created in the last 14 days" to "i want ALL the Actives regardless, and ONLY Completed if it's been closed within the last x days"
Do you have a similar script for Github?
Do you have a similar script for Github?
i do not
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated/fixed user comment stats count. Was only counting the first comment and not its threaded comments, too. Now it counts all that have not been deleted.