mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 13:17:07 +00:00
fix(ci): cancel superseded runs the group leaves behind
A long orchestrator sweep sometimes survives its concurrency group: the occupant sits on queued while its matrix waits for runners, cancel-in-progress does not reap it, and the newer run parks on pending until someone cancels the sweep by hand. PR #380 sat that way for nine hours behind a run from the previous commit. entry-cancel-superseded.yml cancels those leftovers through the API. The group stays primary and untouched: it reaps within seconds and needs no runner, so the fallback normally arrives to find nothing left to do. It carries no concurrency group of its own, because the run holding a group cannot be the run that frees it. Selection mirrors what the group would have done: cancel every matched run except the newest in the group instance, keyed the way the group is keyed. Not by head SHA, which goes stale while the job waits for a runner and would cancel the newest commit's CI while sparing the stale one. INCLUDE_PATHS keeps the sweep inside the group's own members, so a push to main cannot take the cron workflows with it. deleted_branch_runs.sh becomes branch_runs.sh, since it now also serves live branches, and the shared parts move to lib.sh. A rejected cancel is logged and the remaining runs are still attempted; only a denied token fails the job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
653dc632e4
commit
474aaf7bfe
12 files changed
+1120
-321
No files matched your search
@@ -1,9 +1,33 @@
|
||||
# Run Cancellation 🚫
|
||||
|
||||
Shell helpers that cancel in-progress GitHub Actions workflow runs in response to repository lifecycle events.
|
||||
Shell helpers that cancel GitHub Actions workflow runs whose original trigger is no longer relevant.
|
||||
|
||||
## Scope 📋
|
||||
|
||||
This directory contains helpers invoked from workflows that listen on `pull_request_target: closed`, `pull_request_target: converted_to_draft`, and `delete`. The helpers call the GitHub Actions API to cancel runs whose original trigger is no longer relevant (a closed PR, a deleted branch). Helpers MUST NOT be called from workflows that have not received the corresponding lifecycle event, because cancelling unrelated runs would mask real failures and waste accumulated work.
|
||||
This directory contains helpers invoked from workflows that listen on `pull_request_target`, `push`, and `delete`. The helpers call the GitHub Actions API to cancel runs made obsolete by a closed PR, a deleted branch, or a newer commit on the same PR or branch. Helpers MUST NOT be called from workflows that have not received one of those events, because cancelling unrelated runs would mask real failures and waste accumulated work.
|
||||
|
||||
## Superseding runs 🔁
|
||||
|
||||
On a long orchestrator sweep the concurrency group sometimes fails to reap its occupant: the occupant's run-level status sits on `queued` — the normal state of a run whose matrix jobs are still waiting for runners — and the newer run parks on `pending` behind it until someone cancels the sweep by hand. Whether `queued` is the cause or another symptom cannot be decided from outside GitHub; what is observable is that the group does not always reap. The API cancel does not depend on any of that, so [entry-cancel-superseded.yml](../../../.github/workflows/entry-cancel-superseded.yml) calls these helpers on every new commit as well. That workflow deliberately declares no concurrency group: the run holding a group cannot be the run that frees it.
|
||||
|
||||
The group stays primary. It reaps within seconds of a run being created and needs no runner, while the fallback first has to win a runner slot — from the very pool the superseded sweep is saturating, so it is slowest exactly when it is needed. Measured waits in this repository range from under a minute to about twenty. That is the cost of the fallback, and the reason it is a fallback rather than the mechanism.
|
||||
|
||||
Three variables keep the fallback inside the group's bounds:
|
||||
|
||||
- `KEEP_NEWEST_PER` — keeps the newest run of each group instance alive and cancels only the older ones, which is the rule the concurrency group itself applies. It is what makes the fallback safe against races: the head SHA from the event payload goes stale while the job waits for a runner, and a `[skip ci]` or `GITHUB_TOKEN` push moves the branch tip without creating a run at all. Selecting by "not the current head" would then cancel the newest commit's CI and spare the stale one, or leave the branch with no CI at all. Selecting by "not the newest run" cannot, in any ordering.
|
||||
|
||||
The value must mirror how the backed-up group is keyed. `all` treats every matched run as one instance — correct for `global-ci-<repo>-<ref_name>`, which spans both `entry-push-latest.yml` and `entry-manual-steer.yml`, so a push must be able to reap a manual sweep. `event` keeps one instance per event name — correct for `pr-ci-<repo>-<event_name>-pr-<number>`. Grouping any finer than the real key spares a run the group would have reaped: per-workflow grouping leaves the manual sweep alive, because it is the newest run of its own workflow.
|
||||
- `CURRENT_RUN_ID` — keeps the calling run itself alive.
|
||||
- `INCLUDE_PATHS` — newline-separated workflow paths, the members of the concurrency group being backed up. Without it the helpers would also cancel runs the group never governed, such as the `cron-*` workflows that listen on `push`. Derive the list from the `concurrency:` blocks, not from what looks related: `entry-delete-branch.yml` keys its group on the *deleted* ref, so it is not a member of a push's group, and listing it would make a push to `main` cancel an unrelated branch cleanup. A value that lists no path at all aborts rather than silently matching nothing.
|
||||
|
||||
Leave `KEEP_NEWEST_PER` and `INCLUDE_PATHS` unset to cancel every matching run, as the lifecycle events do — a closed PR and a deleted branch have no run left that deserves to survive. A fallback also mirrors whatever gates its group: `global-ci-*` cancels on every ref except `main`, so the push job carries the same `github.ref_name != 'main'` guard, while `pr-ci-*` is declared `cancel-in-progress: true` unconditionally, so the pull request job is ungated too.
|
||||
|
||||
Every selected run is attempted before anything is reported; abandoning the rest on the first rejection would leave them burning runners. `HTTP 409` means the run already finished and is not a rejection at all. Any other rejection is logged as an `ERROR` line, but only a denied token fails the job — a rate limit or a run that vanished between listing and cancel is transient, and a red check for those is a red check nobody reads.
|
||||
|
||||
"Newest" means the run that entered the group last, so the helpers sort on `run_started_at` and fall back to `created_at`. A re-run keeps the `created_at` of its first attempt while re-entering the group as the newest member, and sorting on `created_at` would cancel it as the oldest.
|
||||
|
||||
Two open pull requests that share a head repository *and* a head branch, to different bases, cannot be told apart from the run payload: the runs of a fork PR carry an empty `pull_requests` array, the same `head_branch` and the same `head_sha`. The helper therefore asks the API whether any other open pull request shares that head, and drops the branch matcher while one does — cancelling nothing beats cancelling the wrong pull request. If that lookup itself fails the matcher stays on, because a failed lookup is far more likely than a shared head.
|
||||
|
||||
The branch-name matcher normally also requires the run's head repository to be the pull request's own. Two cases relax that: a run whose `head_repository` the API omits, and a pull request whose head fork has been deleted, where the event payload carries no head repository either. Both are matched on the branch name alone, because otherwise their runs would keep going forever — and neither field is something a contributor can influence. The ambiguity check above is what makes that safe: it runs whenever a head branch is known, with or without a repository, so a shared branch name still disables the matcher rather than cancelling the wrong pull request.
|
||||
|
||||
For the workflow catalog that drives these calls see [workflows.md](../../../docs/contributing/tools/github/actions/workflows.md).
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BRANCH:?Missing BRANCH}"
|
||||
: "${GH_TOKEN:?Missing GH_TOKEN}"
|
||||
: "${REPOSITORY:?Missing REPOSITORY}"
|
||||
CURRENT_RUN_ID="${CURRENT_RUN_ID:-}"
|
||||
INCLUDE_PATHS="${INCLUDE_PATHS:-}"
|
||||
KEEP_NEWEST_PER="${KEEP_NEWEST_PER:-}"
|
||||
|
||||
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
|
||||
|
||||
echo "Searching active workflow runs for branch: ${BRANCH}"
|
||||
|
||||
runs="$(collect_runs)"
|
||||
|
||||
run_ids="$(
|
||||
printf '%s\n' "${runs}" | jq -s -r \
|
||||
--arg branch "${BRANCH}" \
|
||||
--arg current_run_id "${CURRENT_RUN_ID}" \
|
||||
--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(.path != ".github/workflows/entry-pr-closed-cancel-workflows.yml")
|
||||
| select($current_run_id == "" or (.id | tostring) != $current_run_id)
|
||||
| select($include_paths == "" or ((.path // "") | IN($paths[])))
|
||||
| select(
|
||||
.head_branch == $branch
|
||||
or any(.pull_requests[]?; (.head.ref // "") == $branch)
|
||||
)
|
||||
)
|
||||
| (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}"
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BRANCH:?Missing BRANCH}"
|
||||
: "${GH_TOKEN:?Missing GH_TOKEN}"
|
||||
: "${REPOSITORY:?Missing REPOSITORY}"
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "ERROR: gh CLI not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "ERROR: jq not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Searching active workflow runs for deleted branch: ${BRANCH}"
|
||||
|
||||
cancel_runs_by_status() {
|
||||
local status="$1"
|
||||
local run_ids
|
||||
|
||||
run_ids="$(
|
||||
gh api --paginate \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPOSITORY}/actions/runs?status=${status}&per_page=100" |
|
||||
jq -r --arg branch "${BRANCH}" '
|
||||
.workflow_runs[]
|
||||
| select(.path != ".github/workflows/entry-pr-closed-cancel-workflows.yml")
|
||||
| select(
|
||||
.head_branch == $branch
|
||||
or any(.pull_requests[]?; (.head.ref // "") == $branch)
|
||||
)
|
||||
| .id
|
||||
' |
|
||||
sort -u
|
||||
)"
|
||||
|
||||
if [[ -z "${run_ids}" ]]; then
|
||||
echo "No ${status} runs found for ${BRANCH}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while read -r run_id; do
|
||||
[[ -n "${run_id}" ]] || continue
|
||||
echo "Cancelling ${status} run ${run_id}"
|
||||
if ! gh api \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPOSITORY}/actions/runs/${run_id}/cancel" \
|
||||
>/dev/null; then
|
||||
echo "Run ${run_id} could not be cancelled, likely because it already completed"
|
||||
fi
|
||||
done <<<"${run_ids}"
|
||||
}
|
||||
|
||||
cancel_runs_by_status requested
|
||||
cancel_runs_by_status pending
|
||||
cancel_runs_by_status waiting
|
||||
cancel_runs_by_status queued
|
||||
cancel_runs_by_status in_progress
|
||||
Executable
+129
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared by branch_runs.sh and pull_request_runs.sh. Expects REPOSITORY,
|
||||
# GH_TOKEN and INCLUDE_PATHS to be set by the caller.
|
||||
|
||||
RUN_STATUSES=(requested pending waiting queued in_progress)
|
||||
|
||||
require_tools() {
|
||||
local tool
|
||||
|
||||
for tool in gh jq; do
|
||||
if ! command -v "${tool}" >/dev/null 2>&1; then
|
||||
echo "ERROR: ${tool} not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# A non-empty INCLUDE_PATHS that trims to nothing would silently select no run
|
||||
# at all, turning the caller into a no-op that still reports success.
|
||||
validate_include_paths() {
|
||||
if [[ -z "${INCLUDE_PATHS}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$(printf '%s' "${INCLUDE_PATHS}" | tr -d '[:space:]')" ]]; then
|
||||
echo "ERROR: INCLUDE_PATHS is set but lists no workflow path." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Restricted to workflows:"
|
||||
printf '%s\n' "${INCLUDE_PATHS}" | sed '/^[[:space:]]*$/d;s/^/ /'
|
||||
}
|
||||
|
||||
# Mirrors the key of the concurrency group being backed up: `all` for a group
|
||||
# that spans every event on a ref, `event` for one whose key contains the event
|
||||
# name. Grouping any finer keeps a run alive that the group would have reaped.
|
||||
validate_keep_newest_per() {
|
||||
case "${KEEP_NEWEST_PER}" in
|
||||
"" | all | event) ;;
|
||||
*)
|
||||
echo "ERROR: KEEP_NEWEST_PER must be empty, 'all' or 'event'." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Assign the result with `runs="$(collect_runs)"`, never pipe this function
|
||||
# directly: on the left of a pipeline bash drops its exit status, and a failed
|
||||
# page would silently narrow the run set instead of aborting.
|
||||
collect_runs() {
|
||||
local status
|
||||
local payload
|
||||
|
||||
for status in "${RUN_STATUSES[@]}"; do
|
||||
payload="$(
|
||||
gh api --paginate \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPOSITORY}/actions/runs?status=${status}&per_page=100"
|
||||
)" || return 1
|
||||
printf '%s' "${payload}" | jq -c '.workflow_runs[]' || return 1
|
||||
done
|
||||
}
|
||||
|
||||
cancel_run() {
|
||||
local run_id="$1"
|
||||
local response
|
||||
|
||||
if response="$(
|
||||
gh api \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPOSITORY}/actions/runs/${run_id}/cancel" 2>&1
|
||||
)"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${response}" == *"HTTP 409"* ]]; then
|
||||
echo "Run ${run_id} was already completed or already cancelling"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${response}" == *"Resource not accessible"* ]]; then
|
||||
CANCEL_PERMISSION_DENIED=1
|
||||
fi
|
||||
|
||||
echo "ERROR: cancelling run ${run_id} failed: ${response}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# One transient failure must not abandon the remaining runs: they would keep
|
||||
# burning runners. Cancel everything, then report. Only a denied token is
|
||||
# fatal — a rate limit or a run that vanished between listing and cancel is
|
||||
# transient, and painting the cleanup red for those trains everyone to ignore
|
||||
# the red. Counting rejections instead would fail a one-run sweep on a single
|
||||
# transient, which is the common shape for a branch delete.
|
||||
cancel_all() {
|
||||
local run_ids="$1"
|
||||
local run_id
|
||||
local failures=0
|
||||
local successes=0
|
||||
|
||||
CANCEL_PERMISSION_DENIED=0
|
||||
|
||||
if [[ -z "${run_ids}" ]]; then
|
||||
echo "No superseded runs found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while read -r run_id; do
|
||||
[[ -n "${run_id}" ]] || continue
|
||||
echo "Cancelling run ${run_id}"
|
||||
if cancel_run "${run_id}"; then
|
||||
successes=$((successes + 1))
|
||||
else
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
done <<<"${run_ids}"
|
||||
|
||||
if ((failures == 0)); then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ((CANCEL_PERMISSION_DENIED)); then
|
||||
echo "ERROR: the token may not cancel runs; ${failures} of $((failures + successes)) rejected." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "WARNING: ${failures} of $((failures + successes)) run(s) could not be cancelled." >&2
|
||||
}
|
||||
@@ -4,81 +4,107 @@ set -euo pipefail
|
||||
: "${PR_NUMBER:?Missing PR_NUMBER}"
|
||||
: "${GH_TOKEN:?Missing GH_TOKEN}"
|
||||
: "${REPOSITORY:?Missing REPOSITORY}"
|
||||
: "${CURRENT_RUN_ID:?Missing CURRENT_RUN_ID}"
|
||||
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:-}"
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "ERROR: gh CLI not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/github/cancel/lib.sh
|
||||
source "${SCRIPT_DIR}/lib.sh"
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "ERROR: jq not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
require_tools
|
||||
validate_include_paths
|
||||
validate_keep_newest_per
|
||||
|
||||
# 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
|
||||
|
||||
cancel_runs_by_status() {
|
||||
local status="$1"
|
||||
local run_ids
|
||||
disable_ambiguous_branch_fallback
|
||||
|
||||
run_ids="$(
|
||||
gh api --paginate \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPOSITORY}/actions/runs?status=${status}&per_page=100" |
|
||||
jq -r \
|
||||
--argjson pr_number "${PR_NUMBER}" \
|
||||
--argjson 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}" '
|
||||
.workflow_runs[]
|
||||
| select(.id != $current_run_id)
|
||||
| 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 (.head_branch // "") == $pr_head_ref
|
||||
and (
|
||||
$pr_head_repository == ""
|
||||
or (.head_repository.full_name // "") == $pr_head_repository
|
||||
or (.head_repository.full_name // "") == ""
|
||||
)
|
||||
)
|
||||
)
|
||||
| .id
|
||||
' |
|
||||
sort -u
|
||||
)"
|
||||
runs="$(collect_runs)"
|
||||
|
||||
if [[ -z "${run_ids}" ]]; then
|
||||
echo "No ${status} runs found for PR #${PR_NUMBER}"
|
||||
return 0
|
||||
fi
|
||||
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));
|
||||
|
||||
while read -r run_id; do
|
||||
[[ -n "${run_id}" ]] || continue
|
||||
echo "Cancelling ${status} run ${run_id}"
|
||||
if ! gh api \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPOSITORY}/actions/runs/${run_id}/cancel" \
|
||||
>/dev/null; then
|
||||
echo "Run ${run_id} could not be cancelled, likely because it already completed"
|
||||
fi
|
||||
done <<<"${run_ids}"
|
||||
}
|
||||
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_runs_by_status requested
|
||||
cancel_runs_by_status pending
|
||||
cancel_runs_by_status waiting
|
||||
cancel_runs_by_status queued
|
||||
cancel_runs_by_status in_progress
|
||||
cancel_all "${run_ids}"
|
||||
Reference in new issue
Block a user