﻿# Azure Pipelines 無法刪除 Build Pipeline 的處理方法

有時候我們想要在 Azure DevOps Services 上面刪除一些早期的 Pipeline (Build)，但是刪除時卻會遇到 **One or more builds associated with the requested pipeline(s) are retained by a release. The pipeline(s) and builds will not be deleted.** 的問題，導致我們怎樣都無法刪除。這篇文章我打算來說明一下幾個重要觀念，以及如何才能成功刪除這些用不到的 Pipelines。

### 發生錯誤的過程

1.  刪除 Pipeline (Build)
    
    ![刪除 Pipeline](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/eed347699f35c377e4d5-172087159-0568cb7d-91bd-44ea-9327-ae5cf6170abc.jpg)
    
2.  刪除時出現以下錯誤訊息
    
    > One or more builds associated with the requested pipeline(s) are retained by a release. The pipeline(s) and builds will not be deleted.
    
    ![One or more builds associated with the requested pipeline(s) are retained by a release. The pipeline(s) and builds will not be deleted.](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/ec0944bfe01223d45a35-172087075-717d3e13-4c02-40a8-895c-7f46f2e8a2be.jpg)
    

我簡單翻譯一下這段錯誤訊息：

-   One or more builds associated with the requested pipeline(s) are retained by a release.
    
    你所要求刪除的 `pipeline(s)` 的一個或多個 `builds` (建置) 被一個 `release` (發行) 所**保留**(`retained`)。
    
-   The pipeline(s) and builds will not be deleted.
    
    這些 `pipeline(s)` 以及 `builds` 將不會被刪除。
    

我們在 Azure DevOps Services 之中的重要觀念如下：

1.  一個 `pipeline` 會包含一個或多個 `builds` (有時候 builds 也會說成 runs)
    
2.  一個 `release` 可能會從 `pipeline` 取得 `builds` 來發行
    
3.  所以 `release` 跟 `pipeline` 產生的 `build` 之間會有一個或多個 `leases`(租約) 關係
    
4.  在 `release` 之中可以對 `builds` 設定 `Retain` (保留) 的條件
    
    ![Release - Retention](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/004ecb6d31175568064c-172088482-14f42291-6179-4c61-9166-08d175b2d1df.jpg)
    
5.  早期 `release` 的設定預設會**永久保留**所有的 `builds`，但現在的 `release` 最多只會保留 `365` 天，而且在**專案設定**中有明確的**預設值**設定！
    
    ![Project Settings - Retention policy settings](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/481e52056a0ff417e455-172088761-c1e866d5-e49a-4899-ac4a-f67eceb36d7e.jpg)
    

簡單來說，就是你只要有設定 `release` (`CD`)，我們通常都會關聯一個 Pipelines (`CI`) 的 `build` (通常是最新版的 `build artifact`)，所以這些 `builds` 或 `runs` 會預設保留下來一段時間，保留多久是由一條一條的 `lease` 決定的。重點是，只要這些 `builds` 被**保留**住，你就無法刪除 `pipeline`！

### 如何才能順利刪除 Pipeline

事實上，就算你手動刪除 Release 也是無法解鎖這些 Builds 的，因為刪除 Release 之後會被放到「資源回收匣」達 30 天，所以關聯的 Builds 依然無法被刪除。

![Deleted pipelines](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/11b22d4e512ae7b3966a-172089577-f46e1cad-685e-43ec-b8fc-230c4964bb0b.jpg)

所以重點就是如何讓這些 Builds 不要被**保留**(Retain)，方法如下：

1.  進入特定 Pipeline 並顯示所有 Builds，對**每一個** Builds 選擇 `View retention leases` (檢視保留租約) 設定
    
    ![View retention leases](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f2d9bbe5170c78cff23d-172089810-e5b42991-760f-4799-9925-473d8878bc73.jpg)
    
2.  你可以在 **Run retention** 對話框看到這個 Build 的保留原因，並且可以按下 **Remove all** 刪除所有保留設定
    
    ![Run retention](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/0c566c0b91cfcc971e89-172091193-992af346-2887-4836-80e2-13fa1cecac2c.jpg)
    
3.  當你把所有 Builds 的 `Leases` 都移除，就可以順利刪除 Pipeline 了！
    
    是的，就是要一筆一筆刪除，有點麻煩！
    

### 批次刪除 Leases 的方法

