[Workaround] Auto-cancel redundant builds on the default branch

By design the “Auto-cancel redundant builds” functionality does not apply to your project’s default branch.

This workaround can also be used if for any reason you prefer not to enable the “Auto-cancel redundant builds” functionality at the project-level.

Below is a set of instructions that will allow you to auto-cancel redundant workflows on any branch (including the default branch).
You can either:

  • Put the commands into a script within your repository and call it at the start of the workflow (either within a separate job or at the start of the first existing job in the workflow)
  • Add the commands within a job (new or existing) or as pre-steps to another job.

Note that you’ll need to add a personal API key as an environment variable MyToken .

#!/bin/bash

## Get the name of the workflow and the related pipeline number
curl --header "Circle-Token: $MyToken" --request GET "https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}" -o current_workflow.json
WF_NAME=jq -r '.name' current_workflow.json
CURRENT_PIPELINE_NUM=jq -r '.pipeline_number' current_workflow.json

## Get the IDs of pipelines created by the current user on the same branch. (Only consider pipelines that have a pipeline number inferior to the current pipeline)
PIPE_IDS=$(curl --header "Circle-Token: $MyToken" --request GET "https://circleci.com/api/v2/project/gh/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline?branch=$CIRCLE_BRANCH"|jq -r --arg CIRCLE_USERNAME "$CIRCLE_USERNAME" --argjson CURRENT_PIPELINE_NUM "$CURRENT_PIPELINE_NUM" '.items[]|select(.state == "created")|select(.trigger.actor.login == $CIRCLE_USERNAME)|select(.number < $CURRENT_PIPELINE_NUM)|.id')

## Get the IDs of currently running/on_hold workflows that have the same name as the current workflow, in all previously created pipelines.
if [ ! -z "$PIPE_IDS" ]; then
  for PIPE_ID in $PIPE_IDS
  do
    curl --header "Circle-Token: $MyToken" --request GET "https://circleci.com/api/v2/pipeline/${PIPE_ID}/workflow"|jq -r --arg WF_NAME "${WF_NAME}" '.items[]|select(.status == "on_hold" or .status == "running") | select(.name == $WF_NAME) | .id' >> WF_to_cancel.txt
  done
fi

## Cancel any currently running/on_hold workflow with the same name
if [ -s WF_to_cancel.txt ]; then
  echo "Cancelling the following workflow(s):"
  cat WF_to_cancel.txt 
  while read WF_ID;
    do
      curl --header "Circle-Token: $MyToken" --request POST https://circleci.com/api/v2/workflow/$WF_ID/cancel
    done < WF_to_cancel.txt
  ## Allowing some time to complete the cancellation
  sleep 2
  else
    echo "Nothing to cancel"
fi

In case you wish to auto-cancel redundant builds regardless of the user, replace:

PIPE_IDS=$(curl --header "Circle-Token: $MyToken" --request GET "https://circleci.com/api/v2/project/gh/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline?branch=$CIRCLE_BRANCH"|jq -r --arg CIRCLE_USERNAME "$CIRCLE_USERNAME" --argjson CURRENT_PIPELINE_NUM "$CURRENT_PIPELINE_NUM" '.items[] | select(.state == "created") | select(.trigger.actor.login == $CIRCLE_USERNAME)|select(.number < $CURRENT_PIPELINE_NUM)|.id')

with

PIPE_IDS=$(curl --header "Circle-Token: $MyToken" --request GET "https://circleci.com/api/v2/project/gh/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline?branch=$CIRCLE_BRANCH"|jq -r --argjson CURRENT_PIPELINE_NUM "$CURRENT_PIPELINE_NUM" '.items[] | select(.state == "created") | select(.number < $CURRENT_PIPELINE_NUM)|.id')
1 Like

Is there any way to generate a token that only has access to a specific project? If I have multiple projects in CircleCI and generate a key it’d effectively give people access to all of my projects, even if they only have access to certain projects

@JakeBooher,

The above solution leverages CircleCI API v2, and project API tokens are currently not supported by API v2.

The only workaround I can suggest is to create a machine user for each project, and generate a personal API token for each.

…or one machine user with appropriate access to specific projects.

This isn’t really good enough tbh.

if you have two workflows that run close enough together it’s actually possible for them to cancel each other.

1 Like

There’s still no way to accomplish this without such a script. It’s ridiculous that this isn’t a checkbox (EG: Enable on all branches).

4 Likes

I think this is a business choice that CircleCi made to make more money… I also think it is ridiculous that I have to pay for these redundant builds for default branch that do not add any value to our testing process!

To CircleCi admins: please make a checkbox for cancelling redundant builds for default branch also

3 Likes

Throwing in my vote for this too

1 Like

using a script for simple operational things like this seems overly complex…

+1 for a checkbox

2 Likes

This should clearly be an option in the UI.

Is there a way to list the branches that we’d want to not auto-cancel? Right now, it’s the default branch, however integration branches, particularly the top-level integration branch (like ‘develop’), would want all builds to continue.

:wave: Hi @frey,

At the moment, this is not possible. But what you can do is disable the feature altogether, and use the same logic as that in this workaround, with a condition on the branch name.

You could, for example, add a case statement at the start of the script to condition its execution on the CIRCLE_BRANCH built-in environment variable having specific values.

