PSBuildkite.psm1

using namespace Microsoft.PowerShell.Commands;
using namespace System.Management.Automation;

$DEFAULT_PER_PAGE = 30

function Invoke-BuildkiteAPIRequest {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [WebRequestMethod] $Method = [WebRequestMethod]::Get,

        [Parameter(Mandatory, Position = 0)]
        [string] $Path,

        $Body,
        [string] $Accept,

        [ValidateRange(1, [int]::MaxValue)]
        [int] $Page,
        [ValidateRange(1, 100)]
        [int] $PerPage,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Security.SecureString] $Token
    )
    $uri = [Uri]::new([Uri]::new('https://api.buildkite.com/v2/'), $Path).ToString()
    $decodedToken = [PSCredential]::new('dummy', $Token).GetNetworkCredential().Password
    $header = @{
        "Authorization" = "Bearer $decodedToken"
        "User-Agent"    = "PowerShell"
    }
    if ($Accept) {
        $header['Accept'] = $Accept
    }
    if ($Method -ne [WebRequestMethod]::Get) {
        $Body = $Body | ConvertTo-Json
    }
    [string[]]$search = @()
    if ($PSBoundParameters.ContainsKey('Page')) {
        $search += "page=$Page"
    }
    if ($PSBoundParameters.ContainsKey('PerPage')) {
        $search += "per_page$PerPage"
    }
    if ($search) {
        if ($uri.Contains('?')) {
            $uri += '&'
        } else {
            $uri += '?'
        }
        $uri += ($search -join '&')
    }
    if ($Method -eq [WebRequestMethod]::Get -or $PSCmdlet.ShouldProcess("Invoke", "Invoke Buildkite API request?", "API request")) {
        Invoke-RestMethod `
            -Method $Method `
            -Uri $uri `
            -Header $header `
            -ContentType 'application/json' `
            -Body $Body `
            -FollowRelLink
    }
}

function Get-BuildkiteBuild {
    [CmdletBinding()]
    param(
        [string] $Organization,
        [string] $Pipeline,
        [string[]] $Branch,

        [ValidateSet('running', 'scheduled', 'passed', 'failed', 'blocked', 'canceled', 'canceling', 'skipped', 'not_run', 'finished')]
        [string[]] $State,

        [DateTime] $CreatedFrom,
        [DateTime] $CreatedTo,

        [int] $Number,

        [ValidateRange(1, [int]::MaxValue)]
        [int] $Page = 1,
        [ValidateRange(1, 100)]
        [int] $PerPage = $DEFAULT_PER_PAGE,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Security.SecureString] $Token
    )
    $path = if ($Number) {
        "organizations/$Organization/pipelines/$Pipeline/builds/$Number"
    } elseif ($Pipeline -and $Organization) {
        "organizations/$Organization/pipelines/$Pipeline/builds"
    } elseif ($Organization) {
        "organizations/$Organization/builds"
    } else {
        "builds"
    }

    [string[]]$search = @()
    if ($State) {
        $search += ($State | ForEach-Object { "state[]=$_" })
    }
    if ($Branch) {
        $search += ($Branch | ForEach-Object { "branch[]=$_" })
    }
    if ($CreatedFrom) {
        $search += "created_from=$($CreatedFrom.ToString('o'))"
    }
    if ($CreatedTo) {
        $search += "created_to=$($CreatedTo.ToString('o'))"
    }

    if ($search) {
        $path += "?" + ($search -join '&')
    }

    Invoke-BuildkiteAPIRequest $path -Token $Token -Page $Page -PerPage $PerPage | ForEach-Object { $_ } | ForEach-Object {
        $_.PSTypeNames.Insert(0, 'PSBuildkite.Build')
        $_
    }
}

