mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
A cancel request does not stop a running step. The runner asks it to end, and a step that ignores the signal keeps going, bounded only by its own timeout-minutes -- 350 for a deploy. Meanwhile the concurrency group holds the newer run on pending until the occupant is terminal, not until it is cancel-requested. Measured on run 32859801382: the cancel was accepted at 15:27:45 and thirteen minutes later ten deploy jobs were still running and the newer run still had not started. The sweep now waits FORCE_CANCEL_AFTER_SECONDS and force-cancels whatever is still not completed. Cleanup steps are skipped for those, which is the price: the artefacts of an already superseded run help nobody. Only entry-cancel-superseded.yml sets the variable -- a closed pull request and a deleted branch have nothing waiting on the group, so they keep cancelling and walking away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
113 lines
3.8 KiB
Bash
Executable File
113 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
: "${PR_NUMBER:?Missing PR_NUMBER}"
|
|
: "${GH_TOKEN:?Missing GH_TOKEN}"
|
|
: "${REPOSITORY:?Missing REPOSITORY}"
|
|
PR_HEAD_REF="${PR_HEAD_REF:-}"
|
|
PR_HEAD_SHA="${PR_HEAD_SHA:-}"
|
|
PR_HEAD_REPOSITORY="${PR_HEAD_REPOSITORY:-}"
|
|
: "${CURRENT_RUN_ID:?Missing CURRENT_RUN_ID}"
|
|
INCLUDE_PATHS="${INCLUDE_PATHS:-}"
|
|
KEEP_NEWEST_PER="${KEEP_NEWEST_PER:-}"
|
|
FORCE_CANCEL_AFTER_SECONDS="${FORCE_CANCEL_AFTER_SECONDS:-}"
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=scripts/github/cancel/lib.sh
|
|
source "${SCRIPT_DIR}/lib.sh"
|
|
|
|
require_tools
|
|
validate_include_paths
|
|
validate_keep_newest_per
|
|
validate_force_cancel_after
|
|
|
|
# A fork PR's runs carry an empty `pull_requests` array, so the branch matcher
|
|
# is all that is left — and it cannot tell two pull requests apart that share a
|
|
# head repository and head branch. Rather than cancel the wrong one, drop the
|
|
# matcher for as long as the head is ambiguous.
|
|
disable_ambiguous_branch_fallback() {
|
|
local rivals
|
|
local -a query
|
|
|
|
if [[ -z "${PR_HEAD_REF}" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
query=(-f state=open -f per_page=100)
|
|
if [[ -n "${PR_HEAD_REPOSITORY}" ]]; then
|
|
query+=(-f "head=${PR_HEAD_REPOSITORY%%/*}:${PR_HEAD_REF}")
|
|
fi
|
|
|
|
if ! rivals="$(
|
|
gh api --paginate \
|
|
-H "Accept: application/vnd.github+json" \
|
|
-X GET "/repos/${REPOSITORY}/pulls" \
|
|
"${query[@]}" |
|
|
jq -s \
|
|
--argjson pr_number "${PR_NUMBER}" \
|
|
--arg head_ref "${PR_HEAD_REF}" \
|
|
'[.[][] | select(.number != $pr_number and (.head.ref // "") == $head_ref)] | length'
|
|
)"; then
|
|
echo "WARNING: could not list open pull requests for the head; keeping the branch fallback" >&2
|
|
return 0
|
|
fi
|
|
|
|
if [[ "${rivals}" != "0" ]]; then
|
|
echo "Branch fallback disabled: ${rivals} other open pull request(s) share ${PR_HEAD_REPOSITORY%%/*}:${PR_HEAD_REF}"
|
|
PR_HEAD_REF=""
|
|
fi
|
|
}
|
|
|
|
echo "Searching active workflow runs for PR #${PR_NUMBER}"
|
|
if [[ -n "${PR_HEAD_SHA}${PR_HEAD_REF}${PR_HEAD_REPOSITORY}" ]]; then
|
|
echo "Fallback matching enabled for head.sha=${PR_HEAD_SHA:-<empty>} head.ref=${PR_HEAD_REF:-<empty>} head.repo=${PR_HEAD_REPOSITORY:-<empty>}"
|
|
fi
|
|
|
|
disable_ambiguous_branch_fallback
|
|
|
|
runs="$(collect_runs)"
|
|
|
|
run_ids="$(
|
|
printf '%s\n' "${runs}" | jq -s -r \
|
|
--argjson pr_number "${PR_NUMBER}" \
|
|
--arg current_run_id "${CURRENT_RUN_ID}" \
|
|
--arg pr_head_ref "${PR_HEAD_REF}" \
|
|
--arg pr_head_sha "${PR_HEAD_SHA}" \
|
|
--arg pr_head_repository "${PR_HEAD_REPOSITORY}" \
|
|
--arg include_paths "${INCLUDE_PATHS}" \
|
|
--arg keep_newest "${KEEP_NEWEST_PER}" '
|
|
def drop_newest: sort_by([(.run_started_at // .created_at), .id]) | .[:-1];
|
|
def allowlist:
|
|
$include_paths | split("\n") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0));
|
|
|
|
allowlist as $paths
|
|
| unique_by(.id)
|
|
| map(
|
|
select($current_run_id == "" or (.id | tostring) != $current_run_id)
|
|
| select($include_paths == "" or ((.path // "") | IN($paths[])))
|
|
| select(.event == "pull_request" or .event == "pull_request_target")
|
|
| select(
|
|
any(.pull_requests[]?; (.number // -1) == $pr_number)
|
|
or ($pr_head_sha != "" and (.head_sha // "") == $pr_head_sha)
|
|
or (
|
|
$pr_head_ref != ""
|
|
and all(.pull_requests[]?; (.number // $pr_number) == $pr_number)
|
|
and (.head_branch // "") == $pr_head_ref
|
|
and (
|
|
$pr_head_repository == ""
|
|
or (.head_repository.full_name // "") == $pr_head_repository
|
|
or (.head_repository.full_name // "") == ""
|
|
)
|
|
)
|
|
)
|
|
)
|
|
| (if $keep_newest == "all" then drop_newest
|
|
elif $keep_newest == "event" then
|
|
group_by(.event // "") | map(drop_newest) | flatten
|
|
else . end)
|
|
| .[].id
|
|
'
|
|
)"
|
|
|
|
cancel_all "${run_ids}"
|