diff --git a/Team.psd1 b/Team.psd1 index 485207de1..caeba8295 100644 --- a/Team.psd1 +++ b/Team.psd1 @@ -64,7 +64,8 @@ TypesToProcess = @('src\types.ps1xml') # Format files (.ps1xml) to be loaded when importing this module - FormatsToProcess = @('src\TeamTypes.format.ps1xml') + FormatsToProcess = @('src\TeamTypes.format.ps1xml', + 'src\TeamTypes.Artifacts.format.ps1xml') # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess NestedModules = @('src\team.psm1', @@ -123,7 +124,12 @@ 'Get-GitRepository', 'Add-GitRepository', 'Remove-GitRepository', - 'Get-BuildLog') + 'Get-BuildLog', + 'Add-BuildTag', + 'Get-BuildTag', + 'Remove-BuildTag', + 'Get-BuildArtifact', + 'Update-Build') # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. # CmdletsToExport = @() diff --git a/src/TeamTypes.Artifacts.format.ps1xml b/src/TeamTypes.Artifacts.format.ps1xml new file mode 100644 index 000000000..46e813136 --- /dev/null +++ b/src/TeamTypes.Artifacts.format.ps1xml @@ -0,0 +1,194 @@ + + + + + + + Team.Build.Artifact.TableView + + Team.Build.Artifact + + + + + + + + + + + + + + + + + + name + + + type + + + downloadUrl + + + + + + + + + Team.Build.Artifact.WideView + + Team.Build.Artifact + + + + + + downloadUrl + + + + + + + + Team.Build.Artifact.ListView + + Team.Build.Artifact + + + + + + + id + + + name + + + type + + + data + + + downloadUrl + + + + + + + + + Team.Build.Artifact.Resource.TableView + + Team.Build.Artifact.Resource + + + + + + + + + + + + + + + + + + + + + + + + type + + + data + + + url + + + downloadUrl + + + properties + + + + + + + + + Team.Build.Artifact.Resource.WideView + + Team.Build.Artifact.Resource + + + + + + properties + + + + + + + + Team.Build.Artifact.Resource.ListView + + Team.Build.Artifact.Resource + + + + + + + type + + + data + + + url + + + downloadUrl + + + properties + + + + + + + + \ No newline at end of file diff --git a/src/builds.psm1 b/src/builds.psm1 index 2f0f1b772..65295cd92 100644 --- a/src/builds.psm1 +++ b/src/builds.psm1 @@ -4,6 +4,8 @@ Set-StrictMode -Version Latest $here = Split-Path -Parent $MyInvocation.MyCommand.Path . "$here\common.ps1" +$apiVersionQueryString = '?api-version=2.0' + function _buildURL { param( [parameter(Mandatory = $true)] @@ -13,28 +15,69 @@ function _buildURL { [int] $LogIndex ) - if (-not $env:TEAM_ACCT) { - throw 'You must call Add-TeamAccount before calling any other functions in this module.' + if ($Logs.IsPresent -eq $true) { + $rootUrl = _buildRootURL -ProjectName $ProjectName -Id $Id -Logs $Logs -LogIndex $LogIndex } - - $version = '2.0' - $resource = "/build/builds" - $instance = $env:TEAM_ACCT - - if ($id) { - $resource += "/$id" + else { + $rootUrl = _buildRootURL -ProjectName $ProjectName -Id $Id -LogIndex $LogIndex } - if ($Logs.IsPresent) { - $resource += "/logs" - - if ($LogIndex) { - $resource += "/$LogIndex" - } - } # Build the url to list the projects - return $instance + "/$projectName/_apis" + $resource + '?api-version=' + $version + return $rootUrl + $apiVersionQueryString +} + +function _buildChildUrl { + param( + [parameter(Mandatory = $true)] + [string] $ProjectName, + [int] $Id, + [Switch] $Logs, + [int] $LogIndex, + [string] $Child + ) + + if ($Logs.IsPresent -eq $true) { + $rootUrl = _buildRootURL -ProjectName $ProjectName -Id $Id -Logs $Logs -LogIndex $LogIndex + } + else { + $rootUrl = _buildRootURL -ProjectName $ProjectName -Id $Id -LogIndex $LogIndex + } + + # Build the url to list the projects + return $rootUrl + "/$Child" + $apiVersionQueryString +} + +function _buildRootURL { + param( + [parameter(Mandatory = $true)] + [string] $ProjectName, + [int] $Id, + [Switch] $Logs, + [int] $LogIndex + ) + + if (-not $env:TEAM_ACCT) { + throw 'You must call Add-TeamAccount before calling any other functions in this module.' + } + + $resource = "/build/builds" + $instance = $env:TEAM_ACCT + + if ($id) { + $resource += "/$id" + } + + if ($Logs.IsPresent) { + $resource += "/logs" + + if ($LogIndex) { + $resource += "/$LogIndex" + } + } + + # Build the url to list the projects + return $instance + "/$projectName/_apis" + $resource } # Apply types to the returned objects so format and type files can @@ -61,6 +104,15 @@ function _applyTypes { } } +function _applyArtifactTypes { + $item.PSObject.TypeNames.Insert(0, "Team.Build.Artifact") + + if ($item.PSObject.Properties.Match('resource').count -gt 0 -and $null -ne $item.resource) { + $item.resource.PSObject.TypeNames.Insert(0, 'Team.Build.Artifact.Resource') + $item.resource.properties.PSObject.TypeNames.Insert(0, 'Team.Build.Artifact.Resource.Properties') + } +} + function Get-Build { [CmdletBinding(DefaultParameterSetName = 'List')] param ( @@ -370,4 +422,202 @@ function Remove-Build { } } -Export-ModuleMember -Alias * -Function Add-Build, Get-Build, Remove-Build, Get-BuildLog \ No newline at end of file +function Update-Build { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Int] $Id, + + [parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [bool] $KeepForever, + + [parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [string] $BuildNumber, + + [switch] $Force + ) + + DynamicParam { + _buildProjectNameDynamicParam + } + + Process { + $ProjectName = $PSBoundParameters["ProjectName"] + + if ($Force -or $pscmdlet.ShouldProcess($Id, "Update-Build")) { + + $updateUrl = _buildURL -ProjectName $ProjectName -Id $Id + + $body = '{' + + $items = New-Object System.Collections.ArrayList + + if ($KeepForever -ne $null) { + $items.Add("`"keepForever`": $($KeepForever.ToString().ToLower())") > $null + } + + if ($buildNumber -ne $null -and $buildNumber.Length -gt 0) { + $items.Add("`"buildNumber`": `"$BuildNumber`"") > $null + } + + if ($items -ne $null -and $items.count -gt 0) { + $body += ($items -join ", ") + } + + $body += '}' + + # Call the REST API + if (_useWindowsAuthenticationOnPremise) { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Patch -ContentType "application/json" -Body $body -Uri $updateUrl -UseDefaultCredentials + } + else { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Patch -ContentType "application/json" -Body $body -Uri $updateUrl -Headers @{Authorization = "Basic $env:TEAM_PAT"} + } + } + } + +} + +function Get-BuildTag { + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [int] $Id + ) + + DynamicParam { + _buildProjectNameDynamicParam + } + + Process { + $ProjectName = $PSBoundParameters["ProjectName"] + + $rootUrl = _buildChildUrl -projectName $ProjectName -id $Id -child "tags" + + # Call the REST API + if (_useWindowsAuthenticationOnPremise) { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Get -Uri $rootUrl -UseDefaultCredentials + } + else { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Get -Uri $rootUrl -Headers @{Authorization = "Basic $env:TEAM_PAT"} + } + + return $resp.value + } +} + +function Add-BuildTag { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] + [string[]] $Tags, + + [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [int[]] $Id, + + [switch] $Force + ) + + DynamicParam { + _buildProjectNameDynamicParam + } + + Process { + $ProjectName = $PSBoundParameters["ProjectName"] + + foreach ($item in $id) { + if ($Force -or $pscmdlet.ShouldProcess($item, "Add-BuildTag")) { + + $rootUrl = _buildChildUrl -projectName $ProjectName -id $item -child "tags" + + foreach ($tag in $tags) { + + $tagUrl = $rootUrl + "&tag=$tag" + + # Call the REST API + if (_useWindowsAuthenticationOnPremise) { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Put -Uri $tagUrl -UseDefaultCredentials + } + else { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Put -Uri $tagUrl -Headers @{Authorization = "Basic $env:TEAM_PAT"} + } + } + } + } + } +} + +function Remove-BuildTag { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Low")] + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] + [string[]] $Tags, + + [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [int[]] $Id, + + [switch] $Force + ) + + DynamicParam { + _buildProjectNameDynamicParam + } + + Process { + $ProjectName = $PSBoundParameters["ProjectName"] + + foreach ($item in $id) { + if ($Force -or $pscmdlet.ShouldProcess($item, "Remove-BuildTag")) { + + $rootUrl = _buildChildUrl -projectName $ProjectName -id $item -child "tags" + + foreach ($tag in $tags) + { + $tagUrl = $rootUrl + "&tag=$tag" + + # Call the REST API + if (_useWindowsAuthenticationOnPremise) { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Delete -Uri $tagUrl -UseDefaultCredentials + } + else { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Delete -Uri $tagUrl -Headers @{Authorization = "Basic $env:TEAM_PAT"} + } + } + } + } + } +} + +function Get-BuildArtifact { + param( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [int] $Id + ) + + DynamicParam { + _buildProjectNameDynamicParam + } + + Process { + $ProjectName = $PSBoundParameters["ProjectName"] + + $rootUrl = _buildChildUrl -projectName $ProjectName -id $Id -child "artifacts" + + # Call the REST API + if (_useWindowsAuthenticationOnPremise) { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Get -Uri $rootUrl -UseDefaultCredentials + } + else { + $resp = Invoke-RestMethod -UserAgent (_getUserAgent) -Method Get -Uri $rootUrl -Headers @{Authorization = "Basic $env:TEAM_PAT"} + } + + foreach ($item in $resp.value) { + _applyArtifactTypes -item $item + } + + Write-Output $resp.value + } +} + +Export-ModuleMember -Alias * -Function Add-Build, Get-Build, Remove-Build, Get-BuildLog, + Add-BuildTag, Get-BuildTag, Remove-BuildTag, + Get-BuildArtifact, Update-Build \ No newline at end of file diff --git a/src/types.ps1xml b/src/types.ps1xml index 2e1be4650..dc02695bc 100644 --- a/src/types.ps1xml +++ b/src/types.ps1xml @@ -139,6 +139,40 @@ REMAINS WITH THE USER. + + + Team.Build.Artifact + + + type + $this.resource.type + + + data + $this.resource.data + + + url + $this.resource.url + + + downloadUrl + $this.resource.downloadUrl + + + PSStandardMembers + + + DefaultDisplayPropertySet + + id + name + + + + + + Team.Release diff --git a/test/builds.Tests.ps1 b/test/builds.Tests.ps1 index aa025ab4a..37ec4f6b9 100644 --- a/test/builds.Tests.ps1 +++ b/test/builds.Tests.ps1 @@ -9,7 +9,7 @@ InModuleScope builds { # Just a shell for the nest dynamic parameters # Used as Mock for calls below. We can't use normal - # Mock because the module where is lives is not loaded. + # Mock because the module where it lives is not loaded. function Get-BuildDefinition { return new-object psobject -Property @{ id=2 @@ -127,5 +127,88 @@ InModuleScope builds { } } } - } + + Context 'Add-BuildTag' { + Mock Invoke-RestMethod -UserAgent(_getUserAgent) + $inputTags = "Test1", "Test2", "Test3" + + It 'should add tags to Build' { + Add-BuildTag -ProjectName project -id 2 -Tags $inputTags + + foreach ($inputTag in $inputTags) { + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Put' -and + $Uri -eq 'https://test.visualstudio.com/project/_apis/build/builds/2/tags?api-version=2.0' + "&tag=$inputTag" + } + } + } + } + + Context 'Remove-BuildTag' { + Mock Invoke-RestMethod -UserAgent(_getUserAgent) { + return @{ value=$null } + } + [string[]] $inputTags = "Test1", "Test2", "Test3" + + It 'should add tags to Build' { + Remove-BuildTag -ProjectName project -id 2 -Tags $inputTags + + foreach ($inputTag in $inputTags) { + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Delete' -and + $Uri -eq 'https://test.visualstudio.com/project/_apis/build/builds/2/tags?api-version=2.0' + "&tag=$inputTag" + } + } + } + } + + Context 'Get-BuildTag calls correct Url' { + Mock Invoke-RestMethod { + return @{ value='Tag1', 'Tag2'} + } + + It 'should get all Build Tags for the Build.' { + Get-BuildTag -projectName project -id 2 + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://test.visualstudio.com/project/_apis/build/builds/2/tags?api-version=2.0' + } + } + } + + Context 'Get-BuildTag returns correct data' { + $tags = 'Tag1', 'Tag2' + Mock Invoke-RestMethod -UserAgent(_getUserAgent) { + return @{ value=$tags} + } + + It 'should get all Build Tags for the Build.' { + $returndata = Get-BuildTag -projectName project -id 2 + + Compare-Object $tags $returndata | + Should Be $null + } + } + + Context "Get-BuildArtifact calls correct Url" { + Mock Invoke-RestMethod -UserAgent(_getUserAgent) { return @{ + value = @{ + id = 150; + name = "Drop"; + resource = @{type="filepath"; data="C:\Test"} + } + } + } + + It 'should return the build artifact data' { + Get-BuildArtifact -projectName project -id 2 + + Assert-MockCalled Invoke-RestMethod -Exactly -Scope It -Times 1 -ParameterFilter { + $Method -eq 'Get' -and + $Uri -eq 'https://test.visualstudio.com/project/_apis/build/builds/2/artifacts?api-version=2.0' + } + } + } + } } \ No newline at end of file