function Stop-BuildkiteBuild {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Url')]
        [ValidateNotNullOrEmpty()]
        [string] $Url,

        [Parameter(Mandatory, ParameterSetName = 'Params')]
        [string] $Organization,

        [Parameter(Mandatory, ParameterSetName = 'Params')]
        [string] $Pipeline,

        [Parameter(Mandatory, ParameterSetName = 'Params')]
        [int] $Number,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Security.SecureString] $Token
    )

    process {
        if (-not $Url) {
            $Url = "organizations/$Organization/pipelines/$Pipeline/builds/$Number"
        }
        $Url += "/cancel"
        Invoke-BuildkiteAPIRequest -Method PUT $Url -Token $Token | ForEach-Object {
            $_.PSTypeNames.Insert(0, 'PSBuildkite.Build')
            $_
        }
    }
}

function Restart-BuildkiteBuild {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Url')]
        [ValidateNotNullOrEmpty()]
        [string] $Url,

        [Parameter(Mandatory, ParameterSetName = 'Params')]
        [string] $Organization,

        [Parameter(Mandatory, ParameterSetName = 'Params')]
        [string] $Pipeline,

        [Parameter(Mandatory, ParameterSetName = 'Params')]
        [int] $Number,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Security.SecureString] $Token
    )

    process {
        if (-not $Url) {
            $Url = "organizations/$Organization/pipelines/$Pipeline/builds/$Number"
        }
        $Url += "/rebuild"
        Invoke-BuildkiteAPIRequest -Method PUT $Url -Token $Token | ForEach-Object {
            $_.PSTypeNames.Insert(0, 'PSBuildkite.Build')
            $_
        }
    }
}

function Get-BuildkiteOrganization {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string] $Slug,

        [ValidateRange(1, [int]::MaxValue)]
        [int] $Page = 1,
        [ValidateRange(1, 100)]
        [int] $PerPage = $DEFAULT_PER_PAGE,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Security.SecureString] $Token
    )
    $path = "organizations"
    if ($Slug) {
        $path += "/$Slug"
    }
    Invoke-BuildkiteAPIRequest $path -Token $Token -Page $Page -PerPage $PerPage
}

function Get-BuildkitePipeline {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Organization,

        [Parameter()]
        [string] $Slug,

        [ValidateRange(1, [int]::MaxValue)]
        [int] $Page = 1,
        [ValidateRange(1, 100)]
        [int] $PerPage = $DEFAULT_PER_PAGE,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Security.SecureString] $Token
    )
    $path = "organizations/$Organization/pipelines"
    if ($Slug) {
        $path += "/$Slug"
    }
    Invoke-BuildkiteAPIRequest $path -Token $Token -Page $Page -PerPage $PerPage | ForEach-Object {
        $_.PSObject.TypeNames.Insert(0, 'Buildkite.Pipeline')
        $_
    }
}

function Get-BuildkiteJobLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [string] $Organization,

        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [string] $Pipeline,

        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [int] $Build,

        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [string] $JobId,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Job')]
        $Job,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Security.SecureString] $Token
    )

    process {
        $LogUrl = if ($Job) {
            if ($Job.type -ne 'script') {
                Write-Warning "$($Job.type) job has no logs"
                return
            }
            $Job.log_url
        } else {
            "organizations/$Organization/pipelines/$Pipeline/builds/$Build/jobs/$JobId/log"
        }
        Invoke-BuildkiteAPIRequest $LogUrl -Token $Token -Accept 'text/plain'
    }
}

function Get-BuildkiteCruiseControlFeedUrl {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName, ParameterSetName = 'explicit')]
        [string] $Organization,

        [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName, ParameterSetName = 'explicit')]
        [Alias('slug')]
        [string] $Pipeline,

        # An alternative way to provide org and pipeline
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'url')]
        [string] $Url,

        [Parameter()]
        [string] $Branch,

        [Parameter(Mandatory)]
        [securestring] $Token
    )
    if ($Url) {
        $Url -match '/organizations/([^/]+)/pipelines(?:/([^/]+))?' | Out-Null
        $Organization = $Matches[1]
        $Pipeline = $Matches[2]
    }
    $decodedToken = [PSCredential]::new('dummy', $Token).GetNetworkCredential().Password
    $url = "https://cc.buildkite.com/$Organization/$Pipeline.xml?access_token=$decodedToken"
    if ($Branch) {
        $url += "&branch=$Branch"
    }
    $url
}

