<# .SYNOPSIS Analyzes Renovate pull requests across all repositories in an Azure DevOps project and generates detailed statistics. .DESCRIPTION This script connects to an Azure DevOps organization and project to analyze Renovate dependency update pull requests. It processes all repositories (active, disabled, and locked) and generates comprehensive statistics including: - Total Renovate PRs per repository - Open, completed, and abandoned PR counts - Latest creation and completion dates - Branch information for latest PRs - Repository status (active, disabled, locked, error) The script outputs both a detailed text report and a CSV file for further analysis. Renovate PRs are identified by their branch naming pattern: "refs/heads/renovate/*" .PARAMETER Organization The Azure DevOps organization name. Defaults to "effectory" if not specified. .PARAMETER Project The Azure DevOps project name. Defaults to "Survey Software" if not specified. .PARAMETER PAT The Azure DevOps Personal Access Token (PAT) used for authentication. This token must have 'Code (Read)' permissions to access repository and pull request information. .PARAMETER OutputFile The path and filename for the output text report. Defaults to a timestamped filename: "RenovatePRs_Stats_yyyyMMdd_HHmmss.txt" .EXAMPLE .\renovate-stats.ps1 -PAT "your-personal-access-token" Analyzes Renovate PRs using default organization and project settings. .EXAMPLE .\renovate-stats.ps1 -PAT "your-pat-token" -Organization "myorg" -Project "MyProject" Analyzes Renovate PRs for a specific organization and project. .EXAMPLE .\renovate-stats.ps1 -PAT "your-token" -OutputFile "C:\Reports\renovate-analysis.txt" Analyzes Renovate PRs and saves the report to a custom location. .OUTPUTS Creates two output files: 1. Text Report: Detailed statistics with tables and summaries (.txt) 2. CSV Export: Raw data for further analysis (.csv) The text report includes: - Repository-by-repository statistics table (sorted by last created date) - Summary statistics (total repos, repos with/without Renovate, PR counts) - List of disabled/locked/error repositories The CSV contains columns: - Repository: Repository name - TotalRenovatePRs: Total count of Renovate PRs - OpenPRs: Count of active Renovate PRs - CompletedPRs: Count of completed Renovate PRs - AbandonedPRs: Count of abandoned Renovate PRs - LastCreated: Date of most recent Renovate PR creation (yyyy-MM-dd format) - LastCompleted: Date of most recent Renovate PR completion (yyyy-MM-dd format) - LatestOpenBranch: Branch name of most recent open Renovate PR - LatestCompletedBranch: Branch name of most recent completed Renovate PR - LastCompletedPRTitle: Title of most recent completed Renovate PR .NOTES Author: Cloud Engineering Team Created: 2025 Requires: PowerShell 5.1 or later Dependencies: Internet connectivity to Azure DevOps The script uses Azure DevOps REST API version 6.0 to retrieve repository and pull request information. Ensure your Personal Access Token has the necessary permissions before running the script. Renovate is a dependency update tool that creates automated pull requests. This script specifically looks for PRs with branch names matching the pattern "refs/heads/renovate/*" The script handles various repository states: - Active: Normal repositories that can be analyzed - Disabled: Repositories marked as disabled in Azure DevOps - Locked: Repositories that are locked (read-only) - Error: Repositories that couldn't be accessed due to API errors Date parsing is culture-invariant and includes fallback mechanisms to handle various date formats from the Azure DevOps API. .LINK https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-requests https://renovatebot.com/ #> param( [Parameter(Mandatory=$false, HelpMessage="Azure DevOps organization name")] [string]$Organization = "effectory", [Parameter(Mandatory=$false, HelpMessage="Azure DevOps project name")] [string]$Project = "Survey Software", [Parameter(Mandatory=$true, HelpMessage="Azure DevOps Personal Access Token with Code (Read) permissions")] [string]$PAT, [Parameter(Mandatory=$false, HelpMessage="Output file path for the text report")] [string]$OutputFile = "RenovatePRs_Stats_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" ) <# .SYNOPSIS Outputs a message to both the console and the output file. .DESCRIPTION This helper function writes messages to the console with optional color formatting and simultaneously appends the same message to the output file, unless ConsoleOnly is specified. .PARAMETER Message The message to output. .PARAMETER ForegroundColor The console text color. Defaults to "White". .PARAMETER ConsoleOnly If specified, the message will only be written to the console and not the output file. Useful for progress messages that shouldn't clutter the report. #> function Write-Output-Both { param ( [string]$Message, [string]$ForegroundColor = "White", [switch]$ConsoleOnly ) Write-Host $Message -ForegroundColor $ForegroundColor if (-not $ConsoleOnly) { Add-Content -Path $OutputFile -Value $Message } } # Initialize the output file with a header Set-Content -Path $OutputFile -Value "Renovate Pull Requests Statistics - $(Get-Date)`n" # Prepare authentication for Azure DevOps REST API calls # Personal Access Token must be base64 encoded with a colon prefix $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$PAT")) $headers = @{ Authorization = "Basic $base64AuthInfo" } # Retrieve all repositories from the Azure DevOps project $reposUrl = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories?api-version=6.0" $repositories = Invoke-RestMethod -Uri $reposUrl -Method Get -Headers $headers # Initialize arrays to categorize repositories and store statistics $repoStats = @() # Active repositories with detailed statistics $reposWithoutRenovate = @() # Active repositories with no Renovate PRs $disabledRepos = @() # Disabled, locked, or error repositories # Process each repository in the project foreach ($repo in $repositories.value) { $repoName = $repo.name $repoId = $repo.id Write-Output-Both "Analyzing repository: $repoName" -ForegroundColor Gray -ConsoleOnly # Check repository status (disabled, locked, or active) $isDisabled = $repo.isDisabled -eq $true $repoDetailsUrl = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$repoId`?api-version=6.0" try { $repoDetails = Invoke-RestMethod -Uri $repoDetailsUrl -Method Get -Headers $headers $isLocked = $repoDetails.isLocked -eq $true } catch { Write-Output-Both " Could not retrieve repository details. Assuming not locked." -ForegroundColor Yellow -ConsoleOnly $isLocked = $false } # Skip analysis for disabled or locked repositories if ($isDisabled -or $isLocked) { $status = if ($isDisabled) { "DISABLED" } elseif ($isLocked) { "LOCKED" } else { "UNKNOWN" } Write-Output-Both " Repository status: $status - Skipping analysis" -ForegroundColor Yellow -ConsoleOnly # Create entry for disabled/locked repository with N/A values $disabledRepo = [PSCustomObject]@{ Repository = "$repoName ($status)" TotalRenovatePRs = "N/A" OpenPRs = "N/A" CompletedPRs = "N/A" AbandonedPRs = "N/A" LastCreatedDate = "N/A" LastCompletedDate = "N/A" LatestOpenBranch = "N/A" LatestCompletedBranch = "N/A" LastCompletedPRTitle = "N/A" RepoStatus = $status } $disabledRepos += $disabledRepo continue } # Retrieve all pull requests for the current repository $prsUrl = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$repoId/pullrequests`?api-version=6.0&searchCriteria.status=all" try { $pullRequests = Invoke-RestMethod -Uri $prsUrl -Method Get -Headers $headers # Filter for Renovate PRs based on branch naming pattern $renovatePRs = $pullRequests.value | Where-Object { $_.sourceRefName -like "refs/heads/renovate/*" } if ($renovatePRs.Count -gt 0) { # Categorize Renovate PRs by status $openPRs = $renovatePRs | Where-Object { $_.status -eq "active" } $completedPRs = $renovatePRs | Where-Object { $_.status -eq "completed" } $abandonedPRs = $renovatePRs | Where-Object { $_.status -eq "abandoned" } # Count PRs in each category $openCount = $openPRs.Count $completedCount = $completedPRs.Count $abandonedCount = $abandonedPRs.Count # Find the most recent PRs in each category for detailed reporting $latestOpen = $openPRs | Sort-Object creationDate -Descending | Select-Object -First 1 $latestCompleted = $completedPRs | Sort-Object closedDate -Descending | Select-Object -First 1 $latestCreated = $renovatePRs | Sort-Object creationDate -Descending | Select-Object -First 1 # Extract key information from the latest PRs $lastCreatedDate = if ($latestCreated) { $latestCreated.creationDate } else { "N/A" } $lastCompletedDate = if ($latestCompleted) { $latestCompleted.closedDate } else { "N/A" } $lastCompletedPRTitle = if ($latestCompleted) { $latestCompleted.title } else { "N/A" } $latestOpenBranch = if ($latestOpen) { ($latestOpen.sourceRefName -replace "refs/heads/", "") } else { "N/A" } $latestCompletedBranch = if ($latestCompleted) { ($latestCompleted.sourceRefName -replace "refs/heads/", "") } else { "N/A" } $repoStat = [PSCustomObject]@{ Repository = $repoName TotalRenovatePRs = $renovatePRs.Count OpenPRs = $openCount CompletedPRs = $completedCount AbandonedPRs = $abandonedCount LastCreatedDate = $lastCreatedDate LastCompletedDate = $lastCompletedDate LatestOpenBranch = $latestOpenBranch LatestCompletedBranch = $latestCompletedBranch LastCompletedPRTitle = $lastCompletedPRTitle RepoStatus = "ACTIVE" SortDate = if ($lastCreatedDate -eq "N/A") { [DateTime]::MinValue } else { try { [DateTime]::Parse($lastCreatedDate, [System.Globalization.CultureInfo]::InvariantCulture) } catch { try { [DateTime]::ParseExact($lastCreatedDate, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) } catch { Write-Output-Both " Warning: Could not parse date '$lastCreatedDate' for repository $repoName" -ForegroundColor Yellow -ConsoleOnly [DateTime]::MinValue } } } } $repoStats += $repoStat } else { $reposWithoutRenovate += $repoName $repoStat = [PSCustomObject]@{ Repository = $repoName TotalRenovatePRs = 0 OpenPRs = 0 CompletedPRs = 0 AbandonedPRs = 0 LastCreatedDate = "N/A" LastCompletedDate = "N/A" LatestOpenBranch = "N/A" LatestCompletedBranch = "N/A" LastCompletedPRTitle = "N/A" RepoStatus = "ACTIVE" SortDate = [DateTime]::MinValue } $repoStats += $repoStat } } catch { Write-Output-Both " Error accessing pull requests for repository: $_" -ForegroundColor Red -ConsoleOnly $disabledRepo = [PSCustomObject]@{ Repository = "$repoName (ERROR)" TotalRenovatePRs = "Error" OpenPRs = "Error" CompletedPRs = "Error" AbandonedPRs = "Error" LastCreatedDate = "Error" LastCompletedDate = "Error" LatestOpenBranch = "Error" LatestCompletedBranch = "Error" LastCompletedPRTitle = "Error" RepoStatus = "ERROR" } $disabledRepos += $disabledRepo } } # Generate and display the main statistics report Write-Output-Both "`n===== RENOVATE PULL REQUEST STATISTICS BY REPOSITORY (SORTED BY LAST CREATED DATE) =====" -ForegroundColor Green if ($repoStats.Count -gt 0) { # Sort repositories by last created date (most recent first) $sortedStats = $repoStats | Sort-Object -Property SortDate -Descending # Format the data for display with culture-invariant date parsing $displayStats = $sortedStats | Select-Object Repository, TotalRenovatePRs, OpenPRs, CompletedPRs, AbandonedPRs, @{Name="LastCreated"; Expression={ if ($_.LastCreatedDate -eq "N/A" -or $_.LastCreatedDate -eq "Error") { $_.LastCreatedDate } else { try { [DateTime]::Parse($_.LastCreatedDate, [System.Globalization.CultureInfo]::InvariantCulture).ToString("yyyy-MM-dd") } catch { try { [DateTime]::ParseExact($_.LastCreatedDate, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture).ToString("yyyy-MM-dd") } catch { $_.LastCreatedDate } } } }}, @{Name="LastCompleted"; Expression={ if ($_.LastCompletedDate -eq "N/A" -or $_.LastCompletedDate -eq "Error") { $_.LastCompletedDate } else { try { [DateTime]::Parse($_.LastCompletedDate, [System.Globalization.CultureInfo]::InvariantCulture).ToString("yyyy-MM-dd") } catch { try { [DateTime]::ParseExact($_.LastCompletedDate, "MM/dd/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture).ToString("yyyy-MM-dd") } catch { $_.LastCompletedDate } } } }}, LatestOpenBranch, LatestCompletedBranch, LastCompletedPRTitle # Export detailed data to CSV for further analysis $displayStats | Export-Csv -Path "$($OutputFile).csv" -NoTypeInformation Add-Content -Path $OutputFile -Value "Full data available in: $($OutputFile).csv" # Add formatted table to the text report and display on console $statsTable = $displayStats | Format-Table -Property Repository, TotalRenovatePRs, OpenPRs, CompletedPRs, AbandonedPRs, LastCreated, LastCompleted, LatestCompletedBranch, LastCompletedPRTitle | Out-String Add-Content -Path $OutputFile -Value $statsTable.Trim() $displayStats | Format-Table -AutoSize # Calculate summary statistics $totalRepos = $repositories.value.Count $reposWithRenovate = ($repoStats | Where-Object { $_.TotalRenovatePRs -gt 0 }).Count $reposDisabledOrLocked = $disabledRepos.Count $activeRepos = $totalRepos - $reposDisabledOrLocked # Calculate percentage of active repositories with Renovate PRs $percentWithRenovate = if ($activeRepos -gt 0) { [math]::Round(($reposWithRenovate / $activeRepos) * 100, 2) } else { 0 } # Calculate totals across all active repositories $totalPRs = ($repoStats | Measure-Object -Property TotalRenovatePRs -Sum).Sum $totalOpenPRs = ($repoStats | Measure-Object -Property OpenPRs -Sum).Sum $totalCompletedPRs = ($repoStats | Measure-Object -Property CompletedPRs -Sum).Sum $totalAbandonedPRs = ($repoStats | Measure-Object -Property AbandonedPRs -Sum).Sum # Display comprehensive summary statistics Write-Output-Both "`n===== SUMMARY STATISTICS =====" -ForegroundColor Cyan Write-Output-Both "Total repositories: $totalRepos" Write-Output-Both "Disabled, locked, or error repositories: $reposDisabledOrLocked" Write-Output-Both "Active repositories: $activeRepos" Write-Output-Both "Active repositories with renovate PRs: $reposWithRenovate ($percentWithRenovate%)" Write-Output-Both "Active repositories without renovate PRs: $($activeRepos - $reposWithRenovate) ($(100 - $percentWithRenovate)%)" Write-Output-Both "`nTotal renovate PRs: $totalPRs" Write-Output-Both "Total open renovate PRs: $totalOpenPRs" Write-Output-Both "Total completed renovate PRs: $totalCompletedPRs" Write-Output-Both "Total abandoned renovate PRs: $totalAbandonedPRs" # Display list of repositories that couldn't be analyzed if ($disabledRepos.Count -gt 0) { Write-Output-Both "`n===== DISABLED/LOCKED/ERROR REPOSITORIES (NOT INCLUDED IN MAIN REPORT) =====" -ForegroundColor Yellow $disabledList = $disabledRepos | ForEach-Object { $_.Repository } Write-Output-Both ($disabledList -join "`n") } } else { Write-Output-Both "No active repositories found." -ForegroundColor Yellow } # Display completion message Write-Output-Both "`nReport saved to: $OutputFile" -ForegroundColor Cyan