I would also encourage you to add a vote to this feature request.

Thanks for the reply. Has someone built an orb that implements this script?

@frey , there’s no certified/CircleCI-branded orb. As for a third-party orb, you’d need to look into the orb registry.

Script updated to address a potential issue raised by a user in this thread:

if you have two workflows that run close enough together it’s actually possible for them to cancel each other.

A post was split to a new topic: Moderated posts for review

For me the script didn’t work as is; 2 problems:

  • jq statements needed to be enclosed in $()
  • for bitbucket repositories, you need to use /bb/ instead of /gh/ when making the Circleci API calls;

Here’s the updated version of the script that worked for me; please note to replace use /bb/ or /gh/ depending on your integration:

#!/bin/bash

## Get the name of the workflow and the related pipeline number
curl --header "Circle-Token: $MyToken" --request GET "https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}" -o current_workflow.json
WF_NAME=$(jq -r '.name' current_workflow.json)
CURRENT_PIPELINE_NUM=$(jq -r '.pipeline_number' current_workflow.json)

## Get the IDs of pipelines created by the current user on the same branch. (Only consider pipelines that have a pipeline number inferior to the current pipeline)
PIPE_IDS=$(curl --header "Circle-Token: $MyToken" --request GET "https://circleci.com/api/v2/project/bb/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline?branch=$CIRCLE_BRANCH"|jq -r --argjson CURRENT_PIPELINE_NUM "$CURRENT_PIPELINE_NUM" '.items[] | select(.state == "created") | select(.number < $CURRENT_PIPELINE_NUM)|.id')

## Get the IDs of currently running/on_hold workflows that have the same name as the current workflow, in all previously created pipelines.
if [ ! -z "$PIPE_IDS" ]; then
  for PIPE_ID in $PIPE_IDS
  do
    curl --header "Circle-Token: $MyToken" --request GET "https://circleci.com/api/v2/pipeline/${PIPE_ID}/workflow"|jq -r --arg WF_NAME "${WF_NAME}" '.items[]|select(.status == "running") | select(.name == $WF_NAME) | .id' >> WF_to_cancel.txt
  done
fi

## Cancel any currently running/on_hold workflow with the same name
if [ -s WF_to_cancel.txt ]; then
  echo "Cancelling the following workflow(s):"
  cat WF_to_cancel.txt 
  while read WF_ID;
    do
      curl --header "Circle-Token: $MyToken" --request POST https://circleci.com/api/v2/workflow/$WF_ID/cancel
    done < WF_to_cancel.txt
  ## Allowing some time to complete the cancellation
  sleep 2
  else
    echo "Nothing to cancel"
fi

Following up on this, we now successfully enabled auto cancellation for redundant build except for a set of protected branches (here: master and production)

set -e

PROTECTED_BRANCHES="master production"

# Check if the current branch is protected
if [[ " $PROTECTED_BRANCHES " =~ " $CIRCLE_BRANCH " ]]; then
  echo "Current branch is protected. No workflows will be canceled."
  exit 0
fi

WORKFLOW_INFO=$(curl -s --header "Circle-Token: $CIRCLE_CI_API_TOKEN" \
                        --request GET \
                        "https://circleci.com/api/v2/workflow/${CIRCLE_WORKFLOW_ID}")

WF_NAME=$(echo "$WORKFLOW_INFO" | jq -r '.name')
CURRENT_PIPELINE_NUM=$(echo "$WORKFLOW_INFO" | jq -r '.pipeline_number')

PIPE_IDS=$(curl -s --header "Circle-Token: $CIRCLE_CI_API_TOKEN" \
         --request GET \
         "https://circleci.com/api/v2/project/gh/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline?branch=$CIRCLE_BRANCH" | \
      jq -r --argjson CURRENT_PIPELINE_NUM "$CURRENT_PIPELINE_NUM" \
            'if .items and .items != [] then .items[] | select(.state == "created") | select(.number < $CURRENT_PIPELINE_NUM)|.id else empty end')

if [ ! -z "$PIPE_IDS" ]; then
  echo "Found pipelines to check: $PIPE_IDS"
  for PIPE_ID in $PIPE_IDS; do
    WORKFLOWS_TO_CANCEL=$(curl -s --header "Circle-Token: $CIRCLE_CI_API_TOKEN" \
                                  --request GET \
                                  "https://circleci.com/api/v2/pipeline/${PIPE_ID}/workflow" | \
                             jq -r --arg WF_NAME "$WF_NAME" \
                                   '.items[] | select(.status == "running" or .status == "on_hold") | select(.name == $WF_NAME) | .id')

    if [ ! -z "$WORKFLOWS_TO_CANCEL" ]; then
      echo "Cancelling the following workflow(s) in pipeline $PIPE_ID: $WORKFLOWS_TO_CANCEL"
      for WF_ID in $WORKFLOWS_TO_CANCEL; do
        curl -s --header "Circle-Token: $CIRCLE_CI_API_TOKEN" \
                --request POST \
                "https://circleci.com/api/v2/workflow/${WF_ID}/cancel"
      done
    fi
  done
else
  echo "No redundant pipelines found to cancel."
fi

This script is really useful. Thanks!!

I added functionality to auto-cancel jobs within the last 10 minutes, otherwise allow them to keep running. If that is interesting to anybody, see