ManageIssues.psm1

<#
.SYNOPSIS
Returns a list of stale issues from a GitHub repository.
 
.DESCRIPTION
This cmdlet returns a list of stale issues from a GitHub repository based on the age of the issue. You can be filter by one or more labels. The output can be saved to a CSV file for further processing.
 
.PARAMETER OlderThanDays
The number of days that an issue must be older than to be considered stale.
 
.PARAMETER OlderThanMonths
The number of months that an issue must be older than to be considered stale.
 
.PARAMETER Label
One or more labels that the issue must have. Multiple labels are specified as an array
(comma-separated list).
 
.PARAMETER RepoName
The name of the repository to search. The default is "MicrosoftDocs/azure-docs".
 
.PARAMETER OutPath
The path to a CSV file to save the output to. If not specified, the output is displayed in the
console.
 
.EXAMPLE
Find-Issue -OlderThanDays 30 -Label 'cloud-shell/svc'
 
This example returns a list of issues that are older than 30 days and have the label
"cloud-shell/svc".
 
.EXAMPLE
Find-Issue -OlderThanMonths 3 -Label 'sql/subsvc', 'synapse-analytics/svc' -OutPath C:\Temp\issues.csv
 
This example returns a list of issues that are older than 3 months and have the labels "sql/subsvc"
and "synapse-analytics/svc". The output is saved to the file C:\Temp\issues.csv.
 
.NOTES
This cmdlet requires a GitHub personal access token to be stored in the environment variable
$Env:GITHUB_TOKEN.
 
#>

function Find-Issue {
    [CmdletBinding(DefaultParameterSetName = 'ByAgeDays')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ByAgeDays')]
        [uint32]$OlderThanDays,

        [Parameter(Mandatory, ParameterSetName = 'ByAgeMonths')]
        [uint32]$OlderThanMonths,

        [Parameter(ParameterSetName = 'ByAgeDays')]
        [Parameter(ParameterSetName = 'ByAgeMonths')]
        [string[]]$Label,

        [Parameter(ParameterSetName = 'ByAgeDays')]
        [Parameter(ParameterSetName = 'ByAgeMonths')]
        [string]$RepoName = "MicrosoftDocs/azure-docs",

        [Parameter(ParameterSetName = 'ByAgeDays')]
        [Parameter(ParameterSetName = 'ByAgeMonths')]
        [string]$OutPath
    )

    if ([string]::IsNullOrEmpty($Env:GITHUB_TOKEN) -or
        [string]::IsNullOrEmpty($Env:GITHUB_TOKEN.trim())) {
        $PSCmdlet.ThrowTerminatingError('GitHub personal access token not found.' +
        ' store your token in the $Env:GITHUB_TOKEN environment variable.')
    }

    $hdr = @{
        Accept        = 'application/vnd.github.raw+json'
        Authorization = "token ${Env:\GITHUB_TOKEN}"
    }

    $today = Get-Date
    if ($PSCmdlet.ParameterSetName -eq 'ByAgeDays') {
        $date = $today.AddDays(-$OlderThanDays)
    } else {
        $date = $today.AddMonths(-$OlderThanMonths)
    }
    $queryparams = 'is:open repo:{0} created:<{1:yyyy-MM-dd}' -f $RepoName,$date
    if ($Label) {
        $queryparams += ' label:' + ($Label -join ' label:')
    }
    $query = 'q=' + [System.Net.WebUtility]::UrlEncode($queryparams) + '&sort=created&order=asc&per_page=100'

    try {
        $issuelist = Invoke-RestMethod "https://api.github.com/search/issues?$query" -Headers $hdr -FollowRelLink
    } catch {
        if (($_.ErrorDetails.Message | ConvertFrom-Json).message -match 'Validation failed') {
            $PSCmdlet.ThrowTerminatingError('Make sure your access token has been configured ' +
            'for SSO with the target orge (MicrosoftDocs).')
        } else {
            $PSCmdlet.ThrowTerminatingError($_.Exception.Message)
        }
    }

    $records = @()
    $issuelist.items | ForEach-Object {
        try {
            $number = $_.number
            $issue = (Invoke-RestMethod $_.url -Headers $hdr)
            $record = [pscustomobject]@{
                Number    = $RepoName + '#' + $issue.number
                Title     = $issue.title
                Age       = ($today - [datetime]($issue.created_at)).TotalDays
                UpdateAge = ($today - [datetime]($issue.updated_at)).TotalDays
                Labels    = $issue.labels.name -join ', '
                Assignee  = $issue.assignee.login
                htmlUrl   = $issue.html_url
                apiUrl    = $issue.url
                Created   = '{0:yyyy-MM-dd}' -f [datetime]($issue.created_at)
                Updated   = '{0:yyyy-MM-dd}' -f [datetime]($issue.updated_at)
            }
            if ($OutPath -eq '') {
                $record
            } else {
                $records += $record
            }
        } catch {
            if (($_.ErrorDetails.Message | ConvertFrom-Json).message -match 'Bad credentials') {
                $PSCmdlet.ThrowTerminatingError('Invalid GitHub credentials.' +
                ' Check the value of the $Env:GITHUB_TOKEN environment variable.')
            } else {
                Write-Error "Error processing issue #$number - $($_.Exception.Message)"
            }
        }
    }
    if ($OutPath) {
        $records | Export-Csv -Path $OutPath -NoTypeInformation
    }
}

<#
.SYNOPSIS
Adds a comment to a list of issues.
 
.DESCRIPTION
This cmdlet adds a comment to a list of issues. The list of issues is specified in a CSV file. The
CSV file must have a column named "apiUrl" that contains the URL of the GitHub API for the issue.
You can create this CSV file by using the Find-Issue cmdlet. The comment added to the issue is read
from the specified Markdown file.
 
.PARAMETER CsvPath
The path to the CSV file that contains the list of issues to add a comment to.
 
.PARAMETER CommentPath
The path to the Markdown file that contains the comment to add to the issue.
 
.EXAMPLE
Add-ClosingComment -CsvPath C:\Temp\issues.csv -CommentPath C:\Temp\comment.md
 
.NOTES
This cmdlet requires a GitHub personal access token to be stored in the environment variable
$Env:GITHUB_TOKEN.
#>

function Add-ClosingComment {
    param (
        [Parameter(Mandatory, Position = 0)]
        [ValidateScript({Test-Path $_})]
        [string]$CsvPath,

        [Parameter(Mandatory, Position = 1)]
        [ValidateScript({Test-Path $_})]
        [string]$CommentPath
    )

    $hdr = @{
        Accept        = 'application/vnd.github.raw+json'
        Authorization = "token ${Env:\GITHUB_TOKEN}"
    }

    $comment = @{
        body = Get-Content $CommentPath -raw
    }
    $body = $comment | ConvertTo-Json

    Import-Csv $CsvPath | ForEach-Object {
        try {
            $issue = $_
            $number = $issue.Number
            $api = $issue.apiUrl + '/comments'
            $result = Invoke-RestMethod $api -Headers $hdr -Body $body -Method Post
            $result | Select-Object @{n='Issue'; e={$issue.Number}},
                                    @{n='CommentUrl'; e={$_.htmlUrl}}
        } catch {
            if (($_.ErrorDetails.Message | ConvertFrom-Json).message -match 'Bad credentials') {
                $PSCmdlet.ThrowTerminatingError('Invalid GitHub credentials.' +
                ' Check the value of the $Env:GITHUB_TOKEN environment variable.')
            } else {
                Write-Error "Error processing issue $number - $($_.Exception.Message)"
            }
        }
    }
}