Azure DevOps Server 2020で、暫定処理として、古い自動ビルドを削除する

# 概要

Azure DevOps Server 2020のあるバージョンには設定した保持ルールで自動ビルドが削除されないという不具合がある（[2022年5月17日に改善されている](https://learn.microsoft.com/ja-jp/azure/devops/server/release-notes/azuredevops2020u1?view=azure-devops#azure-devops-server-2020-update-12-release-date-may-17-2022)）

PR時などに自動CIを行っている場合、バックアップ容量などの増加を招くことがある。

何らかの理由で最新バージョン、またはパッチ適用できないが、保持ルールに適合していないものを削除したい場合に、次のPowerShellスクリプトを利用する。

# 事前準備

## 個人用アクセストークンを取得する

- 「ビルド（読み取りおよび実行）」権限が必要

https://learn.microsoft.com/ja-jp/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops-2020&tabs=Windows

## スクリプトを実行

### 概要

- 指定プロジェクトの指定パイプラインで、保護されていない、かつ20日以前のものを削除

### スクリプト内の設定

- `$personalAccessToken`に前項で取得したトークンを指定
- `<ProjectName>`にAzure DevOps Serverのプロジェクト名を指定
- `<AzureDevOpsServerName>`にAzure DevOps ServerのURLを指定（例：`http://sampleserver:8080/tfs/`）
- 必要に応じ`$collection`、`$pipelineName`を指定

### スクリプト内の設定


```powershell
#Azure DevOps Personal Access Token
$personalAccessToken = "<AccessToken>"
$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($personalAccessToken)"))
$header = @{authorization = "Basic $token" }

$collection = "DefaultCollection"
$project = "<ProjectName>"
$urlbase = "<AzureDevOpsServerName>"

$pipelineName = "<ProjectName> CI"

#Get all build definitions
# API: GET https://{instance}/{collection}/{project}/_apis/build/builds?api-version=6.0
$url = "$urlbase/$collection/$project/_apis/build/definitions?api-version=6.0"
$allBuildDefinitions = Invoke-RestMethod -Uri $url -Method Get -ContentType "application/json" -Headers $header

$allBuildDefinitions.value | Where-Object { $_.name -eq $pipelineName } | ForEach-Object {
    Write-Host $_.id $_.name $_.queueStatus
 
    # For debugging reasons, just to be sure that we don't delete the wrong build pipeline
    if ( $_.name -ne $pipelineName ) {
        return;
    }

    #Get all Builds for a Definition
    # API: GET https://{instance}/{collection}/{project}/_apis/build/builds?definitions={definitions}&queues={queues}&buildNumber={buildNumber}&minTime={minTime}&maxTime={maxTime}&requestedFor={requestedFor}&reasonFilter={reasonFilter}&statusFilter={statusFilter}&resultFilter={resultFilter}&tagFilters={tagFilters}&properties={properties}&$top={$top}&continuationToken={continuationToken}&maxBuildsPerDefinition={maxBuildsPerDefinition}&deletedFilter={deletedFilter}&queryOrder={queryOrder}&branchName={branchName}&buildIds={buildIds}&repositoryId={repositoryId}&repositoryType={repositoryType}&api-version=6.0

    $maxTime=(Get-Date).AddDays(-20).ToString("yyyy-MM-dd")
    Write-Host "maxTime:" $maxTime

    $url = "$urlbase/$collection/$project/_apis/build/builds?definitions=" + $_.id + "&maxTime="  + $maxTime + "&deletedFilter=excludeDeleted&api-version=6.0"
    $allBuilds = Invoke-RestMethod -Uri $url -Method Get -ContentType "application/json" -Headers $header
    #Process each Build of Definition
    $allBuilds.value | Where-Object { $_.keepForever -eq $false }  | Sort-Object id | ForEach-Object {
        #Report on retain status
        Write-Host "Build Id:" $_.id " keepForever: " $_.keepForever " reason: " $_.reason " sourceBranch: " $_.sourceBranch " finishTime: " $_.finishTime

        #Delete Build
        # API: DELETE https://{instance}/{collection}/{project}/_apis/build/builds/{buildId}?api-version=6.0
        $url = "$urlbase/$collection/$project/_apis/build/builds/" + $_.id + "?api-version=6.0"
        Invoke-RestMethod -Uri $url -Method Delete -ContentType "application/json" -Headers $header

        #Report on Build deleted
        Write-Host "Build Id:" $_.id " deleted"
    }
}
    
Write-Host "Complete."
```

# 参考

https://learn.microsoft.com/en-us/rest/api/azure/devops/build/builds?view=azure-devops-server-rest-6.0

https://stackoverflow.com/questions/66264466/how-can-i-delete-azure-devops-old-build-pipelines-and-there-leases-with-power-sh