Reputation: 18465
With Azure DevOps how can I cancel the current build when a new one is started?
I want to save my build time. On my master branch sometime I do a merge then another developer do one and another do one again. I don't need to do build by build. I can keep only the latest build. So when a new build is queued this one can cancel all previous build.
Is it possible to setup his build definition to do so?
Upvotes: 16
Views: 15149
Reputation: 41625
There is no such feature in Azure DevOps.
The closest thing it's to use "Batching CI builds" - when a build is running, the system wait until the build is completed, then queues another build of all changes that have not yet been built.
To enable it in yaml build add this in the trigger
section:
batch: true
In the calssic editor, go to "Triggers" tab and mark the checkbox "Batch changes while a build is in progress".
Edit:
You can run a PowerShell script in the beginning of the build that cancel the running builds from the same definition:
$header = @{ Authorization = "Bearer $env:System_AccessToken" }
$buildsUrl = "$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_apis/builds/builds"
$builds = Invoke-RestMethod -Uri $url -Method Get -Header $header
$buildsToStop = $builds.value.Where({ ($.status -eq 'inProgress') -and ($_.definition.name -eq $(Build.DefinitionName)) -and ($_.id -ne $(Build.BuildId)) })
ForEach($build in $buildsToStop)
{
$build.status = "Cancelling"
$body = $build | ConvertTo-Json -Depth 10
$urlToCancel = "$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_apis/build/builds/$(builds.id)"
Invoke-RestMethod -Uri $urlToCancel -Method Patch -ContentType application/json -Body $body -Header $header
}
I used OAuth token for authorization (enable it on the job options) and in inline script ($(varName)
and not $env:varName
).
Now, if you have one build that running and someone else trigger another build that started to run, in this step the first build will be canceled.
Upvotes: 19
Reputation: 99
I could not find a solution to cancel the previous build run and all run stages, so I created a template task base on Python API calls.
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
addToPath: true
- script: |
python -m pip install --upgrade pip
pip install requests
- task: PythonScript@0
displayName: 'Run a Python script'
inputs:
scriptSource: inline
script: |
import requests
import base64
import json
TOKEN = '*******************************************'
authorization = str(base64.b64encode(bytes(':' + TOKEN, 'ascii')), 'ascii')
headers = {
'Accept': 'application/json',
'Authorization': 'Basic ' + authorization
}
TeamProject = '$(System.TeamProject)'
DefinitionName = '$(Build.DefinitionName)'
organization = '****************************************'
self_run_id = '$(Build.BuildId)'
print (f"self_run_id - {self_run_id}")
def get_url(url) :
response = requests.get(url=url, headers=headers)
return response
def get_current_project_runs():
pipelines_runs = {}
current_project_runs = get_url(
f"https://dev.azure.com/{organization}/{TeamProject}/_apis/build/builds?statusFilter=inProgress,postponed,notStarted&api-version=7.1-preview.7")
for run in current_project_runs.json().get('value'):
pipeline_name = run.get('definition').get('name')
run_id = run.get('id')
if pipelines_runs.get(pipeline_name):
pipelines_runs.get(pipeline_name).append(run_id)
else:
pipelines_runs.update({pipeline_name: [run_id]})
print(f"current_project_runs - {pipelines_runs}")
return pipelines_runs
def get_current_pipelines_runs(current_project_runs):
_current_project_runs = current_project_runs.get(DefinitionName)
print(f"_pipelines_runs - {_current_project_runs}")
return _current_project_runs
def get_stage_names(build_number):
# buildNumber
stages_name_list = []
url = f"https://dev.azure.com/{organization}/{TeamProject}/_apis/build/builds/{build_number}/Timeline"
current_project_runs = requests.get(url=url, headers=headers)
for stage_data in current_project_runs.json().get('records'):
if stage_data.get('type') == 'Stage':
stages_name_list.append(stage_data.get('name'))
return stages_name_list
def kill_stages(build_number):
stages_name_list = get_stage_names(build_number)
for stage in stages_name_list:
kill_stages_url = f"https://dev.azure.com/{organization}/{TeamProject}/_apis/build/builds/{build_number}/stages/{stage}?api-version=7.1-preview.1"
stage_kill = requests.patch(url=kill_stages_url, headers=headers, json={"state": "cancel"})
print(f"Stage_kill {kill_stages_url} - {stage_kill} - ")
def kill_duplicate_runs(pipelines_runs):
if pipelines_runs and len(pipelines_runs) > 1:
for run_id in sorted(pipelines_runs)[:-1]:
url = f"https://dev.azure.com/{organization}/{TeamProject}/_apis/build/builds/{run_id}?api-version=7.1-preview.7"
kill_response = requests.patch(url=url, headers=headers, json={"status": "cancelling"})
print(f'kill_response {url} - {kill_response}')
kill_stages(run_id)
current_project_runs = get_current_project_runs()
pipelines_runs = get_current_pipelines_runs(current_project_runs)
kill_duplicate_runs(pipelines_runs)
The script checks if it is more than one run of the pipeline, and cancels old runs/stages.
Also, the script can cancel the current run if new ran ware added.
- template: KillPreviousRuns.yml@templates
Upvotes: 2
Reputation: 665
The Powershell from the accepted answer contains a couple of typos and no longer works for Azure DevOps late 2021. Here's a current example for version 6.0 of the Azure DevOps API including YAML:
parameters:
- name: cancelPriorDeployments
displayName: Cancel prior deployments
type: boolean
default: true
variables:
cancelPriorDeployments: '${{ parameters.cancelPriorDeployments }}'
devOpsApiVersion: 6.0
stages:
- stage: CancelPriorDeploymentsStage
displayName: Cancel prior deployments
condition: eq(variables.cancelPriorDeployments, 'true')
jobs:
- job: CancelPriorDeploymentsJob
displayName: List builds, cancel prior in progress
pool:
vmImage: 'windows-latest'
steps:
- checkout: none
- task: PowerShell@2
displayName: Powershell AzDO Invoke-RestMethod
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
inputs:
targetType: inline
script: |
$header = @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" }
$buildsUrl = "$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_apis/build/builds?api-version=$(devOpsApiVersion)"
Write-Host "GET $buildsUrl"
$builds = Invoke-RestMethod -Uri $buildsUrl -Method Get -Header $header
$buildsToStop = $builds.value.Where({ ($_.status -eq 'inProgress') -and ($_.definition.name -eq "$(Build.DefinitionName)") -and ($_.id -lt $(Build.BuildId)) })
ForEach($build in $buildsToStop)
{
$urlToCancel = "$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_apis/build/builds/$($build.id)?api-version=$(devOpsApiVersion)"
$body = @{ status = "cancelling" } | ConvertTo-Json
Write-Host "PATCH $urlToCancel"
Invoke-RestMethod -Uri $urlToCancel -Method Patch -Header $header -ContentType application/json -Body $body
}
In addition, you will need to grant your build user permissions to "Stop Builds":
Triggers
Security
Groups
section on the left and select your build user from the Users
section.Stop builds
from Not set
to Allow
:Without this permission, you'll see an error message similar to:
Access denied. {Your Project} Build Service ({your organisation}) needs Stop builds permissions for vstfs:///Build/Build/12345 in team project {Your Project} to perform the action.
Upvotes: 13
Reputation: 11217
In you specific use case (building master branch) it seems better to batch the jobs. Usually you don't want to stop master build in the middle.
I came here searching for a way to cancel branch builds when there is a new commit. Eventually I found out that you can use PR builds instead, that are canceled by Github webhook automatically. PR are default and automated, so you only need to stop building branches.
# will not build any branch, only PRs because PR are canceled
# automatically after new commit is pushed
trigger: none
Upvotes: 1