Cancel sibling snapshot dependency on failure
I was unable to find a way to configure the build to behave like I wanted. Instead I needed to start using the TeamCity API to achieve this.

In the build.sh and test.sh file that TC runs, I extended the failure function (simple bash function we call whenever our build script fails):

teamcityFailure () {
    . ./cancel_build_chain.sh
    echo "##teamcity[buildProblem description='$1']"
    exit 1
}


This now calls off to this new cancel_build_chain.sh script, which looks something like this:

#!/bin/bash

teamcityProgress () {
    echo "##teamcity[progressMessage '$1']"
}

teamcityProgress "Build step has failed, attempting to cancel the rest of the build chain..."

BUILD_ID=$TEAMCITY_BUILD_ID # passed in from build chain configuration

# Use the current failed step ID to query for the snapshotDependency "from:" here - which returns the final "docker publish" step
FINAL_STEP_DATA=$(curl --silent --request GET \
    "https://teamcity/app/rest/builds/project:MyProjectName,snapshotDependency:(from:(id:$BUILD_ID),includeInitial:true),defaultFilter:false" \
    --header "Content-Type: application/xml"")

# From the "docker publish" step, parse the XML and fetch out all dependent step IDs. One will be this current step, the others will be its sibling steps
# We do this twice, once for queued steps, and once for currently running steps. This is because we need to cancel running vs queued builds differently.
QUEUED_DEPENDENT_STEP_IDS=$(echo $FINAL_STEP_DATA \
 | grep -Eo '&lt;snapshot-dependencies.+&lt;build id="[0-9]+".+state="queued"' \
 | grep -Eo 'id="([0-9]+)"' \
 | grep -Eo '[0-9]+')

# Now remove the current step ID
QUEUED_DEPENDENT_STEP_IDS=${QUEUED_DEPENDENT_STEP_IDS/$BUILD_ID/}

# trim leading / trailing whitespace
QUEUED_DEPENDENT_STEP_IDS=QUEUED_DEPENDENT_STEP_IDS | sed 's/ *$//'

RUNNING_DEPENDENT_STEP_IDS=$(echo $FINAL_STEP_DATA \
 | grep -Eo '&lt;snapshot-dependencies.+&lt;build id="[0-9]+".+state="running"' \
 | grep -Eo 'id="([0-9]+)"' \
 | grep -Eo '[0-9]+')

# Now remove the current step ID
RUNNING_DEPENDENT_STEP_IDS=${RUNNING_DEPENDENT_STEP_IDS/$BUILD_ID/}

# trim leading / trailing whitespace
RUNNING_DEPENDENT_STEP_IDS=RUNNING_DEPENDENT_STEP_IDS | sed 's/ *$//'

for DEP_ID in $QUEUED_DEPENDENT_STEP_IDS
do
    teamcityProgress "Cancelling queued build step $DEP_ID"
    curl --silent --request POST \
        "https://teamcity/app/rest/buildQueue/project:MyProjectName,id:$DEP_ID" \
        --data "&lt;buildCancelRequest comment='Another part of the build chain failed so this sibling step was cancelled.' readdIntoQueue='false' /&gt;" \
        --header "Content-Type: application/xml"
done

for DEP_ID in $RUNNING_DEPENDENT_STEP_IDS
do
    teamcityProgress "Cancelling running build step $DEP_ID"
    curl --silent --request POST \
        "https://teamcity/app/rest/builds/project:MyProjectName,id:$DEP_ID" \
        --data "&lt;buildCancelRequest comment='Another part of the build chain failed so this sibling step was cancelled.' readdIntoQueue='false' /&gt;" \
        --header "Content-Type: application/xml"
done


Not super elegant but it gets the job done and prevents build agents sitting there running build steps for builds which have already failed.