ADOPS.psm1

#region PreCode _PreModule_Requires

#Requires -Modules @{ ModuleName="AzAuth"; ModuleVersion="2.2.2" }

$script:AzTokenCache = 'adops.cache'

$script:loginMethod = 'Default'
#endregion PreCode _PreModule_Requires

#region GitAccessLevels

[Flags()] enum AccessLevels {
    Administer              = 1
    GenericRead             = 2
    GenericContribute       = 4
    ForcePush               = 8
    CreateBranch            = 16
    CreateTag               = 32
    ManageNote              = 64
    PolicyExempt            = 128
    CreateRepository        = 256
    DeleteRepository        = 512
    RenameRepository        = 1024
    EditPolicies            = 2048
    RemoveOthersLocks       = 4096
    ManagePermissions       = 8192
    PullRequestContribute   = 16384
    PullRequestBypassPolicy = 32768
}
#endregion GitAccessLevels

#region ResourceType

enum ResourceType {
    VariableGroup
    Queue
    SecureFile
    Environment
}
#endregion ResourceType

#region SkipTest

class SkipTest : Attribute {
    [string[]]$TestNames

    SkipTest([string[]]$Name)
    {
        $this.TestNames = $Name
    }
}
#endregion SkipTest

#region ConvertRetentionSettingsGetToPatch

<#
.SYNOPSIS
Convert to responses from GET _apis/build/retention & PATCH _apis/build/retention
into the PATCH _apis/build/retention body property names.
 
.DESCRIPTION
Below are the GET & PATCH _apis/build/retention request/respones.
 
Notice the GET and PATCH responses are the same, while the PATCH body uses different property names.
This function will convert the GET/PATCH responses to match the PATCH request body properties.
 
GET _apis/build/retention
--
{
    "purgeArtifacts": { "min": 1, "max": 60, "value": 51 },
    "purgeRuns": { "min": 1, "max": 60, "value": 51 },
    "purgePullRequestRuns": { "min": 1, "max": 60, "value": 51 },
    "retainRunsPerProtectedBranch": null
}
 
POST _apis/build/retention
Request Body:
{
    "artifactsRetention": { "min": 1, "max": 60, "value": 51 },
    "runRetention": { "min": 1, "max": 60, "value": 51 },
    "pullRequestRunRetention": { "min": 1, "max": 60, "value": 51 },
    "retainRunsPerProtectedBranch": { "min": 1, "max": 60, "value": 51 },
}
 
Response Body:
{
    "purgeArtifacts": { "min": 1, "max": 60, "value": 51 },
    "purgeRuns": { "min": 1, "max": 60, "value": 51 },
    "purgePullRequestRuns": { "min": 1, "max": 60, "value": 51 },
    "retainRunsPerProtectedBranch": null
}
 
.NOTES
Research notes aligning UX, GET, PATCH fields:
 
@{
    # UX Label: Days to keep artifacts, symbols and attachments
    # UX Field: PurgeArtifacts
    # Get Field: purgeArtifacts
    # Patch Field: artifactsRetention
    purgeArtifacts = @{
        min = 1
        max = 60
        value = 51
    }
 
    # UX Label: Days to keep runs
    # UX Field: PurgeRuns
    # Get Field: purgeRuns
    # Patch Field: runRetention
    purgeRuns = @{
        min = 30
        max = 731
        value = 37
    }
 
    # UX Label: Days to keep pull request runs
    # UX Field: PurgePullRequestRuns
    # Get Field: purgePullRequestRuns
    # Patch Field: pullRequestRunRetention
    purgePullRequestRuns = @{
        min = 1
        max = 30
        value = 4
    }
 
    # UX Label: Number of recent runs to retain per pipeline
    # UX Help Label: This many runs will also be retained per protected branch and default pipeline branch. (Azure Repos only)
    # UX Field: runsToRetainPerProtectedBranch
    # Get Field: retainRunsPerProtectedBranch
    # Patch Field: retainRunsPerProtectedBranch
    # BUG: Always null on return
    retainRunsPerProtectedBranch = @{
        min = 0
        max = 50
        value = 0
    }
}
#>

function ConvertRetentionSettingsGetToPatch {
    [CmdletBinding()]
    [SkipTest('HasOrganizationParameter')]
    param (
        [Parameter(Mandatory)]
        $Response
    )

    $Settings = @{}

    $FieldMap = @{
        'purgeArtifacts'               = 'artifactsRetention'
        'purgeRuns'                    = 'runRetention'
        'purgePullRequestRuns'         = 'pullRequestRunRetention'

        # Note: This field is bugged, it's always NULL on GET/PATCH response, I think its meant to be runsToRetainPerProtectedBranch
        'retainRunsPerProtectedBranch' = 'retainRunsPerProtectedBranch' 
    }
    $Fields = $Response.psobject.Properties | Where-object Name -in $FieldMap.Keys 

    foreach ($Field in $Fields) {
        $Settings.$($FieldMap[$Field.Name]) = $Field.Value.value
    }

    [pscustomobject]$Settings
}
#endregion ConvertRetentionSettingsGetToPatch

#region ConvertRetentionSettingsToPatchBody

<#
.SYNOPSIS
Converts Retention Settings Dictionary<string, int> into RetentionSetting objects
 
.DESCRIPTION
Converts Retention Settings Dictionary<string, int> into RetentionSetting objects.
 
.PARAMETER Values
Keyed dictionary of integers
 
Example:
@{
    artifactsRetention = 51
    runRetention = 51,
    ...
}
 
.OUTPUTS
@{
    artifactsRetention = @{
        min = 0,
        max = 0,
        value = 51
    },
    runRetention = @{
        min = 0,
        max = 0,
        value = 51
    },
    ...
}
#>

function ConvertRetentionSettingsToPatchBody {
    [CmdletBinding()]
    [SkipTest('HasOrganizationParameter')]
    param (
        [Parameter(Mandatory)]
        $Values
    )

    $Settings = @{}
    if ($Values -is [pscustomobject]) {
        foreach ($ValueProperty in $Values.psobject.Properties) {
            $Settings[$ValueProperty.Name] = @{
                value = $ValueProperty.Value
                min   = $null
                max   = $null
            }
        }
    }
    else {
        foreach ($Value in $Values.GetEnumerator()) {
            $Settings[$Value.key] = @{
                value = $Value.value
                min   = $null
                max   = $null
            }
        }
    }

    [pscustomobject]$Settings
}
#endregion ConvertRetentionSettingsToPatchBody

#region GetADOPSConfigFile

function GetADOPSConfigFile {
    param (
        [Parameter()]
        [string]$ConfigPath = '~/.ADOPS/Config.json'
    )
    
    # Create config if not exists
    if (-not (Test-Path $ConfigPath)) {
        NewADOPSConfigFile
    }
    
    Get-Content $ConfigPath | ConvertFrom-Json -AsHashtable
}
#endregion GetADOPSConfigFile

#region GetADOPSDefaultOrganization

function GetADOPSDefaultOrganization {
    [CmdletBinding()]
    [SkipTest('HasOrganizationParameter')]
    param ()

    $ADOPSConfig = GetADOPSConfigFile

    if([string]::IsNullOrWhiteSpace($ADOPSConfig['Default']['Organization'])) {
        throw 'No default organization found! Use Connect-ADOPS or set Organization parameter.'
    }
    else {
        Write-Output $ADOPSConfig['Default']['Organization']
    }
}
#endregion GetADOPSDefaultOrganization

#region GetADOPSOrganizationAccess

function GetADOPSOrganizationAccess {
    [CmdletBinding()]
    [SkipTest('HasOrganizationParameter')]
    param (
        [Parameter(Mandatory)]
        [string]$AccountId,
        
        [Parameter()]
        [string]$Token
    )

    (InvokeADOPSRestMethod -Method GET -Token $Token -Uri "https://app.vssps.visualstudio.com/_apis/accounts?memberId=$AccountId&api-version=7.1-preview.1").value.accountName
}
#endregion GetADOPSOrganizationAccess

#region InvokeADOPSRestMethod

function InvokeADOPSRestMethod {
    [SkipTest('HasOrganizationParameter')]
    param (
        [Parameter(Mandatory)]
        [URI]$Uri,

        [Parameter()]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,

        [Parameter()]
        [string]$Body,

        [Parameter()]
        [string]$ContentType = 'application/json',

        [Parameter()]
        [switch]$FullResponse,

        [Parameter()]
        [string]$OutFile,

        [Parameter()]
        [string]$Token
    )
    
    if (-not $PSBoundParameters.ContainsKey('Token')) {
        $Token = (NewAzToken).Token
    }

    $InvokeSplat = @{
        'Uri'         = $Uri
        'Method'      = $Method
        'Headers'     = @{
            'Authorization' = "Bearer $Token"
        }
        'ContentType' = $ContentType
    }

    if (-not [string]::IsNullOrEmpty($Body)) {
        $InvokeSplat.Add('Body', $Body)
    }

    if ($FullResponse) {
        $InvokeSplat.Add('ResponseHeadersVariable', 'ResponseHeaders')
        $InvokeSplat.Add('StatusCodeVariable', 'ResponseStatusCode')
    }

    if ($OutFile) {
        Write-Debug "$Method $Uri"
        Invoke-RestMethod @InvokeSplat -OutFile $OutFile
    }
    else {
        Write-Debug "$Method $Uri"
        $Result = Invoke-RestMethod @InvokeSplat

        if ($Result -like "*Azure DevOps Services | Sign In*") {
            throw 'Failed to call Azure DevOps API. Please login using Connect-ADOPS before running commands.'
        }
        elseif ($FullResponse) {
            @{ Content = $Result; Headers = $ResponseHeaders; StatusCode = $ResponseStatusCode }
        }
        else {
            $Result
        }
    }
}
#endregion InvokeADOPSRestMethod

#region NewADOPSConfigFile

function NewADOPSConfigFile {
    param (
        [Parameter()]
        [string]$ConfigPath = '~/.ADOPS/Config.json'
    )

    @{
        'Default' = @{}
    } | SetADOPSConfigFile -ConfigPath $ConfigPath
}
#endregion NewADOPSConfigFile

#region NewAzToken

function NewAzToken {
    [CmdletBinding()]
    [SkipTest('HasOrganizationParameter')]
    param ()

    $TokenSplat = @{
        Resource = '499b84ac-1321-427f-aa17-267ca6975798'
    }
    switch ($script:LoginMethod) {
        'Default' {
            try {
                $UserContext = GetADOPSConfigFile

                $TokenSplat['Username'] = $Usercontext['Default']['Identity']
                $TokenSplat['TenantId'] = $Usercontext['Default']['TenantId']
                Get-AzToken @TokenSplat -TokenCache $script:AzTokenCache
            }
            catch {
                # Make sure we present the inner exception to users but with a nicer error message
                if ($_.Exception.GetType().FullName -eq 'Azure.Identity.CredentialUnavailableException') {
                    $Exception = New-Object System.InvalidOperationException "Could not find existing token, please run the command Connect-ADOPS!", $_.Exception
                    $ErrorRecord = New-Object Management.Automation.ErrorRecord $Exception, 'ADOPSGetTokenError', ([System.Management.Automation.ErrorCategory]::InvalidOperation), $null
                    throw $ErrorRecord
                }
                else {
                    throw $_
                }
            }
        }
        'ManagedIdentity' {
            Get-AzToken @TokenSplat -ManagedIdentity
        }
        'OAuthToken' {
            return $Script:ScriptToken
        }
        Default {
            throw 'No login method was set, module file may have been corrupted!'
        }
    }
}
#endregion NewAzToken

#region SetADOPSConfigFile

function SetADOPSConfigFile {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]$ConfigPath = '~/.ADOPS/Config.json',

        [Parameter(ValueFromPipeline)]
        [object]$ConfigObject
    )

    $null = New-Item -Path '~/.ADOPS/' -ItemType Directory -ErrorAction SilentlyContinue
    Set-Content -Path $ConfigPath -Value ($ConfigObject | ConvertTo-Json -Compress) -Force
}
#endregion SetADOPSConfigFile

#region SetADOPSPipelinePermission

function SetADOPSPipelinePermission {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [string]$Project,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [switch]$AllPipelines,

        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [int]$PipelineId,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [ResourceType]$ResourceType,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [string]$ResourceId,

        [Parameter(ParameterSetName = 'AllPipelines')]
        [Parameter(ParameterSetName = 'SinglePipeline')]
        [bool]$Authorized = $true,

        [Parameter(ParameterSetName = 'AllPipelines')]
        [Parameter(ParameterSetName = 'SinglePipeline')]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }
    
    $URI = "https://dev.azure.com/${Organization}/${Project}/_apis/pipelines/pipelinepermissions/${ResourceType}/${ResourceId}?api-version=7.1-preview.1"
    $method = 'PATCH'

    $Body = switch ($PSCmdlet.ParameterSetName) {
        'AllPipelines' {
            @{
                allPipelines = @{
                    authorized = $Authorized
                }
            }
        }
        'SinglePipeline' {
            @{
                pipelines = @(
                    [ordered]@{
                        id         = $PipelineId
                        authorized = $Authorized
                    }
                )
            }
        }
        'Default' {
            throw 'Invalid parameter set, this should not happen'
        }
    }
    $Body = $Body | ConvertTo-Json -Depth 10 -Compress

    InvokeADOPSRestMethod -Uri $Uri -Method $Method -Body $Body
}
#endregion SetADOPSPipelinePermission