# Workaround for https://github.com/PowerShell/PowerShell/issues/7735
function Add-DefaultParamterValues([string] $Command, [hashtable] $Parameters) {
    foreach ($entry in $global:PSDefaultParameterValues.GetEnumerator()) {
        $commandPattern, $parameter = $entry.Key.Split(':')
        if ($Command -like $commandPattern) {
            $Parameters.Add($parameter, $entry.Value)
        }
    }
}

$organizationCompleter = {
    [CmdletBinding()]
    param([string]$command, [string]$parameter, [string]$wordToComplete, [CommandAst]$commandAst, [Hashtable]$params)
    Add-DefaultParamterValues -Command $command -Parameters $params
    if (-not $params.ContainsKey('Token')) {
        return
    }
    Get-BuildkiteOrganization -Token $params['Token'] |
        ForEach-Object { $_.Slug } |
        Where-Object { $_ -like "$wordToComplete*" } |
        ForEach-Object { [CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_) }
}

$pipelineCompleter = {
    [CmdletBinding()]
    param([string]$command, [string]$parameter, [string]$wordToComplete, [CommandAst]$commandAst, [Hashtable]$params)
    Add-DefaultParamterValues -Command $command -Parameters $params
    if (-not $params.ContainsKey('Organization') -or -not $params.ContainsKey('Token')) {
        return
    }

    Get-BuildkitePipeline -Organization $params['Organization'] -Token $params['Token'] |
        ForEach-Object { $_.Slug } |
        Where-Object { $_ -like "$wordToComplete*" } |
        ForEach-Object { [CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_) }
}

Register-ArgumentCompleter -CommandName Get-BuildkiteOrganization -ParameterName Slug -ScriptBlock $organizationCompleter
Register-ArgumentCompleter -CommandName Get-BuildkitePipeline -ParameterName Organization -ScriptBlock $organizationCompleter
Register-ArgumentCompleter -CommandName Get-BuildkitePipeline -ParameterName Pipeline -ScriptBlock $pipelineCompleter
Register-ArgumentCompleter -CommandName Get-BuildkiteCruiseControlFeedUrl -ParameterName Pipeline -ScriptBlock $pipelineCompleter
Register-ArgumentCompleter -CommandName Get-BuildkiteCruiseControlFeedUrl -ParameterName Organization -ScriptBlock $organizationCompleter
Register-ArgumentCompleter -CommandName Get-BuildkiteBuild -ParameterName Organization -ScriptBlock $organizationCompleter
Register-ArgumentCompleter -CommandName Get-BuildkiteBuild -ParameterName Pipeline -ScriptBlock $pipelineCompleter
Register-ArgumentCompleter -CommandName Get-BuildkiteCruiseControlFeedUrl -ParameterName Organization -ScriptBlock $organizationCompleter
Register-ArgumentCompleter -CommandName Get-BuildkiteCruiseControlFeedUrl -ParameterName Pipeline -ScriptBlock $pipelineCompleter
Register-ArgumentCompleter -CommandName Get-BuildkiteCruiseControlFeedUrl -ParameterName Branch -ScriptBlock {
    [CmdletBinding()]
    param([string]$command, [string]$parameter, [string]$wordToComplete, [CommandAst]$commandAst, [Hashtable]$params)
    Add-DefaultParamterValues -Command $command -Parameters $params
    if (-not $params.ContainsKey('Organization') -or -not $params.ContainsKey('Token')) {
        return
    }

    Get-BuildkitePipeline -Organization $params['Organization'] -Slug $params['Pipeline'] -Token $params['Token'] |
        ForEach-Object { $_.default_branch } |
        Where-Object { $_ -like "$wordToComplete*" } |
        ForEach-Object { [CompletionResult]::new($_, $_, [CompletionResultType]::ParameterValue, $_) }
}