AzureDevOpsIngest.psm1
#Requires -Module SimplySQL # Module Constants # GUID used to identify the connection names by SimplySQL Set-Variable ConnectionNames -option Constant -value (@{ Automatic = (New-Guid) Manual = (New-Guid) }) Function ConvertTo-AuthorizationHeader { <# .SYNOPSIS Convert a PSCredential object, containing the user's Personal Access Token, to an Authorization header suitable for Azure DevOps REST API .PARAMETER Credential The PSCredential object to convert Note: The username is ignored .EXAMPLE ConvertTo-AuthorizationHeader -Credential 'Personal Access Token' #> [CmdletBinding()] param( [Parameter(Mandatory, Position=0)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential ) Process { @{ Authorization = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($Credential.GetNetworkCredential().Password)")))" } | Write-Output } } Function Expand-SingleFileArchive { [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ })] [string] $Path, [Parameter(Mandatory=$false)] [switch] $RemoveOnSuccess, [Parameter(Mandatory)] [ref] $FileNameRef ) Process { $Target = Get-Item -Path $Path -ErrorAction Stop $TargetExpand = "$($env:TEMP)\$($Target.BaseName)" $TargetExpandExists = Test-Path $TargetExpand if($TargetExpandExists) { throw "The target directory already exists: $($TargetExpand)." } else { try { # Attempt to expand the archive and to read the content of the single file Expand-Archive -Path $Target.FullName -DestinationPath $TargetExpand -ErrorAction Stop $TargetFiles = Get-ChildItem -Path $TargetExpand -Recurse -File -ErrorAction Stop if($TargetFiles.Count -eq 1) { $FileNameRef.Value = ($TargetFiles | Split-Path -Leaf) $TargetFiles | Get-Content -ErrorAction Stop | Write-Output if($RemoveOnSuccess) { Remove-Item -Path $Target.FullName -ErrorAction Continue } } else { throw "The archive contains more than one single file." } } finally { if(Test-Path $TargetExpand) { # Remove the expanded directory, if exists Remove-Item -Path $TargetExpand -Recurse -Force -ErrorAction SilentlyContinue } } } } } Function Join-Hashtable { <# .SYNOPSIS Join two hashtables .PARAMETER LeftHandSide The first hashtable .PARAMETER RightHandSide The second hashtable .PARAMETER Force If set, the keys in the RightHandSide will overwrite the keys in the LeftHandSide Otherwise an exception is raised in duplicated keys are found .EXAMPLE $LeftHandSide = @{ Key1 = 'Value1' Key2 = 'Value2' } $RightHandSide = @{ Key2 = 'Value3' Key3 = 'Value4' } Join-Hashtable -LeftHandSide $LeftHandSide -RightHandSide $RightHandSide #> [CmdletBinding()] param( [Parameter(Mandatory, Position=0)] [hashtable] $LeftHandSide, [Parameter(Mandatory = $false, Position=1)] [AllowNull()] [hashtable] $RightHandSide, [Parameter(Mandatory = $false)] [switch] $Force ) Process { $Output = @{} foreach($k in $LeftHandSide.Keys) { $Output += @{$k = $LeftHandSide.$k} } if($null -ne $RightHandSide) { foreach($k in $RightHandSide.Keys) { if($Output.Keys -icontains $k) { if($Force) { $Output.$k = $RightHandSide.$k Write-Debug "Hashtable already contained the key '$k' with value '$($LeftHandSide.$k)', it was overwritten by '$($RightHandSide.$k)'." } else { throw "Hashtable already contains the key '$k', the value '$($RightHandSide.$k)' would be ignored." } } else { $Output += @{$k = $RightHandSide.$k} } } } $Output | Write-Output } } Function Join-Uri { <# .SYNOPSIS Join two URIs and validate the result is within the scope of the Base URI .PARAMETER BaseUri The Base URI. Generated URI are guaranteed to be within this scope .PARAMETER RelativeUri An URI relative to the BaseUri .PARAMETER AbsoluteUri An absolute URI .PARAMETER QueryParameters The hashtable containing the query parameters to append to the URI .EXAMPLE Join-Uri -BaseUri 'https://dev.azure.com/EESC-CoR/' -RelativeUri 'MyProject/_apis/wit/workitems' -QueryParameters @{'ids' = '1,2,3'} .EXAMPLE Join-Uri -BaseUri 'https://dev.azure.com/EESC-CoR/' -AbsoluteUri 'https://dev.azure.com/EESC-CoR/MyProject/_apis/wit/workitems' -QueryParameters @{'ids' = '1,2,3'} #> [CmdletBinding(DefaultParameterSetName='relative')] [OutputType([System.Uri])] param( [Parameter(Mandatory,ParameterSetName='relative')] [Parameter(Mandatory=$false,ParameterSetName='absolute')] [System.Uri] $BaseUri, [Parameter(Mandatory,ParameterSetName='relative')] [string] [ValidatePattern("^[^/]")] $RelativeUri, [Parameter(Mandatory,ParameterSetName='absolute')] [System.Uri] $AbsoluteUri, [Parameter(Mandatory=$false)] [AllowNull()] [hashtable] $QueryParameters = @{} ) Process { # Get the target's Absolute Uri, append Parameters, and Validate the generated URI if($AbsoluteUri) { # An absolute Uri is used; if not Base Uri was received, then we take the received Absolute Uri as Base # See below, next to [System.Uri]::TryCreate, for more details if(-not $BaseUri) { $BaseUri = $AbsoluteUri.AbsoluteUri } $TempUserAbsoluteUri = $AbsoluteUri.AbsoluteUri } else { $TempUserAbsoluteUri = "$($BaseUri.AbsoluteUri)$($RelativeUri)" } # Craft the query string $ExtraQueryString = ($QueryParameters.GetEnumerator() | Sort-Object -Property Key | Foreach-Object { "$([uri]::EscapeDataString($_.Key))=$([uri]::EscapeDataString($_.Value))" }) -join '&' if($ExtraQueryString) { if($TempUserAbsoluteUri -match '\?') { $TempUserAbsoluteUri = "$($TempUserAbsoluteUri)&$($ExtraQueryString)" } else { $TempUserAbsoluteUri = "$($TempUserAbsoluteUri)?$($ExtraQueryString)" } } # Discard trailing ? or &, if any $TempUserAbsoluteUri = $TempUserAbsoluteUri -replace '[?&]$' # Validate the generated URI, checking it belongs to the BaseUri # Note on BaseUri: # When using a Relative Uri, [System.Uri]::TryCreate will append the RelativeUri to the Base Uri # When using an Absolute Uri (therefore when Base Uri == Absolute Uri), [System.Uri]::TryCreate only verifies, it doesn't append the # Therefore the following code works irrespective if a Relative or an Absolute Uri was used. $UserUri = "http://test.com#Uri_TryCreate" if( -not ([System.Uri]::TryCreate($BaseUri, $TempUserAbsoluteUri, [ref]$UserUri))) { throw "URI $UserUri is invalid." } # Checking if the generated Uri belongs to the BaseUri if( -not $BaseUri.IsBaseOf($UserUri)) { throw "URI $UserUri is out of scope." } # Return the generated Uri $UserUri | Write-Output } } Function Get-Project { <# .SYNOPSIS Get the list of projects from Azure DevOps. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 .PARAMETER Expand (optional) If set, the cmdlet will return the details of each project by calling Invoke-Api on each project's url .PARAMETER ProjectId The project's ID If set, the cmdlet will return the details of the project with the given ID If omitted, the cmdlet will return the list of all projects .EXAMPLE # Get all the projects of the organization Get-Project -Credential $AzureCredential .EXAMPLE # Get all the details of all the projects (by calling Invoke-Api on each project's url) Get-Project -Credential $AzureCredential | Invoke-Api -Credential $AzureCredential .EXAMPLE $Project = Get-Project -Credential (Get-Credential PAT | Tee-Object -Variable DevOps) #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ParameterSetName = 'Single',ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory=$false,ParameterSetName = 'List')] [switch] $Expand ) Process { $BaseUri = $null if ($PSCmdlet.ParameterSetName -eq 'Single') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -Project -ProjectId $ProjectId } elseif ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -Project } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | Foreach-Object { if($PSCmdlet.ParameterSetName -eq 'Single' ) { $_ | Write-Output } elseif($PSCmdlet.ParameterSetName -eq 'List' ) { $_.Value | ForEach-Object { if($Expand) { Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $_.url | Write-Output } else { $_ | Write-Output } } } else { throw 'Invalid parameter set' } } } } Function Get-ProjectBuild { <# .SYNOPSIS Get the Builds for a project. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectId The project's ID .PARAMETER BuildId The Build's ID .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory,ParameterSetName = 'Single')] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $BuildId ) Process { if ($PSCmdlet.ParameterSetName -eq 'Single') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -Build -BuildId $BuildId } elseif ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -Build } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | ForEach-Object { if($PSCmdlet.ParameterSetName -eq 'Single') { $_ | Write-Output } else { $_.Value | ForEach-Object { $_ | Write-Output } } } } } Function Get-ProjectBuildArtifact { <# .SYNOPSIS Get the Artifacts for a Build for a project. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectId The project's ID .PARAMETER BuildId The Build's ID .PARAMETER ArtifactName The Artifact's ID .PARAMETER Fast If set, the cmdlet will download the artifact in zip format directly If not set, the cmdlet will download the artifact's description in json format, then use the contained url to download the zip file .PARAMETER ReturnContent If set, the cmdlet will return the content of the artifact to the pipeline If not set, the cmdlet will return the downloaded file as a System.IO.FileSystemInfo object .PARAMETER FileNameRef The reference to the filename of the downloaded artifact This parameter is used to return the filename to the caller .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([System.String[]], ParameterSetName = 'SingleWithContent')] [OutputType([System.IO.FileSystemInfo], ParameterSetName = 'Single')] [OutputType([PSCustomObject[]], ParameterSetName = 'List')] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory)] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $BuildId, [Parameter(Mandatory,ParameterSetName = 'Single')] [Parameter(Mandatory,ParameterSetName = 'SingleWithContent')] [string] $ArtifactName, [Parameter(Mandatory=$false,ParameterSetName = 'Single')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleWithContent')] [switch] $Fast, [Parameter(Mandatory,ParameterSetName = 'SingleWithContent')] [switch] $ReturnContent, [Parameter(Mandatory,ParameterSetName = 'SingleWithContent')] [ref] $FileNameRef ) Process { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -BuildId $BuildId -Artifact if ($PSCmdlet.ParameterSetName -in 'Single','SingleWithContent') { $Target = New-TemporaryFile -ErrorAction Stop if($Fast) { <# # Option 1 (faster, but not sure if the $format=zip option is always available) # Requests the CONTENT in zip format. And save to disk #> $QueryParameters = @{ artifactName = $ArtifactName '$format' = 'zip' 'api-version' = $ApiVersion } Invoke-Api -Credential $Credential -AbsoluteUri $AbsoluteUrl -QueryParameters $QueryParameters -OutFile ($Target) -ErrorAction Stop } else { <# # Option 2 (the default) # Requests a DESCRIPTION of the artifact in JSON, then download it #> $QueryParameters = @{ artifactName = $ArtifactName '$format' = 'json' # Optional: $format could be omitted as it is the default 'api-version' = $ApiVersion } $Descriptor = Invoke-Api -Credential $Credential -AbsoluteUri $AbsoluteUrl -QueryParameters $QueryParameters -ErrorAction Stop Invoke-Api -Credential $Credential -AbsoluteUri $Descriptor.resource.downloadUrl -Method Get -OutFile ($Target) -ErrorAction Stop } if($ReturnContent) { try { # Return the content of the one single file found in the downloaded zip archive # Return the name of that file through the FileNameRef parameter Expand-SingleFileArchive -Path $Target.FullName -RemoveOnSuccess -FileNameRef ($FileNameRef) | Write-Output } finally { # Delete the temporary file $Target | Where-Object { $_ | Test-Path -ErrorAction SilentlyContinue } | Remove-Item -ErrorAction SilentlyContinue } } else { # Return the Temporary File info $Target | Write-Output } } elseif ($PSCmdlet.ParameterSetName -eq 'List') { Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | Select-Object -ExpandProperty value | Write-Output } else { throw 'Invalid parameter set' } } } Function Get-ProjectBuildDefinition { <# .SYNOPSIS Get the Build Definitions for a project. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectId The project's ID .PARAMETER BuildDefinitionId The Build Definition's ID .PARAMETER Yaml (optional) If set, the cmdlet will return the configuration in YAML format .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 .EXAMPLE $BuildDefinition = $Project |% { $ProjectId = $_.id [pscustomobject]@{ id=$ProjectId definitions=($_ | Get-ProjectBuildDefinition -Credential $DevOps|%{ try { [pscustomobject]@{ definition = $_ yaml = Get-ProjectBuildDefinition -ProjectId $ProjectId -BuildDefinitionId $_.id -Credential $DevOps -Yaml | select-object -ExpandProperty yaml } } catch { [pscustomobject]@{ definition = $_ yaml = $null error = $_ } } }) }} #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory,ParameterSetName = 'Single')] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $BuildDefinitionId, [Parameter(Mandatory=$false,ParameterSetName = 'Single')] [switch] $Yaml ) Process { if ($PSCmdlet.ParameterSetName -eq 'Single') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -BuildDefinition -BuildDefinitionId $BuildDefinitionId -Yaml:$Yaml } elseif ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -BuildDefinition } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | ForEach-Object { if($PSCmdlet.ParameterSetName -eq 'Single') { $_ | Write-Output } else { $_.Value | ForEach-Object { $_ | Write-Output } } } } } Function Get-ProjectBuildLog { <# .SYNOPSIS Get the Logs of a Build for a project. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectId The project's ID .PARAMETER BuildId The Build's ID .PARAMETER LogId The Log's ID .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory)] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $BuildId, [Parameter(Mandatory,ParameterSetName = 'Single')] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $LogId ) Process { if ($PSCmdlet.ParameterSetName -eq 'Single') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -BuildId $BuildId -Build -Log -LogId $LogId } elseif ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -BuildId $BuildId -Build -Log } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | ForEach-Object { if($PSCmdlet.ParameterSetName -eq 'Single') { $_ | Write-Output } else { $_.Value | ForEach-Object { $_ | Write-Output } } } } } Function Get-ProjectMetrics { <# .SYNOPSIS Get the Build Metrics for a project. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectId The project's ID .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1-preview #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1-preview', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId ) Process { if ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -BuildMetrics } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | Select-Object -ExpandProperty Value | ForEach-Object { $_ | Write-Output } } } Function Get-ProjectPipeline { <# .SYNOPSIS Get the pipelines for a project. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 .PARAMETER ProjectId The project's ID .PARAMETER Expand (optional) If set, the cmdlet will return the details of each pipeline by calling Invoke-Api on each pipeline's url .PARAMETER PipelineId The pipeline's ID .PARAMETER Yaml (optional) If set, the cmdlet will return the content of the YAML configuration of the pipeline instead of the pipeline data The preferred way to obtain the Build definition's YAML is through the Get-ProjectBuildDefinition -Yaml function .EXAMPLE # Get a specific project's pipelines Get-ProjectPipeline -Credential 'Personal Access Token' -id 'MyProject' .EXAMPLE # Get all the pipelines for the first project Get-Project -Credential $AzureCredential | Select-Object -First 1 & Get-ProjectPipeline -Credential $AzureCredential .EXAMPLE $Pipeline = $Project |% { $ProjectId = $_.id [pscustomobject]@{ id=$ProjectId pipelines=($_ | Get-ProjectPipeline -Credential $DevOps -Expand|%{ try { [pscustomobject]@{ pipeline=$_ yaml=Get-ProjectPipeline -ProjectId $ProjectId -PipelineId $_.id -Credential $DevOps -Yaml } } catch { [pscustomobject]@{ pipeline=$_ yaml = $null error = $_ } } }) }} .EXAMPLE $Pipeline = $Project |% { $ProjectId = $_.id [pscustomobject]@{ id=$ProjectId pipelines=($_ | Get-ProjectPipeline -Credential $DevOps -Expand|%{ try { [pscustomobject]@{ pipeline=$_ yaml=Get-ProjectPipeline -ProjectId $ProjectId -PipelineId $_.id -Credential $DevOps -Yaml } } catch { [pscustomobject]@{ pipeline=$_ yaml = $null error = $_ } } }) }} #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory,ParameterSetName = 'Single')] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $PipelineId, [Parameter(Mandatory=$false,ParameterSetName = 'List')] [switch] $Expand, [Parameter(Mandatory=$false,ParameterSetName = 'Single')] [switch] $Yaml ) Process { if ($PSCmdlet.ParameterSetName -eq 'Single') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -Pipeline -PipelineId $PipelineId } elseif ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -Pipeline } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | ForEach-Object { if($PSCmdlet.ParameterSetName -eq 'Single') { if($Yaml) { if(-not ($_.configuration)) { "Pipeline ($ProjectId/$PipelineId) doesn't contain a configuration property." | Write-Warning } elseif( -not ($_.configuration.type)) { "Pipeline ($ProjectId/$PipelineId): configuration doesn't contain a type property." | Write-Warning } elseif( -not ($_.configuration.type -eq 'yaml')) { "Pipeline ($ProjectId/$PipelineId): configuration.type is not supported ('$($_.type)')." | Write-Verbose } elseif( -not ($_.configuration.repository)) { "Pipeline ($ProjectId/$PipelineId): configuration doesn't contain a repository property." | Write-Warning } elseif( -not ($_.configuration.path)) { "Pipeline ($ProjectId/$PipelineId): configuration doesn't contain a path property." | Write-Warning } elseif( -not ($_.configuration.repository.type -eq 'azureReposGit')) { "Pipeline ($ProjectId/$PipelineId): configuration.repository.type is not supported ('$($_.repository.type)')." | Write-Verbose } else { $OldErrorView = $ErrorView try { Get-RepositoryItem -Credential $Credential -ProjectId $ProjectId -RepositoryId ($_.configuration.repository.id) -Path ($_.configuration.path) | Write-Output } catch { $ErrorView = [System.Management.Automation.ErrorView]::CategoryView "Pipeline ($ProjectId/$PipelineId): Error retrieving the YAML file ('$($_ | Select-Object -ExpandProperty ErrorDetails | Select-Object -ExpandProperty Message | ConvertFrom-Json | Select-Object -ExpandProperty message)')." | Write-Warning throw $_ } finally { $ErrorView = $OldErrorView } } } else { $_ | Write-Output } } else { $_.Value | ForEach-Object { if($Expand) { Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $_.url | Write-Output } else { $_ | Write-Output } } } } } } Function Get-ProjectPipelineRun { <# .SYNOPSIS Get the Runs for a project pipeline. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectId The project's ID .PARAMETER PipelineId The pipeline's ID .PARAMETER RunId The run ID .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory)] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $PipelineId, [Parameter(Mandatory,ParameterSetName = 'Single')] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $RunId ) Process { if ($PSCmdlet.ParameterSetName -eq 'Single') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -PipelineId $PipelineId -Run -RunId $RunId } elseif ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -PipelineId $PipelineId -Run } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | ForEach-Object { if($PSCmdlet.ParameterSetName -eq 'Single') { $_ | Write-Output } else { $_.Value | ForEach-Object { $_ | Write-Output } } } } } Function Get-ProjectPipelineRunLog { <# .SYNOPSIS Get the Runs for a project pipeline. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectId The project's ID .PARAMETER PipelineId The pipeline's ID .PARAMETER RunId The run ID .PARAMETER LogId The Log ID .PARAMETER GetContent If set, the cmdlet will return the content of the log .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory)] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $PipelineId, [Parameter(Mandatory)] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $RunId, [Parameter(Mandatory,ParameterSetName = 'Single')] [ValidateScript({ [int]::TryParse($_, $([ref][int]::Empty)) })] [string] $LogId <# # Removing -GetContent # Instead, Always fetch the log's content # This is to align with the behaviour of Build Logs (where the content is retrieved instead of a json object) # # To re-enable, modify the following piece of code inside the Process{} block: # if($PSCmdlet.ParameterSetName -eq 'Single') { # [CODE] # } else { # by: # if($PSCmdlet.ParameterSetName -eq 'Single') { # = Unchanged # if($GetContent) { # < Added # [CODE] # = Unchanged # } else { # < Added # $_ | Write-Output # < Added # } # < Added # } else { # = Unchanged [Parameter(Mandatory=$false,ParameterSetName = 'Single')] [switch] $GetContent #> ) Process { if ($PSCmdlet.ParameterSetName -eq 'Single') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -PipelineId $PipelineId -RunId $RunId -Log -LogId $LogId } elseif ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -PipelineId $PipelineId -RunId $RunId -Log } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | ForEach-Object { if($PSCmdlet.ParameterSetName -eq 'Single') { # Retrieve a Temporary and Public URL for the log Invoke-Api -AbsoluteUri $_.url -QueryParameters @{'$expand' = 'signedContent';'api-version' = $ApiVersion} -Credential $Credential | Foreach-Object { # Then retrieve the log's content using that public URI Invoke-WebRequest $_.signedContent.url | Select-Object -ExpandProperty Content | Write-Output } } else { $_.logs | ForEach-Object { $_ | Write-Output } } } } } Function Get-ProjectRepository { <# .SYNOPSIS Get the repositories for a project. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectId The project's ID .PARAMETER RepositoryId The repository's ID .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 .EXAMPLE # Get a specific project's repositories Get-ProjectRepository -Credential 'Personal Access Token' -id 'MyProject' .EXAMPLE # Get all the repositories for the first project Get-Project -Credential $AzureCredential | Select-Object -First 1 | Get-ProjectRepository -Credential $AzureCredential #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory,ParameterSetName = 'Single')] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [string] $RepositoryId ) Process { if ($PSCmdlet.ParameterSetName -eq 'Single') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -Repository -RepositoryId $RepositoryId } elseif ($PSCmdlet.ParameterSetName -eq 'List') { $AbsoluteUrl = Resolve-ApiUri -Organization $Organization -ProjectId $ProjectId -Repository } else { throw 'Invalid parameter set' } Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} -AbsoluteUri $AbsoluteUrl | ForEach-Object { if($PSCmdlet.ParameterSetName -eq 'Single') { $_ | Write-Output } else { $_.Value | ForEach-Object { $_ | Write-Output } } } } } Function Get-RepositoryItem { <# .SYNOPSIS Get the content of a repository item. .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 .PARAMETER ProjectId The project's ID .PARAMETER RepositoryId The repository ID .PARAMETER Path The path to the item in the repository #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('id')] [string] $ProjectId, [Parameter(Mandatory)] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [string] $RepositoryId, [Parameter(Mandatory)] [string] $Path ) Process { Get-ProjectRepository -ProjectId $ProjectId -RepositoryId $RepositoryId -Credential $Credential -Organization $Organization -ApiVersion $ApiVersion | Select-Object -ExpandProperty _links | Select-Object -ExpandProperty items | Select-Object -ExpandProperty href | ForEach-Object { $QueryString = @{ path = $Path } Invoke-Api -AbsoluteUri $_ -QueryParameters $QueryString -Credential $Credential -ErrorAction Stop | Write-Output } } } Function Invoke-Api { <# .SYNOPSIS Invoke the Azure DevOps REST API .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER RelativeUri The REST API's endpoint URI, relative to the the Azure DevOps API's root Note: The URI must not start with a slash Note: RelativeUri is only available for the Azure DevOps Rest API. Not for its Test Rest API nor Release Rest API! .PARAMETER AbsoluteUri The absolute REST API's endpoint URI .PARAMETER QueryParameters The hashtable containing the query parameters to append to the URI .PARAMETER HttpHeaders The hashtable containing the HTTP headers to append to the request .PARAMETER Method The HTTP method to use Default: Get .PARAMETER ContentType The content type of the request Default: application/json .PARAMETER OutFile If set, the response will be saved to the file specified by this parameter instead of being returned to the pipeline .EXAMPLE Invoke-Api -Credential 'Personal Access Token' -RelativeUri 'MyProject/_apis/wit/workitems' -QueryParameters @{'ids' = '1,2,3'} .EXAMPLE Invoke-Api -Credential 'Personal Access Token' -AbsoluteUri 'https://dev.azure.com/EESC-CoR/MyProject/_apis/wit/workitems' -QueryParameters @{'ids' = '1,2,3'} .EXAMPLE # Get all the details of all the repositories for all the projects Get-Project -Credential $AzureCredential | Get-ProjectRepository -Credential $AzureCredential | Invoke-Api -Credential $AzureCredential #> [CmdletBinding(DefaultParameterSetName='Pipeline')] [OutputType([PSCustomObject], ParameterSetName='Pipeline')] [OutputType([System.Void], ParameterSetName='File')] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [Alias('url','uri')] [System.Uri] $AbsoluteUri = '', [Parameter(Mandatory=$false)] [Alias('query')] [AllowNull()] [hashtable] $QueryParameters = @{}, [Parameter(Mandatory=$false)] [Alias('header')] [AllowNull()] [hashtable] $HttpHeaders = @{}, [Parameter(Mandatory = $false)] [Microsoft.PowerShell.Commands.WebRequestMethod] $Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Get, <# [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1', #> [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [string] $ContentType = 'application/json', [Parameter(Mandatory,ParameterSetName='File')] [string] $OutFile ) Process { # Build the request Uri, adding the query parameters and the API version <# $QueryParametersWithApiVersion = (Join-Hashtable -LeftHandSide @{'api-version' = $ApiVersion} -RightHandSide $QueryParameters) $UserUri = Join-Uri -AbsoluteUri $AbsoluteUri.AbsoluteUri -QueryParameters $QueryParametersWithApiVersion #> $UserUri = Join-Uri -AbsoluteUri $AbsoluteUri.AbsoluteUri -QueryParameters $QueryParameters "Calling Azure DevOps API with Uri [$UserUri]" | Write-Debug # Build the request headers, adding the Authorization header and the custom headers $Headers = Join-Hashtable -LeftHandSide (ConvertTo-AuthorizationHeader -Credential $Credential) -RightHandSide $HttpHeaders "Calling Azure DevOps API with the following headers [$(($Headers.GetEnumerator() | Foreach-Object {"{$($_.key) = $($_.value)}"}) -join ', ')]" | Write-Debug # Put the HTTP request arguments together $InvokeRestMethodArguments = @{ Method = $Method Uri = $UserUri Headers = $Headers } if($ContentType -ne [string]::Empty -and $ContentType -ne $null) { $InvokeRestMethodArguments.Add('ContentType', $ContentType) } if($OutFile) { $InvokeRestMethodArguments.Add('OutFile', $OutFile) } # Invoke the REST API if($OutFile) { # Output is stored to a file, nothing is returned to the pipeline Invoke-RestMethod @InvokeRestMethodArguments | Out-Null } else { Invoke-RestMethod @InvokeRestMethodArguments | Write-Output } } } Function Read-DevOps { <# .SYNOPSIS Reads Azure DevOps' Projects, Repositories and Pipelines .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .EXAMPLE # Reads Azure DevOps' Projects, Repositories and Pipelines Read-DevOps -Credential $PAT #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] [Alias('AzureDevOpsCredential','PAT')] $Credential ) # Load the list of Projects and their Repositories and Pipelines Get-Project -Credential $Credential | Foreach-Object { [pscustomobject]@{ Project = $_ Pipeline = $_ | Get-ProjectPipeline -Credential $Credential | Invoke-Api -Credential $Credential Repository = $_ | Get-ProjectRepository -Credential $Credential | Invoke-Api -Credential $Credential Yaml = @() } } | Foreach-Object { # Load the Yaml for each Pipeline (if any) $Project = $_ $_.Yaml = $Project.Pipeline | Where-Object { $_.configuration.type -eq 'yaml' } | Foreach-Object { $p = $_ $Project.Repository | Where-Object { $_.id -eq $p.configuration.repository.id } | Foreach-Object { # Prepare the output object (Pipeline, Repository, empty Yaml, empty YamlTemplate list) [pscustomobject]@{ Pipeline = $p Repository = $_ Yaml = $null YamlTemplate = $null } | Write-Output } } | Foreach-Object { $ItemUri = $_.Repository._links.items.href $ItemPath = $_.Pipeline.configuration.path try { # Load the Yaml for the Pipeline $_.Yaml = Invoke-Api -Credential $Credential -AbsoluteUri $ItemUri -QueryParameters @{path = $ItemPath} # Compute the list of imported Templates in the Yaml $_.YamlTemplate = ($_.Yaml -split "`n" | Where-Object {$_ -match '^\s*- template:.*?@'}|Foreach-Object{$_ -replace '^.*?@' }|Where-Object{$_ -notmatch 'self'}|Select-Object -Unique) $_ | Write-Output } catch { $_.ToString() | Write-Warning } } $_ | Write-Output } } Function Resolve-ApiBaseUri { <# .SYNOPSIS #> [CmdletBinding(DefaultParameterSetName = 'DevOps')] [OutputType([string])] param( [Parameter(Mandatory=$false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory=$false,ParameterSetName = 'DevOps')] [switch] $DevOpsApi, [Parameter(Mandatory,ParameterSetName = 'Test')] [switch] $TestApi, [Parameter(Mandatory,ParameterSetName = 'Release')] [switch] $ReleaseApi ) Begin { $AzureDevOpsUri = @{ DevOpsApi = 'https://dev.azure.com' ReleaseApi = 'https://vsrm.dev.azure.com' TestApi = 'https://vstmr.dev.azure.com' } } Process { $BaseUri = switch($PSCmdlet.ParameterSetName) { 'DevOps' { $AzureDevOpsUri.DevOpsApi } 'Test' { $AzureDevOpsUri.TestApi } 'Release' { $AzureDevOpsUri.ReleaseApi } default { throw 'Invalid parameter set' } } "$BaseUri/$Organization/" | Write-Output } } Function Resolve-ApiUri { <# .SYNOPSIS #> [CmdletBinding(DefaultParameterSetName = 'ProjectList')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory=$false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory=$false,ParameterSetName = 'ProjectList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleProject')] [switch] $Project, [Parameter(Mandatory,ParameterSetName = 'SingleProject')] [Parameter(Mandatory,ParameterSetName = 'RepositoryList')] [Parameter(Mandatory,ParameterSetName = 'SingleRepository')] [Parameter(Mandatory,ParameterSetName = 'PipelineList')] [Parameter(Mandatory,ParameterSetName = 'SinglePipeline')] [Parameter(Mandatory,ParameterSetName = 'RunList')] [Parameter(Mandatory,ParameterSetName = 'SingleRun')] [Parameter(Mandatory,ParameterSetName = 'RunLogList')] [Parameter(Mandatory,ParameterSetName = 'SingleRunLog')] [Parameter(Mandatory,ParameterSetName = 'BuildList')] [Parameter(Mandatory,ParameterSetName = 'SingleBuild')] [Parameter(Mandatory,ParameterSetName = 'BuildDefinitionList')] [Parameter(Mandatory,ParameterSetName = 'SingleBuildDefinition')] [Parameter(Mandatory,ParameterSetName = 'SingleBuildDefinitionYaml')] [Parameter(Mandatory,ParameterSetName = 'BuildLogList')] [Parameter(Mandatory,ParameterSetName = 'SingleBuildLog')] [Parameter(Mandatory,ParameterSetName = 'BuildMetrics')] [Parameter(Mandatory,ParameterSetName = 'BuildArtifactList')] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [string] $ProjectId, [Parameter(Mandatory,ParameterSetName = 'BuildMetrics')] [switch] $BuildMetrics, [Parameter(Mandatory,ParameterSetName = 'RepositoryList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleRepository')] [switch] $Repository, [Parameter(Mandatory,ParameterSetName = 'SingleRepository')] [ValidateScript({ [guid]::TryParse($_, $([ref][guid]::Empty)) })] [string] $RepositoryId, [Parameter(Mandatory,ParameterSetName = 'PipelineList')] [Parameter(Mandatory=$false,ParameterSetName = 'SinglePipeline')] [switch] $Pipeline, [Parameter(Mandatory,ParameterSetName = 'SinglePipeline')] [Parameter(Mandatory,ParameterSetName = 'RunList')] [Parameter(Mandatory,ParameterSetName = 'SingleRun')] [Parameter(Mandatory,ParameterSetName = 'RunLogList')] [Parameter(Mandatory,ParameterSetName = 'SingleRunLog')] [long] $PipelineId, [Parameter(Mandatory,ParameterSetName = 'RunList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleRun')] [Parameter(Mandatory=$false,ParameterSetName = 'RunLogList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleRunLog')] [switch] $Run, [Parameter(Mandatory,ParameterSetName = 'SingleRun')] [Parameter(Mandatory,ParameterSetName = 'RunLogList')] [Parameter(Mandatory,ParameterSetName = 'SingleRunLog')] [long] $RunId, [Parameter(Mandatory,ParameterSetName = 'BuildList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleBuild')] [Parameter(Mandatory=$false,ParameterSetName = 'BuildLogList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleBuildLog')] [Parameter(Mandatory=$false,ParameterSetName = 'BuildArtifactList')] [switch] $Build, [Parameter(Mandatory,ParameterSetName = 'SingleBuild')] [Parameter(Mandatory,ParameterSetName = 'BuildLogList')] [Parameter(Mandatory,ParameterSetName = 'SingleBuildLog')] [Parameter(Mandatory,ParameterSetName = 'BuildArtifactList')] [long] $BuildId, [Parameter(Mandatory,ParameterSetName = 'BuildDefinitionList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleBuildDefinition')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleBuildDefinitionYaml')] [switch] $BuildDefinition, [Parameter(Mandatory,ParameterSetName = 'SingleBuildDefinition')] [Parameter(Mandatory,ParameterSetName = 'SingleBuildDefinitionYaml')] [long] $BuildDefinitionId, [Parameter(Mandatory,ParameterSetName = 'RunLogList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleRunLog')] [Parameter(Mandatory,ParameterSetName = 'BuildLogList')] [Parameter(Mandatory=$false,ParameterSetName = 'SingleBuildLog')] [switch] $Log, [Parameter(Mandatory,ParameterSetName = 'SingleRunLog')] [Parameter(Mandatory,ParameterSetName = 'SingleBuildLog')] [long] $LogId, [Parameter(Mandatory,ParameterSetName = 'BuildArtifactList')] [switch] $Artifact, [Parameter(Mandatory,ParameterSetName = 'SingleBuildDefinitionYaml')] [switch] $Yaml ) Process { # Set the default to the Azure DevOps API $BaseUri = Resolve-ApiBaseUri -DevOpsApi -Organization $Organization # Build the relative Uri $RelativeUri = switch($PSCmdlet.ParameterSetName) { 'ProjectList' { '_apis/projects' } 'SingleProject' { "_apis/projects/$($ProjectId)" } 'BuildMetrics' { "$($ProjectId)/_apis/build/Metrics" } 'RepositoryList' { "$($ProjectId)/_apis/git/repositories" } 'SingleRepository' { "$($ProjectId)/_apis/git/repositories/$($RepositoryId)" } 'PipelineList' { "$($ProjectId)/_apis/pipelines" } 'SinglePipeline' { "$($ProjectId)/_apis/pipelines/$($PipelineId)" } 'RunList' { "$($ProjectId)/_apis/pipelines/$($PipelineId)/runs" } 'SingleRun' { "$($ProjectId)/_apis/pipelines/$($PipelineId)/runs/$($RunId)" } 'RunLogList' { if($Log) { "$($ProjectId)/_apis/pipelines/$($PipelineId)/runs/$($RunId)/logs" } else { "$($ProjectId)/_apis/pipelines/$($PipelineId)/runs/$($RunId)" } } 'SingleRunLog' { "$($ProjectId)/_apis/pipelines/$($PipelineId)/runs/$($RunId)/logs/$($LogId)" } 'BuildList' { "$($ProjectId)/_apis/build/builds" } 'SingleBuild' { "$($ProjectId)/_apis/build/builds/$($BuildId)" } 'BuildLogList' { if($Log) { "$($ProjectId)/_apis/build/builds/$($BuildId)/logs" } else { "$($ProjectId)/_apis/build/builds/$($BuildId)" } } 'SingleBuildLog' { "$($ProjectId)/_apis/build/builds/$($BuildId)/logs/$($LogId)" } 'BuildArtifactList' { if($Artifact) { "$($ProjectId)/_apis/build/builds/$($BuildId)/artifacts" } else { "$($ProjectId)/_apis/build/builds/$($BuildId)" } } 'BuildDefinitionList' { "$($ProjectId)/_apis/build/definitions" } 'SingleBuildDefinition' { "$($ProjectId)/_apis/build/definitions/$($BuildDefinitionId)" } 'SingleBuildDefinitionYaml' { if($Yaml) { "$($ProjectId)/_apis/build/definitions/$($BuildDefinitionId)/yaml" } else { "$($ProjectId)/_apis/build/definitions/$($BuildDefinitionId)" } } 'ReleaseList' { # Use the Azure DevOps' Release API $BaseUri = Resolve-ApiBaseUri -ReleaseApi -Organization $Organization "$($ProjectId)/_apis/release/releases" } 'SingleRelease' { # Use the Azure DevOps' Release API $BaseUri = Resolve-ApiBaseUri -ReleaseApi -Organization $Organization "$($ProjectId)/_apis/release/releases/$($ReleaseId)" } default { throw 'Invalid parameter set' } } Join-Uri -BaseUri $BaseUri -RelativeUri $RelativeUri | Select-Object -ExpandProperty AbsoluteUri | Write-Output } } Function Resolve-Project { <# .SYNOPSIS Resolve a Project URI, returning its absolute - non aliased - API friendly - URI .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER ProjectUri The Project URI to resolve .PARAMETER Name The Project Name to resolve .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 .EXAMPLE # Resolve the Project URIs @( 'https://dev.azure.com/EESC-CoR/_git/MembersPortal?path=/README.md' 'https://EESC-CoR@dev.azure.com/EESC-CoR/DM2016/_git/Scripts' 'https://dev.azure.com/EESC-CoR/_git/DM2016' ) | Resolve-Repository -Credential $AzureCredential #> [CmdletBinding(DefaultParameterSetName = 'Uri')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName = 'Uri')] [ValidateScript({ # Validate that the URL is a valid Azure DevOps Project URL # e.g.: https://dev.azure.com/Organization/ProjectName $_.DnsSafeHost -eq 'dev.azure.com' -and $_.LocalPath -match '^/[^/]+/[^/]+/?$' })] [Alias('url')] [System.Uri] $ProjectUri, [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName = 'Name')] [Alias('name')] [string] $ProjectName, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1' ) Process { switch($PSCmdlet.ParameterSetName) { 'Name' { $Uri = "https://dev.azure.com/$($Organization)/_apis/projects/$($ProjectName)" } 'Uri' { $Uri = $ProjectUri.AbsoluteUri # Discard query string arguments $Uri = $Uri -replace '\?.*$','' # Discard the trailing slash $Uri = $Uri -replace '/$','' # Rewrite Uri into an request to the Projects API $Uri = $Uri -replace "^(https?://.*?/[^/]+)/([^/]+)/?$",'$1/_apis/projects/$2' } Default { throw "Invalid ParameterSetName: $($PSCmdlet.ParameterSetName)" } } "Lookup for a Project matching URL '$($Uri)' in the '$($Organization)' Azure DevOps Organization" | Write-Debug Invoke-Api -Credential $Credential -AbsoluteUri $Uri -QueryParameters @{'api-version' = $ApiVersion} | # Get the project details Write-Output } } Function Resolve-Repository { <# .SYNOPSIS Resolve a Repository URI, returning its absolute - non aliased - API friendly - URI .PARAMETER Credential The Personal Access Token in the form of a PSCredential object Note: The username is ignored .PARAMETER RepositoryUri The Repository URI to resolve .PARAMETER Organization (optional) The Azure DevOps organization Default: EESC-CoR .PARAMETER ApiVersion (optional) The Azure DevOps API version Default: 7.1 .EXAMPLE # Resolve the Repository URI @( 'https://dev.azure.com/EESC-CoR/_git/MembersPortal?path=/README.md' 'https://EESC-CoR@dev.azure.com/EESC-CoR/DM2016/_git/Scripts' 'https://dev.azure.com/EESC-CoR/_git/DM2016' ) | Resolve-Repository -Credential $AzureCredential #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [System.Management.Automation.Credential()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] [ValidateScript({ # Validate that the URL is a valid Azure DevOps Repository URL # e.g.: https://dev.azure.com/Organization/_git/ProjectName # e.g.: https://dev.azure.com/Organization/ProjectName/_git/RepositoryName $_.DnsSafeHost -eq 'dev.azure.com' -and $_.LocalPath -match '^/[^/]+/([^/]+/)?_git/[^/]+/?$' })] [Alias('url')] [System.Uri] $RepositoryUri, [Parameter(Mandatory = $false)] [string] $Organization = 'EESC-CoR', [Parameter(Mandatory = $false)] [string] $ApiVersion = '7.1' ) Process { $Uri = $RepositoryUri.AbsoluteUri # Repository's WebUrl accepts query string arguments (e.g. to locate a specific file), discard them $Uri = $Uri -replace '\?.*$','' # Remove the username bit from the URI (there is one, when the user copied the "remoteUrl" of the repo instead of its "webUrl" $Uri = $Uri -replace "^(https?://)$($Organization)@",'$1' # Project all have a primary repository, with the same name as the project name # That repository can be referenced by both: https://AzureUrl/Organization/_git/ProjectName as well as https://AzureUrl/Organization/ProjectName/_git/RepoName $Uri = $Uri -replace "^(.*?/$($Organization))/_git/(.*)$",'$1/$2/_git/$2' "Lookup for Repository matching URL '$($Uri)' in the '$($Organization)' Azure DevOps Organization" | Write-Debug try { # Extract the project URL from the repository URL, and resolve it $Project = $Uri -replace '/_git/.*$','' | Resolve-Project -Credential $Credential -Organization $Organization -ApiVersion $ApiVersion -ErrorAction Stop } catch { # Resolution failed, get all the projects $Project = Get-Project -Credential $Credential } finally { # Lookup for the repository inside the project $Project | Get-ProjectRepository -Credential $Credential | # Get the Repositories in the project Where-Object { $_.webUrl -eq $Uri # Check if the repository matches the one we are searching for } | Select-Object -First 1 | # Interrupts the loop as soon as the first match is found Invoke-Api -Credential $Credential -QueryParameters @{'api-version' = $ApiVersion} | # Get the repository details Write-Output } } } |