Public/Api/Wit/Get-WorkItemRefsListByArtifactUri.ps1

function Get-WorkItemRefsListByArtifactUri {

    <#
        .SYNOPSIS
            Gets list of work item references associated with given artifacts.

        .DESCRIPTION
            Gets list of work item references associated with given artifacts.

        .PARAMETER CollectionUri
            Url for project collection on Azure DevOps server instance.
            Can be ommitted if $CollectionUri was previously accessed via this API.
            If not specified, $global:AzureDevOpsApi_CollectionUri (set by Set-AzureDevopsVariables) is used.

        .PARAMETER Project
            Project name, identifier, full project URI, or object with any one
            these properties.
            Can be ommitted if $Project was previously accessed via this API (will be extracted from the $ArtifactUri).
            If not specified, $global:AzureDevOpsApi_Project (set by Set-AzureDevopsVariables) is used.

        .PARAMETER ArtifactUri
            List of Artifact Uris to query work items for.
            All Artifact Uris must be from the same project collection.

        .OUTPUTS
            WorkItemRef object, deduplicated by url.

        .NOTES
            https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/artifact-uri-query/query?view=azure-devops-rest-6.0&tabs=HTTP

        .EXAMPLE
            Get-WorkItemRefsListByArtifactUri `
                -ArtifactUri 'vstfs:///Git/PullRequestId/9d7a1154-1315-433e-96e5-11f160256a1d%2f96e0832a-94a2-4c0c-887e-48b8f3d2e7ed%2f8357' `
                -CollectionUri 'https://dev-tfs/tfs/internal_projects'

            id url
            -- ---
            405200 https://dev-tfs/tfs/internal_projects/_apis/wit/workitems/405200

        .EXAMPLE
            # Assuming both projects are in the same collection and were accessed previously
            Get-WorkItemRefsListByArtifactUri `
                -ArtifactUri `
                'vstfs:///Git/PullRequestId/9d7a1154-1315-433e-96e5-11f160256a1d%2f96e0832a-94a2-4c0c-887e-48b8f3d2e7ed%2f8357',
                'vstfs:///Git/PullRequestId/9d7a1154-1315-433e-96e5-11f160256a1d%2fc5538a9c-ad60-426a-8898-b50a44ee9e72%2f7179',
                'vstfs:///Git/PullRequestId/5e62fde7-1b9d-40d1-b69c-787f9b7aaadb%2ffccd7d08-bf7c-4995-a1e5-60524f9aab20%2f8636'

            id url
            -- ---
            405200 https://dev-tfs/tfs/internal_projects/_apis/wit/workitems/405200
            422660 https://dev-tfs/tfs/internal_projects/_apis/wit/workitems/422660
    #>


    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [AllowNull()]
        $ArtifactUri,

        [AllowNull()]
        [AllowEmptyString()]
        $Project,

        [AllowNull()]
        [AllowEmptyString()]
        $CollectionUri
    )

    begin {
        # Collect the urls in a hashset to avoid duplicates
        $workItemUris = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    }

    process {

        # If no artifacts are specified, exit
        if (!$ArtifactUri) {
            return
        }

        ## Artifact Uris
        ## Commit:
        ## vstfs:///Git/Commit/{projectId}%2f{repositoryId}%2f{commitId}
        ## vstfs:///Git/Commit/9d7a1154-1315-433e-96e5-11f160256a1d%2f96e0832a-94a2-4c0c-887e-48b8f3d2e7ed%2f56e74acbbd9a5d5ef5a34f4eb086cb1a0b2140d0
        ## Pull Request:
        ## vstfs:///Git/PullRequestId/{projectId}%2f{repositoryId}%2f{pullRequestId}
        ## vstfs:///Git/PullRequestId/9d7a1154-1315-433e-96e5-11f160256a1d%2f96e0832a-94a2-4c0c-887e-48b8f3d2e7ed%2f8357

        # TODO: Add support for other artifact types

        # Group the ArtifactUris by Project
        $regex = '^vstfs:///Git/(Commit|PullRequestId)/(?<projectId>.+)%2f(?<repositoryId>.+)%2f(?<artifactId>.+)$'

        $artifactUrisByProjectGroups = $ArtifactUri `
        | ForEach-Object {
            # Get the project
            if ($_ -match $regex) {
                # Extract project id from artifact uri
                $currentProject = $Matches['projectId']
            } else {
                # Use the specified $Project
                $currentProject = $Project
            }

            # Make the object to allow for grouping
            [PSCustomObject] @{
                ArtifactUri   = $_
                Project       = $currentProject
                CollectionUri = $null
            }
        } `
        | Group-Object -Property 'Project'

        # Query work items for each project
        $artifactUrisByProjectGroups `
        | ForEach-Object {
            $group = $_

            # Determine $CollectionUri from $Project
            $projectObj = Resolve-ApiProject `
                -Project $group.Name `
                -CollectionUri $CollectionUri

            # If project was found, use the cached data
            if ($projectObj -and $projectObj.Verified) {
                $currentCollectionUri = $projectObj.CollectionUri
                $currentProject = $projectObj.ProjectId
            } elseif (!$group.CollectionUri) {
                $currentCollectionUri = $CollectionUri
                $currentProject = $group.Name
            }

            # Get the project connection
            $connection = Get-ApiProjectConnection `
                -CollectionUri $currentCollectionUri `
                -Project $currentProject

            # POST https://dev.azure.com/{organization}/{project}/_apis/wit/artifacturiquery?api-version=6.0-preview.1
            # {
            # "artifactUris": [
            # "vstfs:///Git/Commit/3065bb47-8344-4370-982a-5183abf197fa%2F649107bd-ab35-4192-8584-601f64172f80%2F4800cfa0be564b1e606d6811e99e0380f765a9c4"
            # ]
            # }
            $uri = Join-Uri `
                -BaseUri $connection.ProjectBaseUri `
                -RelativeUri "_apis/wit/artifacturiquery"

            # API is avaliable only in *-preview.1
            if ($connection.ApiVersion -notlike '*-preview*') {
                $connection.ApiVersion += '-preview.1'
            }

            $body = [PSCustomObject] @{
                artifactUris = @($group.Group.ArtifactUri)
            } | ConvertTo-JsonCustom

            # Make the call
            $response = Invoke-Api `
                -ApiCredential $connection.ApiCredential `
                -ApiVersion $connection.ApiVersion `
                -Uri $uri `
                -Body $body `
                -AsHashtable

            # response
            # {
            # "artifactUrisQueryResult": {
            # "vstfs:///Git/Commit/890669f3...00000001": [
            # {
            # "id": 373878,
            # "url": "https://dev-tfs/tfs/internal_projects/_apis/wit/workitems/373878"
            # },
            # {
            # "id": 373877,
            # "url": "https://dev-tfs/tfs/internal_projects/_apis/wit/workitems/373877"
            # }
            # ]
            # "vstfs:///Git/Commit/890669f3...00000002": [
            # {
            # "id": 373878,
            # "url": "https://dev-tfs/tfs/internal_projects/_apis/wit/workitems/373878"
            # },
            # {
            # "id": 373877,
            # "url": "https://dev-tfs/tfs/internal_projects/_apis/wit/workitems/373877"
            # }
            # ]
            # }
            # }

            # If no results, exit
            if (!$response.artifactUrisQueryResult) {
                continue
            }

            if ($response.artifactUrisQueryResult.Values.Count -lt 1) {
                continue
            }

            # Return results
            $response.artifactUrisQueryResult.GetEnumerator() `
            | ForEach-Object {
                # There may be no WorkItem associated with the artifact uri
                if (!$_.Value) {
                    return
                }

                $_.Value | ForEach-Object {
                    if (![string]::IsNullOrWhiteSpace($_.url)) {
                        if (!$workItemUris.Contains($_.url)) {
                            $null = $workItemUris.Add($_.url)

                            # The WorkItem URL does not contain the project id;
                            # add it
                            $url = Get-WorkItemApiUrl `
                                -CollectionUri $currentCollectionUri `
                                -Project $currentProject `
                                -WorkItem $_.id

                            [PSCustomObject] @{
                                id  = $_.id
                                url = $url
                            } | Write-Output
                        }
                    }
                }
            }
        }
    }
}