如果要批次刪除，只能透過 [Leases - REST API (Azure DevOps Build)](https://learn.microsoft.com/en-us/rest/api/azure/devops/build/leases?view=azure-devops-rest-6.0&WT.mc_id=DT-MVP-4015686) 來刪除了。

我已經寫好一份了，超好用：

-   刪除**特定專案**下**所有 Pipelines** (Build Definition) 底下所有的 `builds` 底下所有的 `leases`
    
    ```powershell
    $personalAccessToken = "[PAT]"
    
    $organization = "[OrgId]"
    $project = "[ProjectId]"
    
    $apiVersion = "6.0"
    
    $token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($personalAccessToken)"))
    $header = @{authorization = "Basic $token"}
    
    function DeleteLease($definitionId) {
        $url = "https://dev.azure.com/$organization/$project/_apis/build/retention/leases?api-version=$($apiVersion)&definitionId=$definitionId"
        $leases = (Invoke-RestMethod -Method GET -Uri $url -ContentType "application/json" -Headers $header )
    
        foreach ($lease in $leases.value) {
            $leaseId = $lease.leaseId
            $url = "https://dev.azure.com/$organization/$project/_apis/build/retention/leases?ids=$($leaseId)&api-version=$($apiVersion)"
            $ignore = Invoke-RestMethod -Method DELETE -Uri $url -ContentType "application/json" -Headers $header
            Write-Host "DELETE $url"
        }
    }
    
    $url = "https://dev.azure.com/$organization/$project/_apis/build/definitions?api-version=$apiVersion"
    Write-Host $url
    
    $buildDefinitions = Invoke-RestMethod -Uri $url -Method Get -ContentType "application/json" -Headers $header
    
    foreach ($def in $builddefinitions.value) {
        Write-Host $def.id $def.queueStatus $def.name
        DeleteLease $def.id
    }
    ```
    
-   刪除**特定專案**下**特定一個 Pipelines** (Build Definition) 底下所有 `builds` 的 `leases`
    
    ```powershell
    $personalAccessToken = "[PAT]"
    
    $organization = "[OrgId]"
    $project = "[ProjectId]"
    $definitionId = "[BuildDefinitionId]"
    
    $apiVersion = "6.0"
    
    $token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($personalAccessToken)"))
    $header = @{authorization = "Basic $token"}
    
    function DeleteLease($definitionId) {
        $url = "https://dev.azure.com/$organization/$project/_apis/build/retention/leases?api-version=$($apiVersion)&definitionId=$definitionId"
        $leases = (Invoke-RestMethod -Method GET -Uri $url -ContentType "application/json" -Headers $header )
    
        foreach ($lease in $leases.value) {
            $leaseId = $lease.leaseId
            $url = "https://dev.azure.com/$organization/$project/_apis/build/retention/leases?ids=$($leaseId)&api-version=$($apiVersion)"
            $ignore = Invoke-RestMethod -Method DELETE -Uri $url -ContentType "application/json" -Headers $header
            Write-Host "DELETE $url"
        }
    }
    
    $url = "https://dev.azure.com/$organization/$project/_apis/build/definitions?api-version=$apiVersion"
    Write-Host $url
    
    $buildDefinitions = Invoke-RestMethod -Uri $url -Method Get -ContentType "application/json" -Headers $header
    
    DeleteLease $definitionId
    ```
    
    你可以從網址列上取得 `$definitionId`，如下圖示：
    
    ![definitionId](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/e894e397132f3605d082-172096146-4bb3013c-7e80-4cd4-83e6-3dcaef774833.jpg)
    

### 相關連結

-   [REST API samples - Azure DevOps | Microsoft Docs](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/rest/samples?view=azure-devops&WT.mc_id=DT-MVP-4015686)
-   [Definitions - List - REST API (Azure DevOps Build) | Microsoft Docs](https://learn.microsoft.com/en-us/rest/api/azure/devops/build/definitions/list?view=azure-devops-rest-6.0&WT.mc_id=DT-MVP-4015686)
-   [Leases - REST API (Azure DevOps Build) | Microsoft Docs](https://learn.microsoft.com/en-us/rest/api/azure/devops/build/leases?view=azure-devops-rest-6.0&WT.mc_id=DT-MVP-4015686)
-   [azure devops - Build pipeline does not delete - Stack Overflow](https://stackoverflow.com/a/69586122/910074)
