diff --git a/docs/preview/features/powershell/azure-devops.md b/docs/preview/features/powershell/azure-devops.md
index 35fd0082..b495ff30 100644
--- a/docs/preview/features/powershell/azure-devops.md
+++ b/docs/preview/features/powershell/azure-devops.md
@@ -10,6 +10,7 @@ This module provides the following capabilities:
- [Setting a variable in an Azure DevOps pipeline](#setting-a-variable-in-an-azure-devops-pipeline)
- [Setting ARM outputs to Azure DevOps variable group](#setting-arm-outputs-to-azure-devops-variable-group)
- [Setting ARM outputs to Azure DevOps pipeline variables](#setting-arm-outputs-to-azure-devops-pipeline-variables)
+- [Save Azure DevOps build](#save-azure-devops-build)
## Installation
@@ -105,3 +106,20 @@ PS> Set-AzDevOpsArmOutputsToPipelineVariables -ArmOutputsEnvironmentVariableName
# The pipeline variable my-variable will be updated to value my-value, so it can be used in subsequent tasks of the current job.
# ##vso[task.setvariable variable=my-variable]my-value
```
+
+## Save Azure DevOps build
+
+Saves/retains a specific Azure DevOps pipeline run.
+
+| Parameter | Mandatory | Description |
+| --------------- | --------- | ---------------------------------------------------------------------------|
+| `ProjectId` | yes | The Id of the Project where the build that must be retained can be found |
+| `BuildId` | yes | The Id of the build that must be retained |
+
+**Example**
+
+```powershell
+PS> Save-AzDevOpsBuild -ProjectId $(System.TeamProjectId) -BuildId $(Build.BuildId)
+# The variables $(System.TeamProjectId) and $(Build.BuildId) are predefined Azure DevOps variables
+# Information on them can be found here: https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml
+```
\ No newline at end of file
diff --git a/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.psd1 b/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.psd1
index b6af9a06..fff64231 100644
Binary files a/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.psd1 and b/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.psd1 differ
diff --git a/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.psm1 b/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.psm1
index f3b5d062..bf51fa00 100644
--- a/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.psm1
+++ b/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.psm1
@@ -72,4 +72,31 @@ function Set-AzDevOpsArmOutputsToPipelineVariables {
. $PSScriptRoot\Scripts\Set-AzDevOpsArmOutputs.ps1 -ArmOutputsEnvironmentVariableName $ArmOutputsEnvironmentVariableName -UpdateVariablesForCurrentJob
}
-Export-ModuleMember -Function Set-AzDevOpsArmOutputsToPipelineVariables
\ No newline at end of file
+Export-ModuleMember -Function Set-AzDevOpsArmOutputsToPipelineVariables
+
+<#
+ .Synopsis
+ Indicates that the specified DevOps pipeline-run must be retained indefinetely.
+
+ .Description
+ Indicates that the specified DevOps pipeline-run must be retained indefinetely.
+
+ .Parameter ProjectId
+ The Id of the Project in Azure DevOps to which the build that must be retained, belongs to.
+ (You can use the predefined variable $(System.TeamProjectId) in an Azure DevOps pipeline).
+
+ .Parameter BuildId
+ The Id of the Build that must be retained.
+ (You can use the predefined variable $(Build.BuildId) in an Azure DevOps pipeline).
+
+#>
+function Save-AzDevOpsBuild {
+ param(
+ [Parameter(Mandatory = $true)][string] $ProjectId = $(throw "ProjectId is required"),
+ [Parameter(Mandatory = $true)][string] $BuildId = $(throw "BuildId is required")
+ )
+
+ . $PSScriptRoot\Scripts\Save-AzDevOpsBuild.ps1 -ProjectId $ProjectId -BuildId $BuildId
+}
+
+Export-ModuleMember -Function Save-AzDevOpsBuild
\ No newline at end of file
diff --git a/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.pssproj b/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.pssproj
index 435afd35..1ba027ce 100644
--- a/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.pssproj
+++ b/src/Arcus.Scripting.DevOps/Arcus.Scripting.DevOps.pssproj
@@ -33,6 +33,7 @@
+
diff --git a/src/Arcus.Scripting.DevOps/Scripts/Save-AzDevOpsBuild.ps1 b/src/Arcus.Scripting.DevOps/Scripts/Save-AzDevOpsBuild.ps1
new file mode 100644
index 00000000..de4dc042
--- /dev/null
+++ b/src/Arcus.Scripting.DevOps/Scripts/Save-AzDevOpsBuild.ps1
@@ -0,0 +1,28 @@
+param(
+ [Parameter(Mandatory = $true)][string] $ProjectId = $(throw "ProjectId is required"),
+ [Parameter(Mandatory = $true)][string] $BuildId = $(throw "BuildId is required")
+)
+
+$retentionPayload = @{
+ keepforever='true'
+}
+
+$requestBody = $retentionPayload | ConvertTo-Json -Depth 1 -Compress
+
+$collectionUri = $env:SYSTEM_COLLECTIONURI
+
+if( $collectionUri.EndsWith('/') -eq $False ){
+ $collectionUri = $collectionUri + '/'
+}
+
+$requestUri = "$collectionUri" + "$ProjectId/_apis/build/builds/" + $BuildId + "?api-version=6.0"
+
+Write-Verbose "Saving AzDevOps build with buildid $BuildId in project $ProjectId by posting $requestBody to $requestUri"
+
+$response = Invoke-WebRequest -Uri $requestUri -Method Patch -Body $requestBody -ContentType "application/json" -Headers @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" }
+
+if( $response.StatusCode -ne 200 ) {
+ Throw "Unable to retain build indefinetely. API request returned statuscode $($response.StatusCode)"
+}
+
+exit 0
diff --git a/src/Arcus.Scripting.Tests.Unit/Arcus.Scripting.DevOps.tests.ps1 b/src/Arcus.Scripting.Tests.Unit/Arcus.Scripting.DevOps.tests.ps1
index 70313a47..ecc9441b 100644
--- a/src/Arcus.Scripting.Tests.Unit/Arcus.Scripting.DevOps.tests.ps1
+++ b/src/Arcus.Scripting.Tests.Unit/Arcus.Scripting.DevOps.tests.ps1
@@ -1,7 +1,7 @@
Describe "Arcus" {
Context "Azure DevOps" {
InModuleScope Arcus.Scripting.DevOps {
- It "Seting DevOps variable should write to host" {
+ It "Setting DevOps variable should write to host" {
# Arrange
Mock Write-Host { $Object | Should -Be "#vso[task.setvariable variable=test] value" } -Verifiable
@@ -120,6 +120,76 @@
Assert-VerifiableMock
Assert-MockCalled Write-Host
}
- }
+ It "Save-AzDevOpsBuild fails when API call does not return success-code" {
+ # Arrange
+ $env:SYSTEM_COLLECTIONURI = "https://dev.azure.com/myorganization/"
+ $env:ACCESS_TOKEN = "mocking accesstoken"
+ $projectId = "abc123"
+ $buildId = 128
+
+ Mock Invoke-WebRequest {
+ $statusCode = 400
+ $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
+ return $response
+ } -ModuleName Arcus.Scripting.DevOps
+
+ # Act and Assert
+ { Save-AzDevOpsBuild -ProjectId $projectId -BuildId $buildId } | Should -Throw
+ }
+ It "Save-AzDevOpsBuild succeeds when API call does return success-code" {
+ # Arrange
+ $env:SYSTEM_COLLECTIONURI = "https://dev.azure.com/myorganization/"
+ $env:ACCESS_TOKEN = "mocking accesstoken"
+ $projectId = "abc123"
+ $buildId = 128
+
+ Mock Invoke-WebRequest {
+ $statusCode = 200
+ $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
+ return $response
+ } -ModuleName Arcus.Scripting.DevOps
+
+ # Act and Assert
+ { Save-AzDevOpsBuild -ProjectId $projectId -BuildId $buildId } | Should -Not -Throw
+ }
+ It "Save-AzDevOpsBuild correctly builds API endpoint when CollectionUri has trailing slash" {
+ # Arrange
+ $env:SYSTEM_COLLECTIONURI = "https://dev.azure.com/myorganization/"
+ $env:ACCESS_TOKEN = "mocking accesstoken"
+ $projectId = "abc123"
+ $buildId = 128
+
+ Mock Invoke-WebRequest {
+ $statusCode = 200
+ $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
+ return $response
+ } -ModuleName Arcus.Scripting.DevOps
+
+ # Act
+ Save-AzDevOpsBuild -ProjectId $projectId -BuildId $buildId
+
+ # Assert
+ Should -Invoke -CommandName Invoke-WebRequest -Times 1 -ParameterFilter { $Uri -Like "https://dev.azure.com/myorganization/$projectId/*" }
+ }
+ It "Save-AzDevOpsBuild correctly builds API endpoint when CollectionUri does not have trailing slash" {
+ # Arrange
+ $env:SYSTEM_COLLECTIONURI = "https://dev.azure.com/myorganization"
+ $env:ACCESS_TOKEN = "mocking accesstoken"
+ $projectId = "abc123"
+ $buildId = 128
+
+ Mock Invoke-WebRequest {
+ $statusCode = 200
+ $response = New-Object System.Net.Http.HttpResponseMessage $statusCode
+ return $response
+ } -ModuleName Arcus.Scripting.DevOps
+
+ # Act
+ Save-AzDevOpsBuild -ProjectId $projectId -BuildId $buildId
+
+ # Assert
+ Should -Invoke -CommandName Invoke-WebRequest -Times 1 -ParameterFilter { $Uri -Like "https://dev.azure.com/myorganization/$projectId/*" }
+ }
+ }
}
}