﻿# 如何批次取消 Azure Pipelines 中所有排隊中的 Runs 或 Builds

昨天我建立了一個有 18 個 Repos 的專案，在批次設定的過程，我肯定要用 Azure Pipelines 的 [YAML syntax](https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/?view=azure-pipelines&WT.mc_id=DT-MVP-4015686) 來撰寫，否則用傳統編輯器設定，肯定按到手酸。不過，在撰寫的過程中，有一次不小心寫壞了，導致一次 18 個 CI 同時啟動，但我只有四條 Pipelines 可用，一想到要等全部跑完才能測試下一輪，就覺得我應該要寫支小程式批次取消。因此這篇文章就是為此而生，讓你快速的取消所有尚未啟動的 Pipelines 作業。

要透過 Azure DevOps REST APIs 取消尚未啟動的建置作業(Builds)，基本上就兩個步驟：

1.  取得**尚未啟動**(notStarted)的建置作業清單
    
    ```bash
    Authorization: Basic BASE64ENCODE(':' + PAT)
    
    GET https://dev.azure.com/{organization}/{project}/_apis/build/builds?statusFilter=notStarted&api-version=6.0
    ```
    
    > [BuildStatus](https://learn.microsoft.com/en-us/rest/api/azure/devops/build/builds/update-build?view=azure-devops-rest-6.0&WT.mc_id=DT-MVP-4015686#buildstatus)
    
2.  更新建置作業內容
    
    ```bash
    Authorization: Basic BASE64ENCODE(':' + PAT)
    
    PATCH https://dev.azure.com/{organization}/{project}/_apis/build/builds/{buildId}?api-version=6.0
    
    {"status": "Cancelling"}
    ```
    
    > [BuildStatus](https://learn.microsoft.com/en-us/rest/api/azure/devops/build/builds/update-build?view=azure-devops-rest-6.0&WT.mc_id=DT-MVP-4015686#buildstatus)
    

而要透過 PowerShell 來呼叫 Azure DevOps REST APIs 則有兩種方法：

1.  使用 `Invoke-RestMethod` 呼叫 APIs
2.  使用 `az devops invoke` 呼叫 APIs

### 使用 `Invoke-RestMethod` 呼叫 APIs

使用 `Invoke-RestMethod` 的最大優點，就是「速度快」，沒有什麼繁瑣的檢查程序，拿到 PAT 之後就是直接發出 API 要求，快、狠、準。

我通常會把常用的 PAT 儲存在名為 `AZURE_DEVOPS_EXT_PAT` 的**環境變數**中，你也可以透過以下命令設定目前 Session 下的環境變數：

```powershell
$env:AZURE_DEVOPS_EXT_PAT = "{{PAT}}"
```

剩下的直接看程式碼說故事：

```powershell
function Cancel-AzurePipelineBuilds {
    param (
        $OrganizationName,
        $ProjectName,
        $Status
    )

    $AzureDevOpsAuthHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($env:AZURE_DEVOPS_EXT_PAT)")) }

    $Uri= "https://dev.azure.com/$($OrganizationName)/$($ProjectName)/_apis/build/builds?statusFilter=$($Status)&api-version=6.0"
    $Builds = Invoke-RestMethod -Uri $Uri -Headers $AzureDevOpsAuthHeader -Method get -ContentType "application/json"
    $BuildsToCancel = $Builds.value

    ForEach($build in $BuildsToCancel)
    {
      $build.status = "Cancelling"
      $body = $build | ConvertTo-Json -Depth 10
      $urlToCancel = "https://dev.azure.com/$($OrganizationName)/$($ProjectName)/_apis/build/builds/$($build.id)?api-version=6.0"
      Invoke-RestMethod -Uri $urlToCancel -Method Patch -ContentType application/json -Body $body -Header $AzureDevOpsAuthHeader | Out-Null
      Write-Output "BuildId $($build.id): Canceled ($($build.definition.name))"
    }
}

$OrganizationName = "{{OrgName}}"
$ProjectName = "{{ProjectName}}"

Cancel-AzurePipelineBuilds $OrganizationName $ProjectName 'notStarted'
Cancel-AzurePipelineBuilds $OrganizationName $ProjectName 'inProgress'
```

### 使用 `az devops invoke` 呼叫 APIs

使用 `az devops invoke` 的最大優點，就是可以預先設定好 `OrganizationName` 與 `ProjectName`

```bash
# 僅設定好預設的組織 (我通常都會這樣設定)
az devops configure --defaults organization=https://dev.azure.com/willh

# 設定好預設的組織與專案
az devops configure --defaults organization=https://dev.azure.com/willh project=AngularTW
```

然後最重要的，你只要準備好 Azure DevOps 的 PAT (Personal Access Token) 放到名為 `AZURE_DEVOPS_EXT_PAT` 的環境變數，就不需要再執行 `az devops login` 登入！

```powershell
$env:AZURE_DEVOPS_EXT_PAT = "{{PAT}}"
```

剩下的直接看程式碼說故事：

```bash
function Cancel-AzurePipelineBuilds {
    param (
        $ProjectName,
        $Status
    )

    $runs = $(az pipelines runs list -p $($ProjectName) --status $($Status) -o json | ConvertFrom-Json)

    $PatchFile = New-TemporaryFile
    echo '{"status": "Cancelling"}' | Out-File $PatchFile -Encoding utf8

    ForEach($run in $runs)
    {
      az devops invoke --area build --resource builds --route-parameters buildId=$($run.id) project=$($ProjectName) --in-file $PatchFile --http-method patch | Out-Null
      Write-Output "BuildId $($run.id): Canceled ($($run.definition.name))"
    }
}

$ProjectName = "{{ProjectName}}"

Cancel-AzurePipelineBuilds $ProjectName 'notStarted'
Cancel-AzurePipelineBuilds $ProjectName 'inProgress'
```

### 其他補充的背景知識

這裡我說明一下目前我所知道關於 Azure Pipelines 的重要名詞與其關聯：

![Key concepts for new Azure Pipelines users](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/17e4dfacd5f2e4e118ab-177815150-8443aa84-9493-410c-b2b7-d9fba7a98262.png)

-   所謂 [trigger](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#trigger) 是用來告訴一個 [pipeline](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#pipeline) 開始 [run](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#run) (執行)
-   一個 [pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#pipeline) 可能包含一個或多個 [stages](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#stage) (階段)
-   一組 [pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#pipeline) 可以一次部署到多個 [environments](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#environment) (環境)
-   一組 [pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#pipeline) 可以包含一個或多個 [stage](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#stage)
-   一個 [stage](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#stage) 可以包含一個或多個 [jobs](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#job)
-   一個 [jobs](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#job) 可以包含一個或多個 [steps](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#step) (執行步驟)
    -   [jobs](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#job) 可以跑在一個 [agent](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#agent) 上
    -   [jobs](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#job) 可以跑在一個 `agentless` 的環境上 (有些 [jobs](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#job) 是不需要 agent 就能執行)
-   一個 [step](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#step) 可以是一個 [task](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#task) 或是一個 [script](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#script)
    -   [task](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#task) 與 [script](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#script) 是一個 Pipeline 中的最小單位 (smallest building block)
    -   [task](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#task) 是一個預先封裝好的腳本，方便你可以快速的完成某些複雜的工作
    -   [task](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#task) 做不到的事情，都可以透過自定義的 [script](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#script) 來完成
    -   [script](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#script) 在 Windows Agent 預設是 Command Prompt
    -   [script](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#script) 在 Linux Agent 預設是 Bash
-   每個 [artifact](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#artifact) 是一堆檔案的集合，或是由某一次 [run](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#run) 發行的封裝檔

很複雜對不對？這已經是精鍊過的版本了，以前更亂！ 😆

> 微軟在各種產品與 API 的「命名」上一直有個 "好" 習慣，只要產品發展一段時間，只要覺得產品在演進的過程覺得當初名字取的不太好，就會直接改掉，沒在跟你五四三的。例如：以前 `Pipelines` 被稱之為 `Builds` 或 `Build pipelines`，而早期的 `Builds` 或 `Tests` 則被調整為 `Runs` 這個字。我覺得這樣改名也沒甚麼錯，這對「新人」比較有好處，因為新的名字可以比較好的建構更清楚的概念模型(Mindset)。不過唯一的缺點，就是對我們這些「老人」不太友善，一天到晚改名，改到懷疑人生耶。

上述提到的 [run](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686#run) 就是每次執行的紀錄，這是一個相對較新的名詞，早期這個名詞叫做 `build` 或 `test`！

所以我們常會在[官方文件](https://learn.microsoft.com/en-us/azure/devops/?view=azure-devops&WT.mc_id=DT-MVP-4015686)、[Azure DevOps Services REST API Reference](https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.1&WT.mc_id=DT-MVP-4015686) 或 [Azure DevOps CLI](https://learn.microsoft.com/en-us/azure/devops/cli/?toc=%2Fazure%2Fdevops%2Fdev-resources%2Ftoc.json&bc=%2Fazure%2Fdevops%2Fdev-resources%2Fbreadcrumb%2Ftoc.json&view=azure-devops&WT.mc_id=DT-MVP-4015686) 看到一些似是而非的名詞，真的讓人非常困惑。

所謂的 `Runs` 是一個抽象概念，我們在 [Azure DevOps CLI](https://learn.microsoft.com/en-us/azure/devops/cli/?toc=%2Fazure%2Fdevops%2Fdev-resources%2Ftoc.json&bc=%2Fazure%2Fdevops%2Fdev-resources%2Fbreadcrumb%2Ftoc.json&view=azure-devops&WT.mc_id=DT-MVP-4015686) 會看到一個 [az pipelines runs](https://learn.microsoft.com/en-us/cli/azure/pipelines/runs?view=azure-cli-latest&WT.mc_id=DT-MVP-4015686) 命令，他其實就是在講 `Builds` 的意思。

而我們在 [Azure DevOps Services REST API Reference](https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.1&WT.mc_id=DT-MVP-4015686) 看到的 [Build / Builds](https://learn.microsoft.com/en-us/rest/api/azure/devops/build/builds?view=azure-devops-rest-6.0&WT.mc_id=DT-MVP-4015686) 其實就是一樣的東西，他就是 `Runs` 的意思。

> 基本上，產品名稱要怎麼改都可以，但要是你改了 API 的端點，就會有一堆人跟你拼命！😅

好吧，我再來補充一些官方文件沒有寫的很清楚的關聯資訊：

-   一個 pipeline 包含了一個 definition
    -   想像 pipeline 是一棟蓋好的房子
    -   然而 definition 就是這棟房子的藍圖
    -   我們經常有修繕房子的需求，每次都會需要調整藍圖後才施工
    -   我們經常會調整 pipeline 的需求，每次都會調整 definition 之後才會跑 (run)
    -   因為 definition 有不斷變更的議題，所以 Azure DevOps 預設會將你每次修改進行版控
-   一個 run 就是一個 build，而每個 build/run 包含了一組當下的 build definition
-   你在 build 失敗的時候按下 re-run 按鈕，其實是把之前的 build definition 重跑一次而已，並不會去 Pipeline 抓取最新的 definition 來重新執行

### 相關連結

-   [Azure DevOps documentation](https://learn.microsoft.com/en-us/azure/devops/?view=azure-devops&WT.mc_id=DT-MVP-4015686)
-   [Azure DevOps Services REST API Reference](https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.1&WT.mc_id=DT-MVP-4015686)
-   [Azure DevOps CLI](https://learn.microsoft.com/en-us/azure/devops/cli/?toc=%2Fazure%2Fdevops%2Fdev-resources%2Ftoc.json&bc=%2Fazure%2Fdevops%2Fdev-resources%2Fbreadcrumb%2Ftoc.json&view=azure-devops&WT.mc_id=DT-MVP-4015686)
-   [YAML schema reference for Azure Pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/?view=azure-pipelines&WT.mc_id=DT-MVP-4015686)
-   [使用 Azure CLI 管理 Azure DevOps Services 服務](/post/2019/06/01/Manage-Azure-DevOps-Services-using-Azure-CLI)
-   [Key concepts for new Azure Pipelines users](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops&WT.mc_id=DT-MVP-4015686)
-   [Azure DevOps Pipelines - How to cancel multiple pending jobs](https://web.archive.org/web/20240809125214/https://brandonkorous.com/read/azure-devops-pipelines-how-to-cancel-multiple-pending-jobs)