#region Connect-ADOPS

function Connect-ADOPS {
    [CmdletBinding(DefaultParameterSetName = 'Interactive')]
    param (
        [Parameter(Mandatory, ParameterSetName = 'Interactive')]
        [Parameter(Mandatory, ParameterSetName = 'ManagedIdentity')]
        [Parameter(Mandatory, ParameterSetName = 'OAuthToken')]
        [string]$Organization,
        
        [Parameter(ParameterSetName = 'Interactive')]
        [Parameter(ParameterSetName = 'ManagedIdentity')]
        [Parameter(ParameterSetName = 'OAuthToken')]
        [string]$TenantId,
        
        [Parameter(ParameterSetName = 'Interactive')]
        [Parameter(ParameterSetName = 'ManagedIdentity')]
        [Parameter(ParameterSetName = 'OAuthToken')]
        [switch]$SkipVerification,

        [Parameter(ParameterSetName = 'Interactive')]
        [switch]$Interactive,

        [Parameter(Mandatory, ParameterSetName = 'ManagedIdentity')]
        [switch]$ManagedIdentity,

        [Parameter(Mandatory, ParameterSetName = 'OAuthToken')]
        [String]$OAuthToken
    )
    
    $TokenSplat = @{
        Resource = '499b84ac-1321-427f-aa17-267ca6975798'
        Scope    = '.default'
    }

    # Add TenantId if provided
    if ($PSBoundParameters.ContainsKey('TenantId')) {
        $TokenSplat.Add('TenantId', $TenantId)
    }

    switch ($PSCmdlet.ParameterSetName) {
        'OAuthToken' {
            $script:LoginMethod = 'OAuthToken'
            $script:ScriptToken = @{
                Token = $OAuthToken
            }
            $Token = $OAuthToken
            $TokenTenantId = 'NotSpecified'
            $TokenIdentity = $null
        }
        'ManagedIdentity' {
            $TokenSplat.Add('ManagedIdentity', $true)
            $script:LoginMethod = 'ManagedIdentity'

            $Token = Get-AzToken @TokenSplat
            $TokenTenantId = $Token.TenantId
            $TokenIdentity = $Token.Identity
        }
        'Interactive' {
            $TokenSplat.Add('TokenCache', $script:AzTokenCache)
            $TokenSplat.Add('Interactive', $true)

            $Token = Get-AzToken @TokenSplat
            $TokenTenantId = $Token.TenantId
            $TokenIdentity = $Token.Identity
        }
    }

    if ($Organization -like "https://dev.azure.com/*") {
        $Organization = ($Organization -split "/")[3]
    }
    
    if (-not $PSBoundParameters.ContainsKey('SkipVerification')) {
        # Get User context
        $Me = InvokeADOPSRestMethod -Method GET -Token $Token -Uri 'https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1-preview.3'
    
        # Get available orgs
        $Orgs = GetADOPSOrganizationAccess -AccountId $Me.publicAlias -Token $Token
    
        if ($Organization -notin $Orgs) {
            throw "The connected account does not have access to the organization '$Organization'. Organizations available: $($Orgs -join ",")`nAre you connected to the correct tennant? $TokenTenantId"
        }
    }
    else {
        Write-Verbose 'Skipping organization access verification.'
        $Me = @{ id = 'unverified' }
    }

    # If user provided a token, we have not parsed the JWT for the email/id
    if ($null -eq $TokenIdentity) {
        # Instead take info from the DevOps response
        if (-not [string]::IsNullOrWhiteSpace($Me.emailAddress)) {
            $TokenIdentity = $Me.emailAddress 
        }
        else {
            $TokenIdentity = $Me.id
        }
    }
    
    $ADOPSConfig = GetADOPSConfigFile
    $ADOPSConfig['Default'] = @{
        'Identity'     = $TokenIdentity
        'TenantId'     = $TokenTenantId
        'Organization' = $Organization
    }

    SetADOPSConfigFile -ConfigObject $ADOPSConfig
    
    Write-Output $ADOPSConfig['Default']
}
#endregion Connect-ADOPS

#region Disconnect-ADOPS

function Disconnect-ADOPS {
    [CmdletBinding()]
    [SkipTest('HasOrganizationParameter')]
    param ()

    # Reset context
    NewADOPSConfigFile

    Clear-AzTokenCache -TokenCache $script:AzTokenCache
}
#endregion Disconnect-ADOPS

#region Get-ADOPSArtifactFeed

function Get-ADOPSArtifactFeed {
    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'FeedId', Mandatory)]
        [string]$Project,
        
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'FeedId')]
        [string]$Organization,

        [Parameter(ParameterSetName = 'FeedId', Mandatory)]
        [Alias('Name')]
        [string]$FeedId
    )
    
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }
    
    $Uri = "https://feeds.dev.azure.com/${Organization}"
    if (-not ([string]::IsNullOrEmpty($Project))) {
        $Uri = "${Uri}/${Project}"
    }
    $Uri = "${Uri}/_apis/packaging/feeds"
        if (-not ([string]::IsNullOrEmpty($FeedId))) {
        $Uri = "${Uri}/${FeedId}"
    }
    $Uri = "${Uri}?api-version=7.2-preview.1"
    
    $Method = 'Get'

    $InvokeSplat = @{
        Uri = $Uri
        Method = $Method
    }

    $res = InvokeADOPSRestMethod @InvokeSplat
    if ( 
        (($res | Get-Member -MemberType NoteProperty).Name -contains 'count') -and 
        (($res | Get-Member -MemberType NoteProperty).Name -contains 'value')
    ) {
        Write-Output $res.value -NoEnumerate
    }
    else {
        Write-Output $res
    }
}
#endregion Get-ADOPSArtifactFeed

#region Get-ADOPSAuditActions

function Get-ADOPSAuditActions {
    param (
        [Parameter()]
        [string]$Organization
    )
    
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    (InvokeADOPSRestMethod -Uri "https://auditservice.dev.azure.com/$Organization/_apis/audit/actions" -Method Get).value
}
#endregion Get-ADOPSAuditActions

#region Get-ADOPSBuildDefinition

