.Notes
Author: GitHub Team
Modified By:
- Brad
Last Modified: 2021-11-05
Changes:
- Initial version.
.Synopsis
Collect project information.
.Description
The Get-ProjectInfo script collects project information.
.Parameter Token
This is a personal access token that has full read access to all required Azure DevOps information.
This currently defaults to a personal access token.
.Example
Get-ProjectInfo;
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$Token = $ENV:SYSTEM_ACCESSTOKEN,
[Parameter(Mandatory=$false)]
[string]$InitialOrganizationName = $global:Account,
[Parameter(Mandatory=$false)]
[string]$ProjectName = $global:ProjectName,
[Parameter(Mandatory=$false)]
[string]$csvExportLocation = "C:\dev\test\AzureDevOps",
[Parameter(Mandatory=$false)]
[bool]$JustScanInitialOrganization = $true,
[Parameter(Mandatory=$false)]
[bool]$JustScanInitialProject = $true,
[Parameter(Mandatory=$false)]
[bool]$GetArtifacts = $false,
[Parameter(Mandatory=$false)]
[bool]$IncludeWorkItems = $false
)
if ($null -eq (Get-Module -Name DevOps)) {Import-Module -Name DevOps -Force -Verbose:$False};
Initialize-Script $PSCmdlet.MyInvocation -ShowStarting;
$authHeader = New-AuthorizationHeader $Token;
# Get all organizations
if (-not $JustScanInitialOrganization)
{
#This is a little dirty, and may fail in the future if Azure DevOps changes the way it lists all organizations
$orgRequestBody = "{
""contributionIds"": [""ms.vss-features.my-organizations-data-provider""],
""dataProviderContext"":
{
""properties"":{}
}
}"
$uri = "https://dev.azure.com/$InitialOrganizationName/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1"
$orgResponse = Invoke-RestMethod -Uri $uri -Body $orgRequestBody -ContentType $global:JsonContent -Headers $authHeader -Method Post -ErrorAction Stop
$orgResponseDetails = $orgResponse.dataProviders
$organzations = $orgResponseDetails."ms.vss-features.my-organizations-data-provider".organizations
}
else
{
#Add just the initial organization to the collection (just looping once)
$organzations = @()
$newOrg = New-Object -TypeName PSObject -Property @{
id = "0";
Name = $InitialOrganizationName;
url = "";
}
$organzations += $newOrg
}
$summary = @();
$artifacts = @();
[System.Collections.ArrayList]$builds = @();
[System.Collections.ArrayList]$releases = @();
$repos = @();
$prs = @();
[System.Collections.ArrayList]$workItems = @();
foreach ($organization in $organzations)
{
$orgSummary = @()
$orgName = $organization.name
Write-PrefixedOutput -MessageFormat "Processing organization: {0}" -TokenValues $orgName;
# Get Artifacts
if ($GetArtifacts -eq $true)
{
#https://feeds.dev.azure.com/{organization}/{project}/_apis/packaging/feeds?api-version=6.0-preview.1
$uri = "https://feeds.dev.azure.com/$orgName/_apis/packaging/feeds?api-version=6.0-preview.1"
try
{
$artifactsJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop
$artifacts += $artifactsJson | ConvertTo-Json | ConvertFrom-Json | Select-Object -Expand Value | Select name
}
catch
{
#do nothing
Write-PrefixedOutput -Message "No access to $orgName artifacts"
}
}
# Get projects for organization
#https://dev.azure.com/{organization}/_apis/projects?api-version=5.1
$uri = "https://dev.azure.com/$orgName/_apis/projects?api-version=5.1"
try
{
$projectsJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop
#Extract just the name and last updated date for each project into a new array
$projects = $projectsJson.value | Foreach-Object {
if ((-not $JustScanInitialProject) -or ($_.name -eq $ProjectName))
{
New-Object -TypeName PSObject -Property @{
Name = $_.name
LastUpdateTime = Get-Date $_.lastUpdateTime
};
}
}
}
catch
{
#do nothing
Write-PrefixedOutput -Message "No access to projects in organization $orgName"
$projects = @{}
}
foreach ($project in $projects)
{
Write-PrefixedOutput -MessageFormat "Processing project: {0}" -TokenValues $project.name;
# Builds for project
$uri = "https://dev.azure.com/$orgName/$($project.name)/_apis/build/builds?api-version=5.1"
try
{
Write-PrefixedOutput -Message "Processing builds";
$buildRunsJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop;
$buildRunsJson.value | Foreach-Object {
$null = $builds.Add((New-Object -TypeName PSObject -Property @{
Name = $_.definition.name
BuildNumber = $_.buildNumber
Status = $_.status
Result = $_.result
QueueTime = Get-Date $_.queueTime
Organization = $orgName
ProjectName = $($project.name)
}));
}
}
catch
{
#do nothing
Write-PrefixedOutput -Message "No access to $orgName $($project.name) builds";
}
Write-ElapsedTime -ElapsedLabel ("Processed {0} builds" -f $builds.Count);
# Releases for project
# https://vsrm.dev.azure.com/{organization}/{project}/_apis/release/releases?api-version=5.1
# GET https://vsrm.dev.azure.com/{organization}/{project}/_apis/release/releases?api-version=6.1-preview.8&$top={$top}&path={path}&sourceBranchFilter={sourceBranchFilter}
$uri = "https://vsrm.dev.azure.com/$orgName/$($project.name)/_apis/release/releases?`$top=500&api-version=6.1-preview.8"
Write-PrefixedOutput -MessageFormat "Processing releases: {0}" -TokenValues $uri;
try
{
# default is 50 at a time
$releasesJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop;
#$releasesJson.value | ForEach-Object {
foreach ($nextRelease in $releasesJson.value)
{
$uri = "https://vsrm.dev.azure.com/$orgName/$($project.name)/_apis/release/releases/$($nextRelease.id)?api-version=5.1";
$releaseJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop;
$environments = $releaseJson.environments | ConvertTo-Json -Depth 5 | Where-Object status -ne "notStarted";
$environmentsObj = $environments | ConvertFrom-Json;
$statuses = $environmentsObj | Select name, status | Where-Object status -ne "notStarted";
$null =$releases.Add((New-Object -TypeName PSObject -Property @{
Id = $nextRelease.id;
Name = $nextRelease.name;
ReleaseDefinition = $nextRelease.releaseDefinition.name;
Status = $nextRelease.status;
CreatedOn = if ($releaseJson -ne $null) {Get-Date $releaseJson.createdon} else { Get-Date };
LastEnvironmentName = if($statuses -ne $null) { $statuses[-1].name } else { "none" };
LastEnvironmentStatus = if($statuses -ne $null) { $statuses[-1].status } else { "none" };
Organization = $orgName
ProjectName = $($project.name)
}));
}
}
catch
{
#do nothing
Write-PrefixedOutput -Message "No access to $orgName $($project.name) releases"
}
Write-ElapsedTime -ElapsedLabel ("Processed {0} releases." -f $releases.Count);
if ($IncludeWorkItems)
{
# Work items for project
[System.Collections.ArrayList]$projectWorkItems = @()
$uri = "https://dev.azure.com/$orgName/$($project.name)/_apis/wit/reporting/workitemrevisions?api-version=5.1&includeDeleted=false"#&includeLatestOnly=true"
Write-PrefixedOutput -Message "Processing work items";
do
{
try
{
$workItemsJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop;
foreach ($workItem in $workItemsJson.values)
{
$null = $projectWorkItems.Add(($workItem | ConvertTo-Json -Depth 10 | ConvertFrom-Json));
}
$uri = $workItemsJson.nextLink
}
catch
{
#do nothing
Write-PrefixedOutput -Message "No access to $orgName $($project.name) work items"
}
} While ($workItemsJson.values.Length -gt 0) #Loop while there are items in the list. Once we reach the end of the list, we will have 0 items
$workItems += $projectWorkItems.values | ConvertTo-Json -Depth 10 | ConvertFrom-Json | Get-Unique -AsString;
Write-ElapsedTime -ElapsedLabel "Processed work items.";
}
Write-PrefixedOutput -Message "Processing repositories";
#Repos for project
#GET https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=6.0
$projectRepos = @()
$uri = "https://dev.azure.com/$orgName/$($project.name)/_apis/git/repositories?api-version=6.0"
try
{
$reposJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop
$projectRepos = $reposJson.value | ConvertTo-Json -Depth 10 | ConvertFrom-Json
}
catch
{
#do nothing
Write-PrefixedOutput -Message "No access to $orgName $($project.name) repos"
}
$repos += $projectRepos | ConvertTo-Json -Depth 10 | ConvertFrom-Json | Get-Unique -AsString
Write-ElapsedTime -ElapsedLabel ("Processed {0} repositories." -f $projectRepos.Count);
Write-PrefixedOutput -Message "Processing TFVC repositories";
#TFVCs in project (note there are no PRs - but should we be counting branches?)
#GET https://dev.azure.com/{organization}/{project}/_apis/tfvc/items?api-version=6.0
$tfvcRepoExists = $false
try
{
$uri = "https://dev.azure.com/$orgName/$($project.name)/_apis/tfvc/items?api-version=6"
$tfvcJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop
$projectTFVCRepos = $tfvcJson.value | ConvertTo-Json -Depth 10 | ConvertFrom-Json
if ($projectTFVCRepos.Count -gt 0)
{
$tfvcRepoExists = $true
}
}
catch
{
#do nothing
Write-PrefixedOutput -Message "No access to $orgName TVFC repos"
}
Write-ElapsedTime -ElapsedLabel "Processed TFVC repositories.";
Write-PrefixedOutput -Message "Processing PRs";
#PRs in each repo
#GET https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?searchCriteria.status=completed&api-version=6.0
#Loop through each Repo for PR's
foreach ($projectRepo in $projectRepos)
{
$skipPRs = 0 # The number of pull requests to ignore. For example, to retrieve results 101-150, set top to 50 and skip to 100.
$topPRs = 100 # The number of pull requests to retrieve.
$uri = "https://dev.azure.com/$orgName/$($project.name)/_apis/git/repositories/$($projectRepo.id)/pullrequests?searchCriteria.status=completed&`$skip=$skipPRs&`$top=$topPRs&api-version=6.0"
$projectReposPRs = @();
$tmp = @();
$doMore = $true;
do
{
try
{
$prsJson = Invoke-RestMethod -Uri $uri -ContentType $global:JsonContent -Headers $authHeader -Method Get -ErrorAction Stop
$projectRepoPRsJson = $prsJson.value | ConvertTo-Json -Depth 10 | ConvertFrom-Json
$tmp = $projectRepoPRsJson | ConvertTo-Json -Depth 10 | ConvertFrom-Json | Get-Unique -AsString
$projectReposPRs += $tmp
$skipPRs += $topPRs
$uri = "https://dev.azure.com/$orgName/$($project.name)/_apis/git/repositories/$($projectRepo.id)/pullrequests?searchCriteria.status=all&`$skip=$skipPRs&`$top=$topPRs&api-version=6.0"
}
catch
{
Write-PrefixedOutput -MessageFormat "{0} repository error: {1}" -TokenValues $projectRepo.id,$_.Exception.Message;
$doMore = $false;
}
} while (($tmp.Count -ge 100) -and $doMore) #Loop while there are items in the list. Once we reach the end of the list, we will have 0 items
$prs += $projectReposPRs;
$orgSummary += (New-Object -TypeName PSObject -Property @{
Organization = $orgName
Project = $project.name
WorkItemCount = $projectWorkItems.values.Count
GitRepo = $projectRepo.name
TVFCRepoExists = $tfvcRepoExists
GitRepoCompressedSizeInMB = "{0:n2}" -f [math]::Round(($projectRepo.size / 1000000),2) # dividing by a million, not exact - but close enough
PRsCount = $(if($projectReposPRs.Count -eq $null) {1} else {$projectReposPRs.Count})
BuildsAndReleasesCount = $builds.Count + $releases.Count
});
}
Write-PrefixedOutput -Message "Scanning project $($project.name)... (found $($repos.Length) Git repos, $(if($tfvcRepoExists -eq $true) {1} else {0}) TFVC repos, $($prs.Length) prs, $($builds.Length) builds, $($releases.Length) releases, and $($workItems.Length) work items found so far)"
} # end Foreach ($project in $projects){
if ($orgSummary.Count -gt 0)
{
$orgSummary | Select-Object Organization, Project, WorkItemCount, TVFCRepoExists, GitRepo, GitRepoCompressedSizeInMB, PRsCount | Export-Csv -Path "$csvExportLocation\AzureDevOpsStats_$orgName.csv" -Encoding ascii -NoTypeInformation
$summary += $orgSummary
}
} # end Foreach ($org in $organzationsJson)
Write-PrefixedOutput -Message "Total builds: $($builds.Count)"
#$builds | Select Name, Status, Result, QueueTime | Group-Object -Property Status, Result | Select Count, Name | ft
Write-PrefixedOutput -Message "Total releases: $($releases.Count)"
#$releases | Select LastEnvironmentStatus | Group-Object -Property LastEnvironmentStatus | Select Count, Name | ft
Write-PrefixedOutput -Message "Total work items: $($workItems.Count)"
#$workItems.fields | Select System.WorkItemType, System.ChangedDate | Group-Object -Property System.WorkItemType | Select Count, Name | ft
if ($GetArtifacts -eq $true)
{
Write-PrefixedOutput -Message "Total artifact feeds: $($artifacts.Count)"
#$artifacts | Select name
}
Write-PrefixedOutput -Message "Total Git repos: $($repos.Count)"
$TotalReposOver2GB = ($repos | Where-Object size -gt $(2 * ([Math]::Pow(1000,3)))) #1000^3 is a billion/or ~1 GB
Write-PrefixedOutput -Message "Total Git repos over 2GB: $($TotalReposOver2GB.Count)"
$TotalReposOver2GB | Select name, size | ft
Write-PrefixedOutput -Message "Total PRs: $($prs.Count)"
Write-PrefixedOutput -Message "Summary"
$summary | ft Organization, Project, WorkItemCount, TVFCRepoExists, GitRepo, @{n='GitRepoCompressedSizeInMB';e={$_.GitRepoCompressedSizeInMB};align='right'}, PRsCount #, BuildsAndReleasesCount
Write-ElapsedTime;
Write-Output ("{0}: Finishing: {1}" -f (Get-UtcTimestamp),$PSCmdlet.MyInvocation.Line);
Get-ProjectInfo.ps1.txt
Displaying Get-ProjectInfo.ps1.txt.