Modules/businessdev.ALbuild.Apps/Public/Get-BcCallGraphBaseline.ps1

function Get-BcCallGraphBaseline {
    <#
    .SYNOPSIS
        Downloads the call graph of the last successful build of a branch, as the baseline to compare with.
 
    .DESCRIPTION
        Looks up the most recent completed build of the pipeline definition on the branch whose result is
        succeeded or partially succeeded, skipping the current build, and downloads its call graph artifact.
        Older builds are tried when the newest one has no such artifact (e.g. it ran before the call graph
        was switched on). Returns $null when there is no baseline, which is normal for a first build.
 
        In a pull request build pass the target branch: the change is then measured against what it will be
        merged into.
 
    .PARAMETER CollectionUri
        Organization URL, e.g. https://dev.azure.com/contoso/ (System.CollectionUri).
 
    .PARAMETER Project
        Team project name or id (System.TeamProject).
 
    .PARAMETER DefinitionId
        Pipeline definition id (System.DefinitionId).
 
    .PARAMETER Branch
        Branch to take the baseline from, e.g. refs/heads/master.
 
    .PARAMETER CurrentBuildId
        The running build, never used as its own baseline.
 
    .PARAMETER AccessToken
        System.AccessToken (Bearer) or a PAT (with -AuthScheme Basic).
 
    .PARAMETER AuthScheme
        Bearer (default, for System.AccessToken) or Basic (for a PAT).
 
    .PARAMETER ArtifactName
        Name of the build artifact holding the call graph. Default 'callgraph'.
 
    .PARAMETER Destination
        Folder to extract the baseline into.
 
    .PARAMETER MaxBuilds
        How many recent builds to try. Default 5.
 
    .EXAMPLE
        Get-BcCallGraphBaseline -CollectionUri $env:SYSTEM_COLLECTIONURI -Project $env:SYSTEM_TEAMPROJECT `
            -DefinitionId $env:SYSTEM_DEFINITIONID -Branch 'refs/heads/master' -AccessToken $env:SYSTEM_ACCESSTOKEN `
            -Destination "$env:AGENT_TEMPDIRECTORY/callgraph-baseline"
 
    .OUTPUTS
        PSCustomObject: Path (the folder with graph.jsonl), BuildId, BuildNumber, Branch, SourceVersion; or $null.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [string] $CollectionUri,
        [Parameter(Mandatory)] [string] $Project,
        [Parameter(Mandatory)] [int] $DefinitionId,
        [Parameter(Mandatory)] [string] $Branch,
        [int] $CurrentBuildId = 0,
        [Parameter(Mandatory)] [string] $AccessToken,
        [ValidateSet('Bearer', 'Basic')] [string] $AuthScheme = 'Bearer',
        [string] $ArtifactName = 'callgraph',
        [Parameter(Mandatory)] [string] $Destination,
        [ValidateRange(1, 50)] [int] $MaxBuilds = 5
    )

    if (-not $Branch.StartsWith('refs/')) { $Branch = "refs/heads/$Branch" }
    $base = "$($CollectionUri.TrimEnd('/'))/$([uri]::EscapeDataString($Project))/_apis/build"
    $headers = @{
        Authorization = if ($AuthScheme -eq 'Basic') { 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$AccessToken")) } else { "Bearer $AccessToken" }
    }
    $query = "definitions=$DefinitionId&branchName=$([uri]::EscapeDataString($Branch))&statusFilter=completed&resultFilter=succeeded,partiallySucceeded&queryOrder=finishTimeDescending&`$top=$($MaxBuilds + 1)&api-version=7.1"
    try { $builds = @((Invoke-RestMethod -Uri "$base/builds?$query" -Headers $headers -Method Get -ErrorAction Stop).value) }
    catch { throw "Could not list the builds of definition $DefinitionId on '$Branch': $($_.Exception.Message)" }

    $tried = 0
    foreach ($build in $builds) {
        if ($build.id -eq $CurrentBuildId) { continue }
        if (++$tried -gt $MaxBuilds) { break }
        $artifact = $null
        try { $artifact = Invoke-RestMethod -Uri "$base/builds/$($build.id)/artifacts?artifactName=$([uri]::EscapeDataString($ArtifactName))&api-version=7.1" -Headers $headers -Method Get -ErrorAction Stop }
        catch { Write-ALbuildLog -Level Verbose "Build $($build.buildNumber) has no '$ArtifactName' artifact ($($_.Exception.Message))."; continue }
        $resource = if ($artifact) { $artifact.PSObject.Properties['resource'] } else { $null }
        $url = if ($resource -and $resource.Value -and $resource.Value.PSObject.Properties['downloadUrl']) { $resource.Value.downloadUrl } else { $null }
        if (-not $url) { continue }

        if (Test-Path -LiteralPath $Destination) { Remove-Item -LiteralPath $Destination -Recurse -Force }
        New-Item -ItemType Directory -Force -Path $Destination | Out-Null
        $zip = Join-Path $Destination 'baseline.zip'
        Invoke-WebRequest -Uri $url -Headers $headers -OutFile $zip -UseBasicParsing -ErrorAction Stop
        Add-Type -AssemblyName System.IO.Compression.FileSystem
        [System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $Destination)
        Remove-Item -LiteralPath $zip -Force
        $graph = Get-ChildItem -LiteralPath $Destination -Filter 'graph.jsonl' -File -Recurse | Select-Object -First 1
        if (-not $graph) { Write-ALbuildLog -Level Warning "The '$ArtifactName' artifact of build $($build.buildNumber) has no graph.jsonl."; continue }
        Write-ALbuildLog "Call graph baseline: build $($build.buildNumber) ($($build.sourceBranch), $("$($build.sourceVersion)".Substring(0, [Math]::Min(8, "$($build.sourceVersion)".Length))))."
        return [PSCustomObject]@{
            Path = $graph.DirectoryName; BuildId = $build.id; BuildNumber = $build.buildNumber
            Branch = $build.sourceBranch; SourceVersion = $build.sourceVersion
        }
    }
    Write-ALbuildLog "Call graph baseline: no successful build of definition $DefinitionId on '$Branch' has a '$ArtifactName' artifact yet."
    return $null
}