function Get-ADOPSBuildDefinition {
    [CmdletBinding()]
    Param(
        [Parameter()]
        [string]$Organization,
        
        [Parameter(Mandatory)]
        [string]$Project,
        
        [Parameter()]
        [int]$Id
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if ($PSBoundParameters.ContainsKey('Id')) {
        [int[]]$idList = $id
    }
    else {
        [int[]]$idList = (InvokeADOPSRestMethod -Method GET -Uri "https://dev.azure.com/${Organization}/${Project}/_apis/build/definitions?api-version=7.2-preview.7").value.id
    }

    [array]$res = @()
    foreach ($definition in $idList) {
        [array]$res += InvokeADOPSRestMethod -Method GET -Uri "https://dev.azure.com/${Organization}/${Project}/_apis/build/definitions/${definition}?api-version=7.2-preview.7"
    }

    Write-Output $res -NoEnumerate
}
#endregion Get-ADOPSBuildDefinition

#region Get-ADOPSConnection

function Get-ADOPSConnection {
    [SkipTest('HasOrganizationParameter')]
    param ()
    
    $res = GetADOPSConfigFile
    $res['Default']
}
#endregion Get-ADOPSConnection

#region Get-ADOPSElasticPool

function Get-ADOPSElasticPool {
    [CmdletBinding()]
    param (
        [Parameter()]
        [int32]$PoolId,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if ($PSBoundParameters.ContainsKey('PoolId')) {
        $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools/$PoolId`?api-version=7.1-preview.1"
    } else {
        $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools?api-version=7.1-preview.1"
    }
    
    $Method = 'GET'
    $ElasticPoolInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method -Body $Body
    if ($ElasticPoolInfo.psobject.properties.name -contains 'value') {
        Write-Output $ElasticPoolInfo.value
    } else {
        Write-Output $ElasticPoolInfo
    }
}
#endregion Get-ADOPSElasticPool

#region Get-ADOPSFileContent

function Get-ADOPSFileContent {
    param (
        [Parameter()]
        [string]$Organization,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter(Mandatory)]
        [string]$RepositoryId,

        [Parameter(Mandatory)]
        [string]$FilePath
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if (-Not $FilePath.StartsWith('/')) {
        $FilePath = $FilePath.Insert(0, '/')
    }

    $UrlEncodedFilePath = [System.Web.HttpUtility]::UrlEncode($FilePath)
    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$RepositoryId/items?path=$UrlEncodedFilePath&api-version=7.1-preview.1"

    InvokeADOPSRestMethod -Uri $Uri -Method Get
}
#endregion Get-ADOPSFileContent

#region Get-ADOPSGroup

function Get-ADOPSGroup {
    param ([Parameter()]
        [string]$Organization,

        [Parameter(DontShow)]
        [string]$ContinuationToken
    )
    
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    
    if (-not [string]::IsNullOrEmpty($ContinuationToken)) {
        $Uri = "https://vssps.dev.azure.com/$Organization/_apis/graph/groups?continuationToken=$ContinuationToken&api-version=7.1-preview.1"
    }
    else {
        $Uri = "https://vssps.dev.azure.com/$Organization/_apis/graph/groups?api-version=7.1-preview.1"
    }
    
    $Method = 'GET'

    $Response = InvokeADOPSRestMethod -FullResponse -Uri $Uri -Method $Method

    $Groups = $Response.Content.value
    Write-Verbose "Found $($Response.Content.count) groups"

    if($Response.Headers.ContainsKey('X-MS-ContinuationToken')) {
        Write-Verbose "Found continuationToken. Will fetch more groups."
        $parameters = [hashtable]$PSBoundParameters
        $parameters.Add('ContinuationToken', $Response.Headers['X-MS-ContinuationToken']?[0])
        $Groups += Get-ADOPSGroup @parameters
    }
    
    Write-Output $Groups
}
#endregion Get-ADOPSGroup

#region Get-ADOPSNode

function Get-ADOPSNode {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [int32]$PoolId,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools/$PoolId/nodes?api-version=7.1-preview.1"

    $Method = 'GET'
    $NodeInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method

    if ($NodeInfo.psobject.properties.name -contains 'value') {
        Write-Output $NodeInfo.value
    } else {
        Write-Output $NodeInfo
    }
}
#endregion Get-ADOPSNode

#region Get-ADOPSPipeline

function Get-ADOPSPipeline {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter()]
        [int]$Revision,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/pipelines?api-version=7.1-preview.1"
    
    $InvokeSplat = @{
        Method = 'Get'
        Uri    = $URI
    }

    $AllPipelines = (InvokeADOPSRestMethod @InvokeSplat).value

    if ($PSBoundParameters.ContainsKey('Name')) {
        $Pipelines = $AllPipelines | Where-Object { $_.name -eq $Name }
        if (-not $Pipelines) {
            throw "The specified PipelineName $Name was not found amongst pipelines: $($AllPipelines.name -join ', ')!" 
        } 
    }
    else {
        $Pipelines = $AllPipelines
    }

    $return = @()

    foreach ($Pipeline in $Pipelines) {

        $pipelineRevision = [Uri]::EscapeDataString($PSBoundParameters.ContainsKey('Revision') ? $Revision : $Pipeline.revision)
        $pipelineUrl = "https://dev.azure.com/$Organization/$Project/_apis/pipelines/$($Pipeline.id)?api-version=7.1-preview.1&pipelineVersion=$pipelineRevision"

        $InvokeSplat = @{
            Method = 'Get'
            Uri    = $pipelineUrl
        }
    
        $result = InvokeADOPSRestMethod @InvokeSplat

        $return += $result
    }

    return $return
}

#endregion Get-ADOPSPipeline

#region Get-ADOPSPipelineRetentionSettings

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

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/build/retention?api-version=7.2-preview.1"
    $Response = InvokeADOPSRestMethod -Uri $Uri -Method Get

    $Settings = ConvertRetentionSettingsGetToPatch -Response $Response

    Write-Output $Settings
}
#endregion Get-ADOPSPipelineRetentionSettings

#region Get-ADOPSPipelineSettings

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

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/build/generalsettings?api-version=7.1-preview.1"
    $Settings = InvokeADOPSRestMethod -Uri $Uri -Method Get

    Write-Output $Settings
}
#endregion Get-ADOPSPipelineSettings

#region Get-ADOPSPipelineTask

function Get-ADOPSPipelineTask {
    param (
        [Parameter()]
        [string]$Name,

        [Parameter()]
        [string]$Organization,

        [Parameter()]
        [int]$Version
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri =  "https://dev.azure.com/$Organization/_apis/distributedtask/tasks?api-version=7.1-preview.1"

    $result = InvokeADOPSRestMethod -Uri $Uri -Method Get

    $ReturnValue = $result | ConvertFrom-Json -AsHashtable | Select-Object -ExpandProperty value

    if (-Not [string]::IsNullOrEmpty($Name)) {
        $ReturnValue = $ReturnValue |  Where-Object -Property name -EQ $Name
    }
    if ($Version) {
        $ReturnValue = $ReturnValue |  Where-Object -FilterScript {$_.version.major -eq $Version}
    }

    $ReturnValue
}
#endregion Get-ADOPSPipelineTask

#region Get-ADOPSPool

function Get-ADOPSPool {
    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(Mandatory, ParameterSetName = 'PoolId')]
        [int32]$PoolId,

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

        # Include legacy pools
        [Parameter(ParameterSetName = 'All')]
        [switch]$IncludeLegacy,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    switch ($PSCmdlet.ParameterSetName) {
        'PoolId' { $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/pools/$PoolId`?api-version=7.1-preview.1" }
        'PoolName' { $uri = "https://dev.azure.com/$Organization/_apis/distributedtask/pools?poolName=$PoolName`&api-version=7.1-preview.1" }
        'All' { $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/pools?api-version=7.1-preview.1" }
    }
    
    $Method = 'GET'
    $PoolInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method

    if ($PoolInfo.psobject.properties.name -contains 'value') {
        $PoolInfo = $PoolInfo.value
    }
    if ((-not ($IncludeLegacy.IsPresent)) -and $PSCmdlet.ParameterSetName -eq 'All') {
        $PoolInfo = $PoolInfo | Where-Object { $_.IsLegacy -eq $false }
    }
    Write-Output $PoolInfo
}
#endregion Get-ADOPSPool

#region Get-ADOPSProject

function Get-ADOPSProject {
    [CmdletBinding(DefaultParameterSetName='All')]
    param (
        [Parameter(ParameterSetName='All')]
        [Parameter(ParameterSetName='ByName')]
        [Parameter(ParameterSetName='ById')]
        [string]$Organization,

        [Parameter(ParameterSetName='ByName')]
        [Alias('Project')]
        [string]$Name,

        [Parameter(ParameterSetName='ById')]
        [string]$Id

    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/_apis/projects?api-version=7.1-preview.4"

    $Method = 'GET'
    $ProjectInfo = (InvokeADOPSRestMethod -Uri $Uri -Method $Method).value

    if ($PSCmdlet.ParameterSetName -eq 'ByName') {
        $ProjectInfo = $ProjectInfo | Where-Object -Property Name -eq $Name
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'ById') {
        $ProjectInfo = $ProjectInfo | Where-Object -Property Id -eq $Id
    }

    Write-Output $ProjectInfo
}
#endregion Get-ADOPSProject

#region Get-ADOPSRepository

function Get-ADOPSRepository {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Repository,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if ($PSBoundParameters.ContainsKey('Repository')) {
        $Uri = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$Repository`?api-version=7.1-preview.1"
    }
    else {
        $Uri = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories?api-version=7.1-preview.1"
    }
    
    try {
        $result = InvokeADOPSRestMethod -Uri $Uri -Method Get
    }
    catch [Microsoft.PowerShell.Commands.HttpResponseException] {
        $ErrorMessage = $_.ErrorDetails.Message | ConvertFrom-Json
        if ($ErrorMessage.message -like "TF401019:*") {
            Write-Verbose "The Git repository with name or identifier $Repository does not exist or you do not have permissions for the operation you are attempting."
            $result = $null
        }
        elseif ($ErrorMessage.message -like "TF200016:*") {
            Write-Verbose "The following project does not exist: $Project. Verify that the name of the project is correct and that the project exists on the specified Azure DevOps Server."
            $result = $null
        }
        else {
            Throw $_
        }
    }

    if ($result.psobject.properties.name -contains 'value') {
        Write-Output -InputObject $result.value
    }
    else {
        Write-Output -InputObject $result
    }
}
#endregion Get-ADOPSRepository

#region Get-ADOPSServiceConnection

function Get-ADOPSServiceConnection {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization,
        
        [Parameter()]
        [switch]
        $IncludeFailed
    )
    
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/serviceendpoint/endpoints?includeFailed=$IncludeFailed&api-version=7.1-preview.4"
    
    $InvokeSplat = @{
        Method = 'Get'
        Uri    = $URI
    }

    $ServiceConnections = (InvokeADOPSRestMethod @InvokeSplat).value

    if ($PSBoundParameters.ContainsKey('Name')) {
        $ServiceConnection = $ServiceConnections | Where-Object { $_.name -eq $Name }
        if (-not $ServiceConnection) {
            throw "The specified ServiceConnectionName $Name was not found amongst Connections: $($ServiceConnections.name -join ', ')!" 
        }
        return $ServiceConnection
    }
    else {
        return $ServiceConnections
    }

}
#endregion Get-ADOPSServiceConnection

#region Get-ADOPSUsageData

function Get-ADOPSUsageData {
    param(
        [Parameter()]
        [ValidateSet('Private','Public')]
        [string]$ProjectVisibility = 'Public',

        [Parameter()]
        [Switch]$SelfHosted,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if ($SelfHosted.IsPresent) {
        $Hosted = $false
    }
    else {
        $Hosted = $true
    }

    $URI = "https://dev.azure.com/$Organization/_apis/distributedtask/resourceusage?parallelismTag=${ProjectVisibility}&poolIsHosted=${Hosted}&includeRunningRequests=true"
    $Method = 'Get'
    
    $InvokeSplat = @{
        Method       = $Method
        Uri          = $URI
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion Get-ADOPSUsageData

#region Get-ADOPSUser

function Get-ADOPSUser {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory, ParameterSetName = 'Name', Position = 0)]
        [string]$Name,

        [Parameter(Mandatory, ParameterSetName = 'Descriptor', Position = 0)]
        [string]$Descriptor,

        [Parameter()]
        [string]$Organization,

        [Parameter(ParameterSetName = 'Default', DontShow)]
        [string]$ContinuationToken
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if ($PSCmdlet.ParameterSetName -eq 'Default') {
        $Uri = "https://vssps.dev.azure.com/$Organization/_apis/graph/users?api-version=7.1-preview.1"
        $Method = 'GET'
        if(-not [string]::IsNullOrEmpty($ContinuationToken)) {
            $Uri += "&continuationToken=$ContinuationToken"
        }
        $Response = (InvokeADOPSRestMethod -FullResponse -Uri $Uri -Method $Method)
        $Users = $Response.Content.value
        Write-Verbose "Found $($Response.Content.count) users"

        if($Response.Headers.ContainsKey('X-MS-ContinuationToken')) {
            Write-Verbose "Found continuationToken. Will fetch more users."
            $parameters = [hashtable]$PSBoundParameters
            $parameters.Add('ContinuationToken', $Response.Headers['X-MS-ContinuationToken']?[0])
            $Users += Get-ADOPSUser @parameters
        }   
        Write-Output $Users
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Name') {
        $Uri = "https://vsaex.dev.azure.com/$Organization/_apis/UserEntitlements?`$filter=name eq '$Name'&`$orderBy=name Ascending&api-version=7.1-preview.3"
        $Method = 'GET'
        $Users = (InvokeADOPSRestMethod -Uri $Uri -Method $Method).members.user
        if ($null -eq $Users) {
            Get-ADOPSUser | Where-Object -Property displayName -eq $Name
        }
        Write-Output $Users
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Descriptor') {
        $Uri = "https://vssps.dev.azure.com/$Organization/_apis/graph/users/$Descriptor`?api-version=7.1-preview.1"
        $Method = 'GET'
        $User = (InvokeADOPSRestMethod -Uri $Uri -Method $Method)
        Write-Output $User
    }
}
#endregion Get-ADOPSUser

#region Get-ADOPSVariableGroup

function Get-ADOPSVariableGroup {
    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Name')]
        [Parameter(ParameterSetName = 'Id')]
        [string]$Organization,
        
        [Parameter(Mandatory, ParameterSetName = 'All')]
        [Parameter(Mandatory, ParameterSetName = 'Name')]
        [Parameter(Mandatory, ParameterSetName = 'Id')]
        [string]$Project,
        
        [Parameter(ParameterSetName = 'Name')]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'Id')]
        [int]$Id
    )
    
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Method = 'Get'

    if ($PSCmdlet.ParameterSetName -eq 'Name') {
        $Uri = "https://dev.azure.com/$Organization/$Project/_apis/distributedtask/variablegroups?groupName=$Name&api-version=7.2-preview.2"
    }
    else {
        $Uri = "https://dev.azure.com/$Organization/$Project/_apis/distributedtask/variablegroups?api-version=7.2-preview.2"
    }

    $InvokeSplat = @{
        Uri = $Uri
        Method = $Method
    }

    $result = (InvokeADOPSRestMethod @InvokeSplat).value

    if ($PSCmdlet.ParameterSetName -eq 'Id') {
        $result = $result.Where({$_.Id -eq $Id})
    }

    Write-Output $result
}
#endregion Get-ADOPSVariableGroup

#region Get-ADOPSWiki

function Get-ADOPSWiki {
    param (
        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$WikiId,

        [Parameter()]
        [string]$Organization
    )
 
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $BaseUri = "https://dev.azure.com/$Organization/$Project/_apis/wiki/wikis"
    
    if ($WikiId) {
        $Uri = "${BaseUri}/${WikiId}?api-version=7.1-preview.2"
    }
    else {
        $Uri = "${BaseUri}?api-version=7.1-preview.2"
    }

    $Method = 'Get'

    $res = InvokeADOPSRestMethod -Uri $URI -Method $Method
    
    if ($res.psobject.properties.name -contains 'value') {
        Write-Output -InputObject $res.value
    }
    else {
        Write-Output -InputObject $res
    }
}
#endregion Get-ADOPSWiki

#region Grant-ADOPSPipelinePermission

function Grant-ADOPSPipelinePermission {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [string]$Project,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [switch]$AllPipelines,

        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [int]$PipelineId,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [ResourceType]$ResourceType,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [string]$ResourceId,

        [Parameter(ParameterSetName = 'AllPipelines')]
        [Parameter(ParameterSetName = 'SinglePipeline')]
        [string]$Organization
    )

    SetADOPSPipelinePermission @PSBoundParameters -Authorized $true
}
#endregion Grant-ADOPSPipelinePermission

#region Import-ADOPSRepository

function Import-ADOPSRepository {
    [CmdLetBinding(DefaultParameterSetName = 'RepositoryName')]
    param (
        [Parameter(Mandatory)]
        [string]$GitSource,

        [Parameter(Mandatory, ParameterSetName = 'RepositoryId')]
        [string]$RepositoryId,
        
        [Parameter(Mandatory, ParameterSetName = 'RepositoryName')]
        [string]$RepositoryName,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Organization,

        [Parameter()]
        [switch]$Wait
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    switch ($PSCmdlet.ParameterSetName) {
        'RepositoryName' { $RepoIdentifier = $RepositoryName }
        'RepositoryId' { $RepoIdentifier = $RepositoryId }
        Default {}
    }
    $InvokeSplat = @{
        URI    = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$RepoIdentifier/importRequests?api-version=7.1-preview.1"
        Method = 'Post'
        Body   = "{""parameters"":{""gitSource"":{""url"":""$GitSource""}}}"
    }

    $repoImport = InvokeADOPSRestMethod @InvokeSplat

    if ($PSBoundParameters.ContainsKey('Wait')) {
        # There appears to be a bug in this API where sometimes you don't get the correct status Uri back. Fix it by constructing a correct one instead.
        $verifyUri = "https://dev.azure.com/$Organization/$Project/_apis$($repoImport.url.Split('_apis')[1])"
        while ($repoImport.status -ne 'completed') {
            $repoImport = InvokeADOPSRestMethod -Uri $verifyUri -Method Get
            Start-Sleep -Seconds 1
        }
    }

    $repoImport
}
#endregion Import-ADOPSRepository

#region Initialize-ADOPSRepository

function Initialize-ADOPSRepository {
    [CmdletBinding()]
    [SkipTest('HasOrganizationParameter')]
    param (
        [Parameter()]
        [string]$Message = 'Repo initialized using ADOPS module.',
        
        [Parameter()]
        [string]$Branch = 'Main',

        [Parameter(Mandatory)]
        [string]$RepositoryId,
        
        [Parameter()]
        [ValidateSet('Actionscript.gitignore', 'Ada.gitignore', 'Agda.gitignore', 'Android.gitignore', 'AppceleratorTitanium.gitignore', 'AppEngine.gitignore', 'ArchLinuxPackages.gitignore', 'Autotools.gitignore', 'C++.gitignore', 'C.gitignore', 'CakePHP.gitignore', 'CFWheels.gitignore', 'ChefCookbook.gitignore', 'Clojure.gitignore', 'CMake.gitignore', 'CodeIgniter.gitignore', 'CommonLisp.gitignore', 'Composer.gitignore', 'Concrete5.gitignore', 'Coq.gitignore', 'CraftCMS.gitignore', 'CUDA.gitignore', 'D.gitignore', 'Dart.gitignore', 'Delphi.gitignore', 'DM.gitignore', 'Drupal.gitignore', 'Eagle.gitignore', 'Elisp.gitignore', 'Elixir.gitignore', 'Elm.gitignore', 'EPiServer.gitignore', 'Erlang.gitignore', 'ExpressionEngine.gitignore', 'ExtJs.gitignore', 'Fancy.gitignore', 'Finale.gitignore', 'ForceDotCom.gitignore', 'Fortran.gitignore', 'FuelPHP.gitignore', 'gcov.gitignore', 'GitBook.gitignore', 'Go.gitignore', 'Godot.gitignore', 'Gradle.gitignore', 'Grails.gitignore', 'GWT.gitignore', 'Haskell.gitignore', 'Idris.gitignore', 'IGORPro.gitignore', 'Java.gitignore', 'Jboss.gitignore', 'Jekyll.gitignore', 'JENKINS_HOME.gitignore', 'Joomla.gitignore', 'Julia.gitignore', 'KiCAD.gitignore', 'Kohana.gitignore', 'Kotlin.gitignore', 'LabVIEW.gitignore', 'Laravel.gitignore', 'Leiningen.gitignore', 'LemonStand.gitignore', 'Lilypond.gitignore', 'Lithium.gitignore', 'Lua.gitignore', 'Magento.gitignore', 'Maven.gitignore', 'Mercury.gitignore', 'MetaProgrammingSystem.gitignore', 'nanoc.gitignore', 'Nim.gitignore', 'Node.gitignore', 'Objective-C.gitignore', 'OCaml.gitignore', 'Opa.gitignore', 'opencart.gitignore', 'OracleForms.gitignore', 'Packer.gitignore', 'Perl.gitignore', 'Phalcon.gitignore', 'PlayFramework.gitignore', 'Plone.gitignore', 'Prestashop.gitignore', 'Processing.gitignore', 'PureScript.gitignore', 'Python.gitignore', 'Qooxdoo.gitignore', 'Qt.gitignore', 'R.gitignore', 'Rails.gitignore', 'Raku.gitignore', 'RhodesRhomobile.gitignore', 'ROS.gitignore', 'Ruby.gitignore', 'Rust.gitignore', 'Sass.gitignore', 'Scala.gitignore', 'Scheme.gitignore', 'SCons.gitignore', 'Scrivener.gitignore', 'Sdcc.gitignore', 'SeamGen.gitignore', 'SketchUp.gitignore', 'Smalltalk.gitignore', 'stella.gitignore', 'SugarCRM.gitignore', 'Swift.gitignore', 'Symfony.gitignore', 'SymphonyCMS.gitignore', 'Terraform.gitignore', 'TeX.gitignore', 'Textpattern.gitignore', 'TurboGears2.gitignore', 'Typo3.gitignore', 'Umbraco.gitignore', 'Unity.gitignore', 'UnrealEngine.gitignore', 'VisualStudio.gitignore', 'VVVV.gitignore', 'Waf.gitignore', 'WordPress.gitignore', 'Xojo.gitignore', 'Yeoman.gitignore', 'Yii.gitignore', 'ZendFramework.gitignore', 'Zephir.gitignore')]
        [string[]]$NewContentTemplate,

        [Parameter()]
        [switch]$Readme,

        [Parameter()]
        [string]$Path,

        [Parameter()]
        [string]$Content = 'Repo initialized using ADOPS module.'
    )
 
    $Organization = GetADOPSDefaultOrganization
    
    $Uri = "https://dev.azure.com/$Organization/_apis/git/repositories/$RepositoryId/pushes?api-version=7.2-preview.3"

    if ($Branch -notmatch '^refs/.*') {
        $Branch = 'refs/heads/' + $Branch
    }

    $changes = @()
    
    if ($Readme -or ( [String]::IsNullOrEmpty($Path) -and ($newContentTemplate.Count -eq 0) )) {
        $changes += @{
            changeType = 1
            item = @{path = "/README.md" }
            newContentTemplate = @{
                name = "README.md"
                type = "readme"
            }
        }
    }

    if (-not ([string]::IsNullOrEmpty($Path))) {
        $changes += @{
            changeType = "add"
            item = @{
                path = $Path
            }
            newContent = @{
                content = $Content
                contentType = "rawtext"
            }
        }
    }

    if ($newContentTemplate.Count -eq 1) {
        $changes += @{
            changeType = 1
            item = @{path = "/.gitignore" }
            newContentTemplate = @{
                name = $newContentTemplate[0]
                type = 'gitignore'
            }
        }
    }

    if ($newContentTemplate.Count -gt 1) {
        foreach ($t in $newContentTemplate) {
            $changes += @{
                changeType = 1
                item = @{path = "/$t" }
                newContentTemplate = @{
                    name = $t
                    type = 'gitignore'
                }
            }
        }
    }

    $Body = @{
        commits    = @(
            @{
                comment = $Message
                changes = $changes
            }
        )
        refUpdates = @(
            @{
                name        = $Branch.ToLower()
                oldObjectId = "0000000000000000000000000000000000000000"
            }
        )
    }




    $InvokeSplat = @{
        Uri    = $Uri
        Method = 'Post'
        Body   = $Body | ConvertTo-Json -Compress -Depth 100
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion Initialize-ADOPSRepository

#region Invoke-ADOPSRestMethod

function Invoke-ADOPSRestMethod {
    [SkipTest('HasOrganizationParameter')]
    param (
        [Parameter(Mandatory)]
        [string]$Uri,

        [Parameter()]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method = 'Get',

        [Parameter()]
        [string]$Body
    )

    If ( ($Uri -NotLike "*dev.azure.com*") -and ($Uri -NotLike "*visualstudio.com*")) {
        $Organization = GetADOPSDefaultOrganization
        $Uri = "https://dev.azure.com/$Organization/$Uri"
    }

    $InvokeSplat = @{
        Uri = $Uri
        Method = $Method
    }

    if (-Not [String]::IsNullOrEmpty($Body)) {
        $InvokeSplat.Add('Body', $Body)
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion Invoke-ADOPSRestMethod

#region New-ADOPSArtifactFeed

function New-ADOPSArtifactFeed {
    param (    
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter()]
        [string]$Organization,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter()]
        [string]$Description,

        [Parameter()]
        [Alias('UpstreamEnabled')]
        [switch]$IncludeUpstream
    )
    
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://feeds.dev.azure.com/$Organization/$Project/_apis/packaging/feeds?api-version=7.2-preview.1"

    $body = [ordered]@{
        name = $name
        upstreamEnabled = $IncludeUpstream.IsPresent
        hideDeletedPackageVersions = $true
        project = @{
            visibility = 'Private'
        }
    }

    if (-not [string]::IsNullOrEmpty($Description)) {
        $body.Add('description', $Description)
    }

    if ($IncludeUpstream.IsPresent) {
        $upstreamSources = @(
            @{
                name = "npmjs"
                protocol = "npm"
                location = "https://registry.npmjs.org/"
                displayLocation = "https://registry.npmjs.org/"
                upstreamSourceType = "public"
                status = "ok"
            }
            @{
                name = "NuGet Gallery"
                protocol = "nuget"
                location = "https://api.nuget.org/v3/index.json"
                displayLocation = "https://api.nuget.org/v3/index.json"
                upstreamSourceType = "public"
                status = "ok"
            }
            @{
                name = "PowerShell Gallery"
                protocol = "nuget"
                location = "https://www.powershellgallery.com/api/v2/"
                displayLocation = "https://www.powershellgallery.com/api/v2/"
                upstreamSourceType = "public"
                status = "ok"
            }
            @{
                name = "PyPI"
                protocol = "pypi"
                location = "https://pypi.org/"
                displayLocation = "https://pypi.org/"
                upstreamSourceType = "public"
                status = "ok"
            }
            @{
                name = "Maven Central"
                protocol = "Maven"
                location = "https://repo.maven.apache.org/maven2/"
                displayLocation = "https://repo.maven.apache.org/maven2/"
                upstreamSourceType = "public"
                status = "ok"
            }
            @{
                name = "Google Maven Repository"
                protocol = "Maven"
                location = "https://dl.google.com/android/maven2/"
                displayLocation = "https://dl.google.com/android/maven2/"
                upstreamSourceType = "public"
                status = "ok"
            }
            @{
                name = "JitPack"
                protocol = "Maven"
                location = "https://jitpack.io/"
                displayLocation = "https://jitpack.io/"
                upstreamSourceType = "public"
                status = "ok"
            }
            @{
                name = "Gradle Plugins"
                protocol = "Maven"
                location = "https://plugins.gradle.org/m2/"
                displayLocation = "https://plugins.gradle.org/m2/"
                upstreamSourceType = "public"
                status = "ok"
            }
            @{
                name = "crates.io"
                protocol = "Cargo"
                location = "https://index.crates.io/"
                displayLocation = "https://index.crates.io/"
                upstreamSourceType = "public"
                status = "ok"
            }
        )
        $body.Add('upstreamSources', $upstreamSources)
    }

    $users = Get-ADOPSUser
    $buildService = $users.Where({$_.displayName -eq "$Project build service ($Organization)"})
    if ($buildService.Count -eq 0) {
        Write-Verbose "Failed to find build service account. Not adding it as contributor."
    }
    else {
        $buildServiceDescriptorObject = invokeADOPSRestMethod -Uri "https://vssps.dev.azure.com/$Organization/_apis/Identities?identityIds=$($buildService.originId)" -Method Get
        $permissions = @(
            @{
                identityDescriptor = "$($buildServiceDescriptorObject.Descriptor.IdentityType);$($buildServiceDescriptorObject.Descriptor.Identifier)"
                role = 'contributor'
                identityId = $buildServiceDescriptorObject.Id
            }
        )
        $body.Add('permissions', $permissions)
    }

    $InvokeSplat = @{
        Uri = $Uri
        Method = 'Post'
        Body = $body | ConvertTo-Json -Compress
    }
    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSArtifactFeed

#region New-ADOPSAuditStream

function New-ADOPSAuditStream {
    [CmdletBinding(DefaultParameterSetName = 'AzureMonitorLogs')]
    param (
        [Parameter()]
        [string]$Organization,

        [Parameter(Mandatory, ParameterSetName = 'AzureMonitorLogs')]
        [ValidatePattern('^[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12}$', ErrorMessage = 'WorkspaceId should be in GUID format.')]
        [string]$WorkspaceId,

        [Parameter(Mandatory, ParameterSetName = 'AzureMonitorLogs')]
        [string]$SharedKey,

        [Parameter(Mandatory, ParameterSetName = 'Splunk')]
        [ValidatePattern('^(http|HTTP)[sS]?:\/\/', ErrorMessage = 'SplunkUrl must start with http:// or https://')]
        [string]$SplunkUrl,

        [Parameter(Mandatory, ParameterSetName = 'Splunk')]
        [ValidatePattern('^[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12}$', ErrorMessage = 'SplunkEventCollectorToken should be in GUID format.')]
        [string]$SplunkEventCollectorToken,

        [Parameter(Mandatory, ParameterSetName = 'AzureEventGrid')]
        [ValidatePattern('^(http|HTTP)[sS]?:\/\/', ErrorMessage = 'EventGridTopicHostname must start with http:// or https://')]
        [string]$EventGridTopicHostname,

        [Parameter(Mandatory, ParameterSetName = 'AzureEventGrid')]
        [ValidatePattern('^[A-Za-z0-9+\/]*={0,2}$', ErrorMessage = 'EventGridTopicAccessKey should be Base64 encoded')]
        [string]$EventGridTopicAccessKey
    )
    
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Body = switch ($PSCmdlet.ParameterSetName) {
        'AzureMonitorLogs' { 
            [ordered]@{
                consumerType = 'AzureMonitorLogs'
                consumerInputs = [Ordered]@{
                    WorkspaceId = $WorkspaceId
                    SharedKey = $SharedKey
                }
            } | ConvertTo-Json -Compress
         }
        'Splunk' { 
            [ordered]@{
                consumerType = 'Splunk'
                consumerInputs = [Ordered]@{
                    SplunkUrl = $SplunkUrl
                    SplunkEventCollectorToken = $SplunkEventCollectorToken
                }
            } | ConvertTo-Json -Compress
         }
        'AzureEventGrid' { 
            [ordered]@{
                consumerType = 'AzureEventGrid'
                consumerInputs = [ordered]@{
                    EventGridTopicHostname = $EventGridTopicHostname
                    EventGridTopicAccessKey = $EventGridTopicAccessKey
                }
            } | ConvertTo-Json -Compress
         }
    }
    $InvokeSplat = @{
        Uri = "https://auditservice.dev.azure.com/$Organization/_apis/audit/streams?api-version=7.1-preview.1"
        Method = 'Post'
        Body = $Body
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSAuditStream

#region New-ADOPSBuildPolicy

function New-ADOPSBuildPolicy {
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter()]
        [string]$Organization,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$RepositoryId,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Branch,

        [Parameter(Mandatory)]
        [int]$PipelineId,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Displayname,

        [Parameter()]
        [string[]]$FilenamePatterns
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if (-Not ($Branch -match '^\w+/\w+/\w+$')) {
        $Branch = "refs/heads/$Branch"
    }
    $GitBranchRef = $Branch

    $settings = [ordered]@{
        scope = @(
            [ordered]@{
                repositoryId = $RepositoryId
                refName = $GitBranchRef
                matchKind = "exact"
            }
        )
        buildDefinitionId = $PipelineId.ToString()
        queueOnSourceUpdateOnly = $false
        manualQueueOnly = $false
        displayName = $Displayname
        validDuration = "0"
    }

    if ($FilenamePatterns.Count -gt 0) {
        $settings.Add('filenamePatterns', $FilenamePatterns)
    }

    $Body = [ordered]@{
        type = [ordered]@{
            id = "0609b952-1397-4640-95ec-e00a01b2c241" 
        }
        isBlocking = $true
        isEnabled = $true
        settings = $settings
    } 
    
    $Body = $Body | ConvertTo-Json -Depth 10 -Compress
    
    $InvokeSplat = @{
        Uri = "https://dev.azure.com/$Organization/$Project/_apis/policy/configurations?api-version=7.1-preview.1"
        Method = 'POST'
        Body = $Body
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSBuildPolicy

#region New-ADOPSElasticpool

function New-ADOPSElasticPool {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$PoolName,
        
        [Parameter(Mandatory)]
        $ElasticPoolObject,

        [Parameter()]
        [string]$ProjectId,

        [Parameter()]
        [switch]$AuthorizeAllPipelines,

        [Parameter()]
        [switch]$AutoProvisionProjectPools,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if ($PSBoundParameters.ContainsKey('ProjectId')) {
        $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools?poolName=$PoolName`&authorizeAllPipelines=$AuthorizeAllPipelines`&autoProvisionProjectPools=$AutoProvisionProjectPools`&projectId=$ProjectId`&api-version=7.1-preview.1"
    } else {
        $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools?poolName=$PoolName`&authorizeAllPipelines=$AuthorizeAllPipelines`&autoProvisionProjectPools=$AutoProvisionProjectPools`&api-version=7.1-preview.1"
    }

    if ($ElasticPoolObject.gettype().name -eq 'String') {
        $Body = $ElasticPoolObject
    } else {
        try {
            $Body = $ElasticPoolObject | ConvertTo-Json -Depth 100
        }
        catch {
            throw "Unable to convert the content of the ElasticPoolObject to json."
        }
    }
    
    $Method = 'POST'
    $ElasticPoolInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method -Body $Body
    Write-Output $ElasticPoolInfo
}
#endregion New-ADOPSElasticpool

#region New-ADOPSElasticPoolObject

function New-ADOPSElasticPoolObject {
    [SkipTest('HasOrganizationParameter')]
    [CmdletBinding()]
    param (
        # Service Endpoint Id
        [Parameter(Mandatory)]
        [guid]
        $ServiceEndpointId,

        # Service Endpoint Scope
        [Parameter(Mandatory)]
        [guid]
        $ServiceEndpointScope,

        # Azure Id
        [Parameter(Mandatory)]
        [string]
        $AzureId,

        # Operating System Type
        [Parameter()]
        [ValidateSet('linux', 'windows')]
        [string]
        $OsType = 'linux',

        # MaxCapacity
        [Parameter()]
        [int]
        $MaxCapacity = 1,

        # DesiredIdle
        [Parameter()]
        [int]
        $DesiredIdle = 0,

        # Recycle VM after each use
        [Parameter()]
        [boolean]
        $RecycleAfterEachUse = $false,

        # Desired Size of pool
        [Parameter()]
        [int]
        $DesiredSize = 0,

        # Agent Interactive UI
        [Parameter()]
        [boolean]
        $AgentInteractiveUI = $false,

        # Time before scaling down
        [Parameter()]
        [Alias('TimeToLiveMinues')]
        [int]
        $TimeToLiveMinutes = 15,

        # maxSavedNodeCount
        [Parameter()]
        [int]
        $MaxSavedNodeCount = 0,

        # Output Type
        [Parameter()]
        [ValidateSet('json','pscustomobject')]
        [string]
        $OutputType = 'pscustomobject'
    )

    if ($DesiredIdle -gt $MaxCapacity) {
        throw "The desired idle count cannot be larger than the max capacity."
    }

    $ElasticPoolObject = [PSCustomObject]@{
        serviceEndpointId = $ServiceEndpointId
        serviceEndpointScope = $ServiceEndpointScope
        azureId = $AzureId
        maxCapacity = $MaxCapacity
        desiredIdle = $DesiredIdle
        recycleAfterEachUse = $RecycleAfterEachUse
        maxSavedNodeCount = $MaxSavedNodeCount
        osType = $OsType
        desiredSize = $DesiredSize
        agentInteractiveUI = $AgentInteractiveUI
        timeToLiveMinutes = $TimeToLiveMinutes
    }
    
    if ($OutputType -eq 'json') {
        $ElasticPoolObject = $ElasticPoolObject | ConvertTo-Json -Depth 100
    }

    Write-Output $ElasticPoolObject
}
#endregion New-ADOPSElasticPoolObject

#region New-ADOPSEnvironment

function New-ADOPSEnvironment {
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization,
        
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,
        
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,
        
        [Parameter()]
        [string]$Description,
        
        [Parameter()]
        [string]$AdminGroup,
        
        [Parameter()]
        [switch]$SkipAdmin
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$organization/$project/_apis/distributedtask/environments?api-version=7.1-preview.1"

    $Body = [Ordered]@{
        name = $Name
        description = $Description
    } | ConvertTo-Json -Compress

    $InvokeSplat = @{
        Uri = $Uri
        Method = 'Post'
        Body = $Body
    }

    Write-Verbose "Setting up environment"
    $Environment = InvokeADOPSRestMethod @InvokeSplat

    if ($PSBoundParameters.ContainsKey('SkipAdmin')) {
        Write-Verbose 'Skipped admin group'
    }
    else {
        $secUri = "https://dev.azure.com/$organization/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/$($Environment.project.id)_$($Environment.id)?api-version=7.1-preview.1"

        if ([string]::IsNullOrEmpty($AdminGroup)) {
            $AdmGroupPN = "[$project]\Project Administrators"
        } 
        else {
            $AdmGroupPN = $AdminGroup
        }
        $ProjAdm = (Get-ADOPSGroup | Where-Object {$_.principalName -eq $AdmGroupPN}).originId

        $SecInvokeSplat = @{
            Uri = $secUri
            Method = 'Put'
            Body = "[{`"userId`":`"$ProjAdm`",`"roleName`":`"Administrator`"}]"
        }

        try {
            $SecResult = InvokeADOPSRestMethod @SecInvokeSplat
        }
        catch {
            Write-Error 'Failed to update environment security. The environment may still have been created.'
        }
    }

    Write-Output $Environment
}
#endregion New-ADOPSEnvironment

#region New-ADOPSGitBranch

function New-ADOPSGitBranch {
    param (
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$Organization,

    [Parameter(Mandatory)]
    [ValidatePattern('^[a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}$', ErrorMessage = 'RepositoryId must be in GUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)')]
    [string]$RepositoryId,

    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$Project,

    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$BranchName,

    [Parameter(Mandatory)]
    [ValidateLength(40,40)]
    [string]$CommitId
    )
    
    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Body = @(
        [ordered]@{
            name = "refs/heads/$BranchName"
            oldObjectId = '0000000000000000000000000000000000000000'
            newObjectId = $CommitId
        }
    )
    $Body = ConvertTo-Json -InputObject $Body -Compress

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$RepositoryId/refs?api-version=7.1-preview.1"
    $InvokeSplat = @{
        Uri = $Uri
        Method = 'Post'
        Body = $Body
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSGitBranch

#region New-ADOPSGitFile

function New-ADOPSGitFile {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]$Organization,
        
        [Parameter(Mandatory)]
        [string]$Project,
        
        [Parameter(Mandatory)]
        [string]$Repository,
        
        [Parameter(Mandatory)]
        [string]$File,
        
        [Parameter()]
        [string]$FileName,
        
        [Parameter()]
        [string]$Path,
        
        [Parameter()]
        [string]$CommitMessage = 'File added using the ADOPS PowerShell module'
    )

    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }
    
    if ([string]::IsNullOrEmpty($Path)) {
        $Path = '/'
    }

    if ([string]::IsNullOrEmpty($FileName)) {
        $FileName = (Get-Item -Path $File).Name
    }

    $newFilePath = "/${Path}/$FileName" -replace '/{2,}','/' # Make sure there are never two or more slashes in a row.

    $repoDetails = Get-ADOPSRepository -Project $Project -Repository $Repository

    $refIduri = "$($repoDetails.url)/refs?filter=$($repoDetails.defaultBranch -replace '^refs/','')&includeStatuses=true&latestStatusesOnly=true&api-version=7.2-preview.2"
    $refId = InvokeADOPSRestMethod -Uri $refIduri -Method Get | Select-Object -ExpandProperty value
    
    $body = [ordered]@{
        refUpdates = @(
            [ordered]@{
                name        = $repoDetails.defaultBranch
                oldObjectId = $refId.objectId
            }
        )
        commits    = @(
            [ordered]@{
                comment = $CommitMessage
                changes = @(
                    [ordered]@{
                        changeType = "add"
                        item       = [ordered]@{
                            path = $newFilePath
                        }
                        newContent = [ordered]@{
                            content     = $(Get-Content $File -Raw)
                            contentType = "rawtext"
                        }
                    }
                )
            }
        )
    } | ConvertTo-Json -Depth 100 -Compress

    $Uri = "$($repoDetails.url)/pushes?api-version=7.2-preview.3"
    $InvokeSplat = @{
        Method = 'Post'
        Uri = $Uri
        Body = $Body
    }
    
    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSGitFile

#region New-ADOPSMergePolicy

function New-ADOPSMergePolicy {
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter()]
        [string]$Organization,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$RepositoryId,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Branch,

        [Parameter()]
        [Switch]$AllowNoFastForward,

        [Parameter()]
        [Switch]$AllowSquash,

        [Parameter()]
        [Switch]$AllowRebase,

        [Parameter()]
        [Switch]$AllowRebaseMerge
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    if (-Not ($Branch -match '^\w+/\w+/\w+$')) {
        $Branch = "refs/heads/$Branch"
    }
    $GitBranchRef = $Branch

    $settings = [ordered]@{
        scope = @(
            [ordered]@{
                repositoryId = $RepositoryId
                refName = $GitBranchRef
                matchKind = "exact"
            }
        )
        allowNoFastForward = $AllowNoFastForward.IsPresent
        allowSquash = $AllowSquash.IsPresent
        allowRebase = $AllowRebase.IsPresent
        allowRebaseMerge = $AllowRebaseMerge.IsPresent
    }


    $Body = [ordered]@{
        type = [ordered]@{
            id = "fa4e907d-c16b-4a4c-9dfa-4916e5d171ab" 
        }
        isBlocking = $true
        isEnabled = $true
        settings = $settings
    } 
    
    $Body = $Body | ConvertTo-Json -Depth 10 -Compress
    
    $InvokeSplat = @{
        Uri = "https://dev.azure.com/$Organization/$Project/_apis/policy/configurations?api-version=7.1-preview.1"
        Method = 'POST'
        Body = $Body
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSMergePolicy

#region New-ADOPSPipeline

function New-ADOPSPipeline {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter(Mandatory)]
        [ValidateScript( { 
            $_ -like '*.yaml' -or
            $_ -like '*.yml'
        },
        ErrorMessage = "Path must be to a yaml file in your repository like: folder/file.yaml or folder/file.yml")] 
        [string]$YamlPath,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Repository,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$FolderPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/pipelines?api-version=7.1-preview.1"

    try {
        $RepositoryID = (Get-ADOPSRepository -Organization $Organization -Project $Project -Repository $Repository -ErrorAction Stop).id
    }
    catch {
        throw "The specified Repository $Repository was not found."
    }

    if ($null -eq $RepositoryID) {
        throw "The specified Repository $Repository was not found."
    }

    $Body = [ordered]@{
        "name" = $Name
        "folder" = "\$FolderPath"
        "configuration" = [ordered]@{
            "type" = "yaml"
            "path" = $YamlPath
            "repository" = [ordered]@{
                "id" = $RepositoryID
                "type" = "azureReposGit"
            }
        }
    }
    $Body = $Body | ConvertTo-Json -Compress

    $InvokeSplat = @{
        Method       = 'Post'
        Uri          = $URI
        Body         = $Body 
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSPipeline

#region New-ADOPSProject

function New-ADOPSProject {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Alias('Project')]
        [string]$Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Description,
        
        [Parameter(Mandatory)]
        [ValidateSet('Private', 'Public')]
        [string]$Visibility,
        
        [Parameter()]
        [ValidateSet('Git', 'Tfvc')]
        [string]$SourceControlType = 'Git',
        
        # The process type for the project, such as Basic, Agile, Scrum or CMMI
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ProcessTypeName,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization,
        
        [Parameter()]
        [switch]$Wait
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    # Get organization process templates
    $URI = "https://dev.azure.com/$Organization/_apis/process/processes?api-version=7.1-preview.1"

    $InvokeSplat = @{
        Method       = 'Get'
        Uri          = $URI
    }

    $ProcessTemplates = (InvokeADOPSRestMethod @InvokeSplat).value

    if ([string]::IsNullOrWhiteSpace($ProcessTypeName)) {
        $ProcessTemplateTypeId = $ProcessTemplates | Where-Object isDefault -eq $true | Select-Object -ExpandProperty id
    }
    else {
        $ProcessTemplateTypeId = $ProcessTemplates | Where-Object name -eq $ProcessTypeName | Select-Object -ExpandProperty id
        if ([string]::IsNullOrWhiteSpace($ProcessTemplateTypeId)) {
            throw "The specified ProcessTypeName was not found amongst options: $($ProcessTemplates.name -join ', ')!"
        }
    }

    # Create project endpoint
    $URI = "https://dev.azure.com/$Organization/_apis/projects?api-version=7.1-preview.4"

    $Body = [ordered]@{
        'name'         = $Name
        'visibility'   = $Visibility
        'capabilities' = [ordered]@{
            'versioncontrol'  = [ordered]@{
                'sourceControlType' = $SourceControlType
            }
            'processTemplate' = [ordered]@{
                'templateTypeId' = $ProcessTemplateTypeId
            }
        }
    }
    if (-not [string]::IsNullOrEmpty($Description)) {
        $Body.Add('description', $Description)
    }
    $Body = $Body | ConvertTo-Json -Compress
    
    $InvokeSplat = @{
        Method       = 'Post'
        Uri          = $URI
        Body         = $Body
    }

    $Out = InvokeADOPSRestMethod @InvokeSplat

    if ($PSBoundParameters.ContainsKey('Wait')) {
        $projectCreated = $Out.status
        while ($projectCreated -ne 'succeeded') {
            $projectCreated = (Invoke-ADOPSRestMethod -Uri $Out.url -Method Get).status
            Start-Sleep -Seconds 1
        }
        $Out = Get-ADOPSProject -Project $Name 
    }

    $Out
}
#endregion New-ADOPSProject

#region New-ADOPSRepository

function New-ADOPSRepository {
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $ProjectID = (Get-ADOPSProject -Name $Project -Organization $Organization).id

    $URI = "https://dev.azure.com/$Organization/_apis/git/repositories?api-version=7.1-preview.1"
    $Body = "{""name"":""$Name"",""project"":{""id"":""$ProjectID""}}"

    $InvokeSplat = @{
        Uri          = $URI
        Method       = 'Post'
        Body         = $Body
    }
    
    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSRepository

#region New-ADOPSServiceConnection

function New-ADOPSServiceConnection {
    [cmdletbinding(DefaultParameterSetName = 'ServicePrincipal')]
    param(
        [Parameter()]
        [string]$Organization,
        
        [Parameter(Mandatory)]
        [string]$TenantId,

        [Parameter(Mandatory)]
        [string]$SubscriptionName,

        [Parameter(Mandatory)]
        [string]$SubscriptionId,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$ConnectionName,

        [Parameter()]
        [string]$Description,
      
        [Parameter(Mandatory, ParameterSetName = 'ServicePrincipal')]
        [Parameter(Mandatory, ParameterSetName = 'ManagedServiceIdentity')]
        [pscredential]$ServicePrincipal,

        [Parameter(Mandatory, ParameterSetName = 'ManagedServiceIdentity')]
        [switch]$ManagedIdentity,

        [Parameter(Mandatory, ParameterSetName = 'WorkloadIdentityFederation')]
        [switch]$WorkloadIdentityFederation,

        [Parameter(ParameterSetName = 'WorkloadIdentityFederation')]
        [string]$AzureScope,

        [Parameter(ParameterSetName = 'WorkloadIdentityFederation')]
        [ValidateSet('Manual', 'Automatic')]
        [string]
        $CreationMode = 'Automatic'
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    # Get ProjectId
    $ProjectInfo = Get-ADOPSProject -Organization $Organization -Project $Project

    # Set connection name if not set by parameter
    if (-not $ConnectionName) {
        $ConnectionName = $SubscriptionName -replace ' '
    }
    
    switch ($PSCmdlet.ParameterSetName) {
        
        'ServicePrincipal' {
            $authorization = [ordered]@{
                parameters = [ordered]@{
                    tenantid            = $TenantId
                    serviceprincipalid  = $ServicePrincipal.UserName
                    authenticationType  = 'spnKey'
                    serviceprincipalkey = $ServicePrincipal.GetNetworkCredential().Password
                }
                scheme     = 'ServicePrincipal'
            }
    
            $data = [ordered]@{
                subscriptionId   = $SubscriptionId
                subscriptionName = $SubscriptionName
                environment      = 'AzureCloud'
                scopeLevel       = 'Subscription'
                creationMode     = 'Manual'
            }
        }

        'ManagedServiceIdentity' {
            $authorization = [ordered]@{
                parameters = [ordered]@{
                    tenantid            = $TenantId
                    serviceprincipalid  = $ServicePrincipal.UserName
                    serviceprincipalkey = $ServicePrincipal.GetNetworkCredential().Password
                }
                scheme     = 'ManagedServiceIdentity'
            }
    
            $data = [ordered]@{
                subscriptionId   = $SubscriptionId
                subscriptionName = $SubscriptionName
                environment      = 'AzureCloud'
                scopeLevel       = 'Subscription'
            }
        }

        'WorkloadIdentityFederation' {
            if ($PSBoundParameters.ContainsKey('AzureScope')) {
                $AuthParams =  [ordered]@{
                    tenantid = $TenantId
                    scope    = $AzureScope
                }
            } else {
                 $AuthParams =  [ordered]@{
                    tenantid = $TenantId
                }
            }

            $authorization = [ordered]@{
                parameters = $AuthParams
                scheme     = 'WorkloadIdentityFederation'
            }
    
            $data = [ordered]@{
                subscriptionId   = $SubscriptionId
                subscriptionName = $SubscriptionName
                environment      = 'AzureCloud'
                scopeLevel       = 'Subscription'
                creationMode     = $CreationMode
                isDraft          = ($CreationMode = 'Manual') ? $True : $False
            }
        }
    }

    # Create body for the API call
    $Body = [ordered]@{
        data                             = $data
        name                             = $ConnectionName
        description                      = $Description
        type                             = 'AzureRM'
        url                              = 'https://management.azure.com/'
        authorization                    = $authorization
        isShared                         = $false
        isReady                          = $true
        serviceEndpointProjectReferences = @(
            [ordered]@{
                projectReference = [ordered]@{
                    id   = $ProjectInfo.Id
                    name = $Project
                }
                name             = $ConnectionName
            }
        )
    } | ConvertTo-Json -Depth 10

    # Run function
    $URI = "https://dev.azure.com/$Organization/$Project/_apis/serviceendpoint/endpoints?api-version=7.2-preview.4"
    $InvokeSplat = @{
        Uri    = $URI
        Method = 'POST'
        Body   = $Body
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSServiceConnection

#region New-ADOPSUserStory

function New-ADOPSUserStory {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Title,

        [Parameter(Mandatory)]
        [string]$ProjectName,

        [Parameter()]
        [string]$Description,

        [Parameter()]
        [string]$Tags,

        [Parameter()]
        [string]$Priority,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $URI = "https://dev.azure.com/$Organization/$ProjectName/_apis/wit/workitems/`$User Story?api-version=7.1-preview.3"
    $Method = 'POST'

    $desc = $Description.Replace('"', "'")
    $Body = "[
      {
        `"op`": `"add`",
        `"path`": `"/fields/System.Title`",
        `"value`": `"$($Title)`"
      },
      {
        `"op`": `"add`",
        `"path`": `"/fields/System.Description`",
        `"value`": `"$($desc)`"
      },
      {
        `"op`": `"add`",
        `"path`": `"/fields/System.Tags`",
        `"value`": `"$($Tags)`"
      },
      {
        `"op`": `"add`",
        `"path`": `"/fields/Microsoft.VSTS.Common.Priority`",
        `"value`": `"$($Priority)`"
      },
    ]"

    
    $ContentType = 'application/json-patch+json'  
  
    $InvokeSplat = @{
        Uri          = $URI
        ContentType  = $ContentType
        Method       = $Method
        Body         = $Body
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSUserStory

#region New-ADOPSVariableGroup

function New-ADOPSVariableGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$VariableGroupName,

        [Parameter(Mandatory, ParameterSetName = 'VariableSingle')]
        [string]$VariableName,

        [Parameter(Mandatory, ParameterSetName = 'VariableSingle')]
        [string]$VariableValue,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter(ParameterSetName = 'VariableSingle')]
        [switch]$IsSecret,

        [Parameter(Mandatory, ParameterSetName = 'VariableHashtable')]
        [ValidateScript(
            {
                $_ | ForEach-Object { $_.Keys -Contains 'Name' -and $_.Keys -Contains 'IsSecret' -and $_.Keys -Contains 'Value' -and $_.Keys.count -eq 3 }
            },
            ErrorMessage = 'The hashtable must contain the following keys: Name, IsSecret, Value')]
        [hashtable[]]$VariableHashtable,

        [Parameter()]
        [string]$Description,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $ProjectInfo = Get-ADOPSProject -Organization $Organization -Project $Project

    $URI = "https://dev.azure.com/${Organization}/_apis/distributedtask/variablegroups?api-version=7.1-preview.2"
    $Method = 'POST'

    if ($VariableName) {
        $Body = @{
            Name                           = $VariableGroupName
            Description                    = $Description
            Type                           = 'Vsts'
            variableGroupProjectReferences = @(@{
                    Name             = $VariableGroupName
                    Description      = $Description
                    projectReference = @{
                        Id = $ProjectInfo.Id
                    }
                })
            variables                      = @{
                $VariableName = @{
                    isSecret = $IsSecret.IsPresent
                    value    = $VariableValue
                }
            }
        } | ConvertTo-Json -Depth 10
    }
    else {

        $Variables = @{}
        foreach ($Hashtable in $VariableHashtable) {
            $Variables.Add(
                $Hashtable.Name, @{
                    isSecret = $Hashtable.IsSecret
                    value    = $Hashtable.Value
                }
            )
        }

        $Body = @{
            Name                           = $VariableGroupName
            Description                    = $Description
            Type                           = 'Vsts'
            variableGroupProjectReferences = @(@{
                    Name             = $VariableGroupName
                    Description      = $Description
                    projectReference = @{
                        Id = $($ProjectInfo.Id)
                    }
                })
            variables                      = $Variables
        } | ConvertTo-Json -Depth 10
    }

    InvokeADOPSRestMethod -Uri $Uri -Method $Method -Body $Body
}
#endregion New-ADOPSVariableGroup

#region New-ADOPSWiki

function New-ADOPSWiki {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$WikiName,
        
        [Parameter(Mandatory)]
        [string]$WikiRepository,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$WikiRepositoryPath = '/',
        
        [Parameter()]
        [string]$GitBranch = 'main',

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    try {
        $ProjectId = (Get-ADOPSProject -Project $Project).id
    }
    catch {
        throw "The specified Project $Project was not found."
    }
    if ($null -eq $ProjectId) {
        throw "The specified Project $Project was not found."
    }

    try {
        $RepositoryId = (Get-ADOPSRepository -Project $Project -Repository $WikiRepository).id
    }
    catch {
        throw "The specified Repository $WikiRepository was not found."
    }

    if ($null -eq $RepositoryID) {
        throw "The specified Repository $WikiRepository was not found."
    }

    $URI = "https://dev.azure.com/$Organization/_apis/wiki/wikis?api-version=7.1-preview.2"
    
    $Method = 'Post'
    $Body = [ordered]@{
        'type' = 'codeWiki'
        'name' = $WikiName
        'projectId' = $ProjectId
        'repositoryId' = $RepositoryId
        'mappedPath' = $WikiRepositoryPath
        'version' = @{'version' = $GitBranch}
    } 

    $InvokeSplat = @{
        Uri = $URI
        Method = $Method
        Body = $Body | ConvertTo-Json -Compress
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSWiki

#region Remove-ADOPSRepository

function Remove-ADOPSRepository {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$RepositoryID,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$RepositoryID`?api-version=7.1-preview.1"
    
    $result = InvokeADOPSRestMethod -Uri $Uri -Method Delete

    if ($result.psobject.properties.name -contains 'value') {
        Write-Output -InputObject $result.value
    }
    else {
        Write-Output -InputObject $result
    }
}
#endregion Remove-ADOPSRepository

#region Remove-ADOPSVariableGroup

function Remove-ADOPSVariableGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$VariableGroupName,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/distributedtask/variablegroups?api-version=7.1-preview.2"
    $VariableGroups = (InvokeADOPSRestMethod -Uri $Uri -Method 'Get').value

    $GroupToRemove = $VariableGroups | Where-Object name -eq $VariableGroupName
    if ($null -eq $GroupToRemove) {
        throw "Could not find group $VariableGroupName! Groups found: $($VariableGroups.name -join ', ')."
    }
    
    $ProjectId = (Get-ADOPSProject -Organization $Organization -Project $Project).id

    $URI = "https://dev.azure.com/$Organization/_apis/distributedtask/variablegroups/$($GroupToRemove.id)?projectIds=$ProjectId&api-version=7.1-preview.2"
    $null = InvokeADOPSRestMethod -Uri $Uri -Method 'Delete'
}
#endregion Remove-ADOPSVariableGroup

#region Revoke-ADOPSPipelinePermission

function Revoke-ADOPSPipelinePermission {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [string]$Project,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [switch]$AllPipelines,

        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [int]$PipelineId,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [ResourceType]$ResourceType,

        [Parameter(Mandatory, ParameterSetName = 'AllPipelines')]
        [Parameter(Mandatory, ParameterSetName = 'SinglePipeline')]
        [string]$ResourceId,

        [Parameter(ParameterSetName = 'AllPipelines')]
        [Parameter(ParameterSetName = 'SinglePipeline')]
        [string]$Organization
    )

    SetADOPSPipelinePermission @PSBoundParameters -Authorized $false
}
#endregion Revoke-ADOPSPipelinePermission

#region Save-ADOPSPipelineTask

function Save-ADOPSPipelineTask {
    [CmdletBinding(DefaultParameterSetName = 'InputData')]
    param (
        [Parameter(ParameterSetName = 'InputData')]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$Organization,

        [Parameter(ParameterSetName = 'InputData')]
        [Parameter(ParameterSetName = 'InputObject')]
        [string]$Path = '.',

        [Parameter(Mandatory, ParameterSetName = 'InputData')]
        [string]$TaskId,

        [Parameter(Mandatory, ParameterSetName = 'InputData')]
        [version]$TaskVersion,

        [Parameter(ParameterSetName = 'InputData')]
        [string]$FileName,

        [Parameter(Mandatory, ParameterSetName = 'InputObject', ValueFromPipeline, Position = 0)]
        [psobject[]]$InputObject
    )
    begin {
        # If user didn't specify org, get it from saved context
        if ([string]::IsNullOrEmpty($Organization)) {
            $Organization = GetADOPSDefaultOrganization
        }
    }
    process {
        switch ($PSCmdlet.ParameterSetName) {
            'InputData' {
                if ([string]::IsNullOrEmpty($FileName)) {
                    $FileName = "$TaskId.$($TaskVersion.ToString(3)).zip"
                }
                if (-Not $FileName -match '.zip$' ) {
                    $FileName = "$FileName.zip"
                }

                [array]$FilesToDownload = @{
                    TaskId            = $TaskId
                    TaskVersionString = $TaskVersion.ToString(3)
                    OutputFile        = Join-Path -Path $Path -ChildPath $FileName
                }
            }
            'InputObject' {
                [array]$FilesToDownload = foreach ($o in $InputObject) {
                    @{
                        TaskId            = $o.id
                        TaskVersionString = "$($o.version.major).$($o.version.minor).$($o.version.patch)"
                        OutputFile        = Join-Path -Path $Path -ChildPath "$($o.name)-$($o.id)-$($o.version.major).$($o.version.minor).$($o.version.patch).zip"
                    }
                }
            }
        }

        foreach ($File in $FilesToDownload) {
            $Url = "https://dev.azure.com/$Organization/_apis/distributedtask/tasks/$($File.TaskId)/$($File.TaskversionString)"
            InvokeADOPSRestMethod -Uri $Url -Method Get -OutFile $File.OutputFile
        }
    }
    end {}
}
#endregion Save-ADOPSPipelineTask

#region Set-ADOPSArtifactFeed

function Set-ADOPSArtifactFeed {
    [CmdletBinding()]
    param (   
        [Parameter(Mandatory)]
        [string]$Project,
        
        [Parameter()]
        [string]$Organization,

        [Parameter(Mandatory)]
        [Alias('Name')]
        [string]$FeedId,
        
        [Parameter()]
        [string]$Description,
        
        [Parameter()]
        [Alias('IncludeUpstream')]
        [bool]$UpstreamEnabled
    )
 
    if (
        -not ($PSBoundParameters.ContainsKey('Description')) -and
        -not ($PSBoundParameters.ContainsKey('UpstreamEnabled'))
    ) {
        Write-Verbose 'Nothing to do. Exiting early'
    }
    else {
        # If user didn't specify org, get it from saved context
        if ([string]::IsNullOrEmpty($Organization)) {
            $Organization = GetADOPSDefaultOrganization
        }
        
        $Uri = "https://feeds.dev.azure.com/${Organization}/${Project}/_apis/packaging/feeds/${FeedId}?api-version=7.2-preview.1"
        $Method = 'Patch'

        $Body = [ordered]@{}
        if ($PSBoundParameters.ContainsKey('Description')) {
            $Body['description'] = $Description
        }
        if ($PSBoundParameters.ContainsKey('UpstreamEnabled')) {
            $Body['upstreamEnabled'] = $UpstreamEnabled
                if ($UpstreamEnabled -eq $true) {                
                $upstreamSources = @(
                    @{
                        name = "npmjs"
                        protocol = "npm"
                        location = "https://registry.npmjs.org/"
                        displayLocation = "https://registry.npmjs.org/"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                    @{
                        name = "NuGet Gallery"
                        protocol = "nuget"
                        location = "https://api.nuget.org/v3/index.json"
                        displayLocation = "https://api.nuget.org/v3/index.json"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                    @{
                        name = "PowerShell Gallery"
                        protocol = "nuget"
                        location = "https://www.powershellgallery.com/api/v2/"
                        displayLocation = "https://www.powershellgallery.com/api/v2/"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                    @{
                        name = "PyPI"
                        protocol = "pypi"
                        location = "https://pypi.org/"
                        displayLocation = "https://pypi.org/"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                    @{
                        name = "Maven Central"
                        protocol = "Maven"
                        location = "https://repo.maven.apache.org/maven2/"
                        displayLocation = "https://repo.maven.apache.org/maven2/"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                    @{
                        name = "Google Maven Repository"
                        protocol = "Maven"
                        location = "https://dl.google.com/android/maven2/"
                        displayLocation = "https://dl.google.com/android/maven2/"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                    @{
                        name = "JitPack"
                        protocol = "Maven"
                        location = "https://jitpack.io/"
                        displayLocation = "https://jitpack.io/"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                    @{
                        name = "Gradle Plugins"
                        protocol = "Maven"
                        location = "https://plugins.gradle.org/m2/"
                        displayLocation = "https://plugins.gradle.org/m2/"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                    @{
                        name = "crates.io"
                        protocol = "Cargo"
                        location = "https://index.crates.io/"
                        displayLocation = "https://index.crates.io/"
                        upstreamSourceType = "public"
                        status = "ok"
                    }
                )
                $body.Add('upstreamSources', $upstreamSources)
            }
        }

        $Body = $Body | ConvertTo-Json -Compress

        $InvokeSplat = @{
            Uri = $Uri
            Method = $Method
            Body = $Body
        }

        InvokeADOPSRestMethod @InvokeSplat
    }
}
#endregion Set-ADOPSArtifactFeed

#region Set-ADOPSBuildDefinition

function Set-ADOPSBuildDefinition {
    [CmdletBinding()]
    Param(
        [Parameter()]
        [string]$Organization,
        
        [Parameter(Mandatory)]
        [Alias('Definition')]
        [Object]$DefinitionObject
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $project = $DefinitionObject.project.id
    $id = $DefinitionObject.id

    $Uri = "https://dev.azure.com/${Organization}/${project}/_apis/build/definitions/${id}?api-version=7.2-preview.7"
    $Method = 'Put'

    if (-Not ($DefinitionObject -is [string])) {
        $DefinitionObject = $DefinitionObject | ConvertTo-Json -Compress -Depth 100
    }

    $InvokeSplat = @{
        Uri = $Uri
        Method = $Method
        Body = $DefinitionObject
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion Set-ADOPSBuildDefinition

#region Set-ADOPSElasticPool

function Set-ADOPSElasticPool {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [int]$PoolId,
        
        [Parameter(Mandatory)]
        $ElasticPoolObject,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools/$PoolId`?api-version=7.1-preview.1"

    if ($ElasticPoolObject.GetType().Name -eq 'String') {
        $Body = $ElasticPoolObject
    }
    else {
        try {
            $Body = $ElasticPoolObject | ConvertTo-Json -Depth 100
        }
        catch {
            throw 'Unable to convert the content of the ElasticPoolObject to json.'
        }
    }
    
    $Method = 'PATCH'
    $ElasticPoolInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method -Body $Body
    Write-Output $ElasticPoolInfo
}
#endregion Set-ADOPSElasticPool

#region Set-ADOPSGitPermission

function Set-ADOPSGitPermission {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization,
        
        [Parameter(Mandatory)]
        [Alias('ProjectId')]
        [string]$Project,
        
        [Parameter(Mandatory)]
        [Alias('RepositoryId')]
        [string]$Repository,
        
        [Parameter(Mandatory)]
        [ValidatePattern('^[a-z]{3,5}\.[a-zA-Z0-9]{40,}$', ErrorMessage = 'Descriptor must be in the descriptor format')]
        [string]$Descriptor,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [AccessLevels[]]$Allow,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [AccessLevels[]]$Deny
    )
    
    # Allow input of names instead of IDs
    if ($Project -notmatch '^[a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}$') {
        $Project = Get-ADOPSProject -Name $Project | Select-Object -ExpandProperty id
        if ($null -eq $Project) {
            throw "No project named $Project found."
        }
    }
    if ($Repository -notmatch '^[a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}$') {
        $Repository = Get-ADOPSRepository -Repository $Repository -Project $Project
        if ($null -eq $Repository) {
            throw "No repository named $Repository in project $Project found."
        }
    }


    if (-not $Allow -and -not $Deny) {
        Write-Verbose 'No allow or deny rules set'
    }
    else {
        if ($null -eq $Allow) {
            $allowRules = 0
        }
        else {
            $allowRules = ([accesslevels]$Allow).value__
        }
        if ($null -eq $Deny) {
            $denyRules = 0
        }
        else {
            $denyRules = ([accesslevels]$Deny).value__
        }
    
        # If user didn't specify org, get it from saved context
        if ([string]::IsNullOrEmpty($Organization)) {
            $Organization = GetADOPSDefaultOrganization
        }
        
        $SubjectDescriptor = (InvokeADOPSRestMethod -Uri "https://vssps.dev.azure.com/$Organization/_apis/identities?subjectDescriptors=$Descriptor&queryMembership=None&api-version=7.1-preview.1" -Method Get).value.descriptor

        $Body = [ordered]@{
            token                = "repov2/$Project/$Repository"
            merge                = $true
            accessControlEntries = @(
                [ordered]@{
                    allow      = $allowRules
                    deny       = $denyRules
                    descriptor = $SubjectDescriptor
                }
            )
        } | ConvertTo-Json -Compress -Depth 10
        
        $Uri = "https://dev.azure.com/$Organization/_apis/accesscontrolentries/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?api-version=7.1-preview.1"
        
        $InvokeSplat = @{
            Uri    = $Uri 
            Method = 'Post' 
            Body   = $Body
        }
        
        InvokeADOPSRestMethod @InvokeSplat
    }
}
#endregion Set-ADOPSGitPermission

#region Set-ADOPSPipelineRetentionSettings

function Set-ADOPSPipelineRetentionSettings {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]$Organization,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        $Values
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/build/retention?api-version=7.1-preview.1"

    $Body = ConvertRetentionSettingsToPatchBody -Values $Values | ConvertTo-Json
    Write-Debug $Body
    
    $Response = InvokeADOPSRestMethod -Uri $Uri -Method Patch -Body $Body
    Write-Debug $Response

    $Settings = ConvertRetentionSettingsGetToPatch -Response $Response
    
    Write-Output $Settings
}
#endregion Set-ADOPSPipelineRetentionSettings

#region Set-ADOPSPipelineSettings

function Set-ADOPSPipelineSettings {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]$Organization,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter(Mandatory)]
        $Values
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/build/generalsettings?api-version=7.1-preview.1"

    $Body =  $Values | ConvertTo-Json -Compress
    $Settings = InvokeADOPSRestMethod -Uri $Uri -Method 'PATCH' -Body $Body

    Write-Output $Settings
}
#endregion Set-ADOPSPipelineSettings

#region Set-ADOPSProject

function Set-ADOPSProject {
    [CmdletBinding(DefaultParameterSetName = 'ProjectName')]
    param (    
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization,
        
        [Parameter(Mandatory, ParameterSetName = 'ProjectId')]
        [ValidateScript({
            [guid]::Parse($_)
        }, ErrorMessage = 'ProjectID format is wrong.')]
        [string]$ProjectId,
        
        [Parameter(Mandatory, ParameterSetName = 'ProjectName')]
        [ValidateNotNullOrEmpty()]
        [Alias('Project', 'Name')]
        [string]$ProjectName,

        [Parameter()]
        [string]$Description,
        
        [Parameter()]
        [ValidateSet('Private', 'Public')]
        [string]$Visibility,
        
        [Parameter()]
        [switch]$Wait
    )

    if (-Not ($PSBoundParameters.ContainsKey('Description')) -and -Not ($PSBoundParameters.ContainsKey('Visibility'))) {
        Write-Verbose 'Nothing to update. Exiting.'
    }
    else {
        # If user didn't specify org, get it from saved context
        if ([string]::IsNullOrEmpty($Organization)) {
            $Organization = GetADOPSDefaultOrganization
        }

        if ($PSCmdlet.ParameterSetName -eq 'ProjectName') {
            $ProjectId = Get-ADOPSProject -Name $ProjectName | Select-Object -ExpandProperty id
        }

        $uri = "https://dev.azure.com/${Organization}/_apis/projects/${ProjectId}?api-version=7.2-preview.4"

        
        $body = [ordered]@{}
        
        if ($PSBoundParameters.ContainsKey('Description')) {
            $body.Add('description', $Description)
        }

        if (-not [string]::IsNullOrEmpty($Visibility)) {
            $body.Add('visibility', $Visibility.ToLower())
        }
            
        $body = $body | ConvertTo-Json -Compress

        $InvokeSplat = [ordered]@{
            'Uri' = $uri
            'Method' = 'Patch'
            'Body' = $body
        }

        $res = InvokeADOPSRestMethod @InvokeSplat

        if ($PSBoundParameters.ContainsKey('Wait')) {
            $i = 0
            while (($res.status -notin @('cancelled', 'failed', 'succeeded')) -and $i -le 20) {
                $res = InvokeADOPSRestMethod -Uri $res.url -Method Get
                $i++
                Start-Sleep -Seconds 1
            }
            if ($i -ge 20) {
                Write-Verbose 'Status still not complete, failed, or canceled. Please verify project update.'
            }
        }

        $res
    }
}
#endregion Set-ADOPSProject

#region Set-ADOPSRepository

function Set-ADOPSRepository {
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$RepositoryId,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$DefaultBranch,

        [Parameter()]
        [bool]$IsDisabled,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$NewName
    )

    if ( ([string]::IsNullOrEmpty($DefaultBranch)) -and ([string]::IsNullOrEmpty($NewName)) -and (-Not $PSBoundParameters.ContainsKey('IsDisabled')) ) {
        # Nothing to do, exit early
    }
    else {
        # If user didn't specify org, get it from saved context
        if ([string]::IsNullOrEmpty($Organization)) {
            $Organization = GetADOPSDefaultOrganization
        }
        
        $URI = "https://dev.azure.com/${Organization}/${Project}/_apis/git/repositories/${RepositoryId}?api-version=7.1-preview.1"
        
        $InvokeSplat = @{
            URI = $Uri
            Method = 'Patch'
        }

        if ($PSBoundParameters.ContainsKey('IsDisabled') -and ($false -eq $IsDisabled)) {
            # Enabling a repo needs to be done in a separate call before changing any other settings.
            $Body = [ordered]@{
                'isDisabled' = $IsDisabled
            }
            $InvokeSplat.Body = $Body | ConvertTo-Json -Compress
            try {
                InvokeADOPSRestMethod @InvokeSplat
            }
            catch {
                if (($_.ErrorDetails.Message | ConvertFrom-Json).message -eq 'The repository change is not supported.') {
                    Write-Warning 'Failed to enable the repo. This is most likely because it is already enabled.'
                }
                else {
                    throw $_
                }
            }
        }

        $Body = [ordered]@{}

        if (-Not [string]::IsNullOrEmpty($NewName)) {
            $Body.Add('name',$NewName)
        }

        if (-Not [string]::IsNullOrEmpty($DefaultBranch)) {
            if (-Not ($DefaultBranch -match '^\w+/\w+/\w+$')) {
                $DefaultBranch = "refs/heads/$DefaultBranch"
            }
            $Body.Add('defaultBranch',$DefaultBranch)
        }

        if ($body.Keys.Count -gt 0) {
            $InvokeSplat.Body = $Body | ConvertTo-Json -Compress
            try {
                InvokeADOPSRestMethod @InvokeSplat
            }
            catch {
                if (($_.ErrorDetails.Message | ConvertFrom-Json).message -like "TF401019*") {
                    Write-Warning 'Failed to update the repo. This may happen if the repo is disabled. Make sure it is enabled, or add -IsDisabled:$false'
                }
                else {
                    throw $_
                }
            }
        }
        
        if ($PSBoundParameters.ContainsKey('IsDisabled') -and ($true -eq $IsDisabled)) {
            # Disabling a repo needs to be done in a separate call and after any other changes.
            $Body = [ordered]@{
                'isDisabled' = $IsDisabled
            }
            $InvokeSplat.Body = $Body | ConvertTo-Json -Compress
            try {
                InvokeADOPSRestMethod @InvokeSplat
            }
            catch {
                if (($_.ErrorDetails.Message | ConvertFrom-Json).message -eq 'The repository change is not supported.') {
                    Write-Warning 'Failed to disable the repo. This is most likely because it is already disabled.'
                }
                else {
                    throw $_
                }
            }
        }
    }
}
#endregion Set-ADOPSRepository

#region Set-ADOPSServiceConnection

function Set-ADOPSServiceConnection {
    [CmdletBinding(DefaultParameterSetName = 'ServicePrincipal')]
    param (
        [Parameter()]
        [string]$Organization,
        
        [Parameter(Mandatory)]
        [string]$TenantId,

        [Parameter(Mandatory)]
        [string]$SubscriptionName,

        [Parameter(Mandatory)]
        [string]$SubscriptionId,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter(Mandatory)]
        [guid]$ServiceEndpointId,

        [Parameter()]
        [string]$ConnectionName,

        [Parameter()]
        [string]$Description,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$EndpointOperation,
      
        [Parameter(Mandatory, ParameterSetName = 'ServicePrincipal')]
        [pscredential]$ServicePrincipal,

        [Parameter(Mandatory, ParameterSetName = 'ManagedServiceIdentity')]
        [switch]$ManagedIdentity,

        [Parameter(Mandatory, ParameterSetName = 'WorkloadIdentityFederation')]
        [string]$ServicePrincipalId,

        [Parameter(Mandatory, ParameterSetName = 'WorkloadIdentityFederation')]
        [string]$WorkloadIdentityFederationIssuer,

        [Parameter(Mandatory, ParameterSetName = 'WorkloadIdentityFederation')]
        [string]$WorkloadIdentityFederationSubject

    )
    
    process {

        # If user didn't specify org, get it from saved context
        if ([string]::IsNullOrEmpty($Organization)) {
            $Organization = GetADOPSDefaultOrganization
        }

        # Get ProjectId
        $ProjectInfo = Get-ADOPSProject -Organization $Organization -Project $Project

        # Set connection name if not set by parameter
        if (-not $ConnectionName) {
            $ConnectionName = $SubscriptionName -replace " "
        }

        switch ($PSCmdlet.ParameterSetName) {
        
            'ServicePrincipal' {
                $authorization = [ordered]@{
                    parameters = [ordered]@{
                        tenantid            = $TenantId
                        serviceprincipalid  = $ServicePrincipal.UserName
                        authenticationType  = "spnKey"
                        serviceprincipalkey = $ServicePrincipal.GetNetworkCredential().Password
                    }
                    scheme     = "ServicePrincipal"
                }
        
                $data = [ordered]@{
                    subscriptionId   = $SubscriptionId
                    subscriptionName = $SubscriptionName
                    environment      = "AzureCloud"
                    scopeLevel       = "Subscription"
                    creationMode     = "Manual"
                }
            }
    
            'ManagedServiceIdentity' {
                $authorization = [ordered]@{
                    parameters = [ordered]@{
                        tenantid = $TenantId
                    }
                    scheme     = "ManagedServiceIdentity"
                }
            }

            'WorkloadIdentityFederation' {
                $authorization = [ordered]@{
                    parameters = [ordered]@{
                        tenantid                          = $TenantId
                        serviceprincipalid                = $ServicePrincipalId
                        workloadIdentityFederationIssuer  = $WorkloadIdentityFederationIssuer
                        workloadIdentityFederationSubject = $WorkloadIdentityFederationSubject
                    }
                    scheme     = "WorkloadIdentityFederation"
                }
        
                $data = [ordered]@{
                    subscriptionId   = $SubscriptionId
                    subscriptionName = $SubscriptionName
                    environment      = "AzureCloud"
                    scopeLevel       = "Subscription"
                    creationMode     = "Manual"
                }
            }
        }

        # Create body for the API call
        $Body = [ordered]@{
            authorization                    = $authorization
            data                             = $data
            description                      = $Description
            id                               = $ServiceEndpointId
            isReady                          = $true
            isShared                         = $false
            name                             = $ConnectionName
            serviceEndpointProjectReferences = @(
                [ordered]@{
                    projectReference = [ordered]@{
                        id   = $ProjectInfo.Id
                        name = $Project
                    }
                    name             = $ConnectionName
                }
            )
            type                             = "AzureRM"
            url                              = "https://management.azure.com/"
        } | ConvertTo-Json -Depth 10
    
        if ($PSBoundParameters.ContainsKey('EndpointOperation')) {
            $URI = "https://dev.azure.com/$Organization/_apis/serviceendpoint/endpoints/$ServiceEndpointId`?operation=$EndpointOperation`&api-version=7.1-preview.4"
        }
        else {
            $URI = "https://dev.azure.com/$Organization/_apis/serviceendpoint/endpoints/$ServiceEndpointId`?api-version=7.1-preview.4"
        }
        
        $InvokeSplat = @{
            Uri    = $URI
            Method = "PUT"
            Body   = $Body
        }
    
        InvokeADOPSRestMethod @InvokeSplat
    }
}
#endregion Set-ADOPSServiceConnection

#region Start-ADOPSPipeline

function Start-ADOPSPipeline {
    param (
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Branch = 'main',

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $AllPipelinesURI = "https://dev.azure.com/$Organization/$Project/_apis/pipelines?api-version=7.1-preview.1"
    $AllPipelines = InvokeADOPSRestMethod -Method Get -Uri $AllPipelinesURI
    $PipelineID = ($AllPipelines.value | Where-Object -Property Name -EQ $Name).id

    if ([string]::IsNullOrEmpty($PipelineID)) {
        throw "No pipeline with name $Name found."
    }

    if ($Branch -notmatch '^refs/.*') {
        $Branch = 'refs/heads/' + $Branch
    }
    $URI = "https://dev.azure.com/$Organization/$Project/_apis/pipelines/$PipelineID/runs?api-version=7.1-preview.1"
    $Body = '{"stagesToSkip":[],"resources":{"repositories":{"self":{"refName":"' + $Branch + '"}}},"variables":{}}'
    
    $InvokeSplat = @{
        Method       = 'Post' 
        Uri          = $URI 
        Body         = $Body
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion Start-ADOPSPipeline

#region Test-ADOPSYamlFile

function Test-ADOPSYamlFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter(Mandatory)]
        [ValidateScript({
                $_ -match '.*\.y[aA]{0,1}ml$'
            }, ErrorMessage = 'Fileextension must be ".yaml" or ".yml"')]
        [string]$File,

        [Parameter(Mandatory)]
        [int]$PipelineId,

        [Parameter()]
        [string]$Organization
    )

    # If user didn't specify org, get it from saved context
    if ([string]::IsNullOrEmpty($Organization)) {
        $Organization = GetADOPSDefaultOrganization
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/pipelines/$PipelineId/runs?api-version=7.1-preview.1"

    $FileData = Get-Content $File -Raw

    $Body = @{
        previewRun         = $true
        templateParameters = @{}
        resources          = @{}
        yamlOverride       = $FileData
    } | ConvertTo-Json -Depth 10 -Compress
    
    $InvokeSplat = @{
        Uri          = $URI
        Method       = 'Post'
        Body         = $Body
    }
    
    try {
        $Result = InvokeADOPSRestMethod @InvokeSplat
        Write-Output "$file validation success."
    } 
    catch [Microsoft.PowerShell.Commands.HttpResponseException] {
        if ($_.ErrorDetails.Message) {
            $r = $_.ErrorDetails.Message | ConvertFrom-Json
            if ($r.typeName -like '*PipelineValidationException*') {
                Write-Warning "Validation failed:`n$($r.message)"
            }
            else {
                throw $_
            }
        }
    }
}
#endregion Test-ADOPSYamlFile