Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dca874b
fix(scheduler): resolve live refs before cancelling runs
seonghobae Aug 26, 2026
7348cf6
fix(actions): fail closed on malformed PR heads
seonghobae Aug 26, 2026
50ceb75
Merge branch 'main' into fix/queue-hygiene-live-ref-race
seonghobae Aug 28, 2026
5025350
Merge branch 'main' into fix/queue-hygiene-live-ref-race
seonghobae Aug 28, 2026
7c69378
fix(scheduler): bound live ref lookups
seonghobae Aug 28, 2026
b99f7f1
fix(scheduler): deduplicate live ref lookups
seonghobae Aug 28, 2026
6fb6303
Merge branch 'main' into fix/queue-hygiene-live-ref-race
seonghobae Aug 28, 2026
25aac56
Merge protected main into fix/queue-hygiene-live-ref-race
seonghobae Aug 28, 2026
10ece3e
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 28, 2026
b61c4c8
Merge branch 'main' into fix/queue-hygiene-live-ref-race
claude Aug 30, 2026
1eebe1b
Merge remote-tracking branch 'origin/main' into test-fix/queue-hygien…
claude Aug 30, 2026
af519b7
Merge remote-tracking branch 'origin/main' into fix/queue-hygiene-liv…
claude Aug 30, 2026
03f87fa
ci(repair): revalidate queue cancellation candidates
seonghobae Sep 1, 2026
5cfc2ff
ci(repair): trigger PR 1348 final revalidation
seonghobae Sep 1, 2026
4651a12
fix(scheduler): add fail-closed final queue revalidation
seonghobae Sep 1, 2026
c2c2324
test(scheduler): execute final queue revalidation races
seonghobae Sep 1, 2026
d820181
ci(repair): make PR 1348 repair driver executable
seonghobae Sep 1, 2026
59f374e
test(scheduler): expose aged-orphan cancellation regression
seonghobae Sep 1, 2026
4aa4b15
fix(scheduler): preserve aged-orphan cleanup in final revalidation
seonghobae Sep 1, 2026
7483507
chore(ci): remove obsolete PR 1348 repair workflow
seonghobae Sep 1, 2026
64eef29
chore(ci): remove obsolete PR 1348 repair trigger
seonghobae Sep 1, 2026
e417a0c
test(queue): cover late open-PR association before cancellation
seonghobae Sep 1, 2026
c8b086c
fix(queue): revalidate late PR association before cancellation
seonghobae Sep 1, 2026
0f390a8
ci(repair): wire final live-state queue revalidation
seonghobae Sep 1, 2026
0d7b7a1
test(queue): reproduce stale orphan PR snapshot cancellation
seonghobae Sep 1, 2026
0338b17
fix(queue): revalidate live ref for unassociated PR runs
seonghobae Sep 1, 2026
db86866
chore(queue): remove completed one-shot repair writer
seonghobae Sep 1, 2026
79d4461
fix(scheduler): reconcile live-ref cancellation guard
seonghobae Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 49 additions & 33 deletions .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1087,37 +1087,40 @@ jobs:
fi
fi

# Queue hygiene, part 1: cancel every queued/in-progress PR run whose
# head SHA no longer matches its open PR's Current HEAD, plus default-
# branch push/schedule runs superseded by a newer default HEAD. PR
# concurrency normally does this on synchronize/close events, but it
# cannot repair runs left behind by an outage or a manual dispatch.
# Compare live refs on every sweep instead of waiting for an age
# threshold: previous-head checks are never useful merge evidence.
# Queue hygiene, part 1: classify queued/in-progress runs against a
# bounded PR/default-branch snapshot. The snapshot is intentionally
# cheap and may race with a subsequent head move; every destructive
# cancellation is therefore revalidated against live run/PR/ref state
# immediately before the mutation by the production helper below.
queue_hygiene_ready=true
if ! open_pr_heads_json="$(
open_pr_heads_json="{}"
if open_pr_payload_json="$(
gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${repo_full_name}/pulls?state=open&per_page=100" \
--paginate \
| jq -sc '
add
| map(
select(
.head.repo.full_name != null and
.head.ref != null and
.head.sha != null
)
| {
key: "\(.head.repo.full_name):\(.head.ref)",
value: .head.sha
}
)
| from_entries
'
| jq -sc '[.[] | .[]]'
)"; then
if ! jq -e '
all(.[];
(.head.repo.full_name | type) == "string" and (.head.repo.full_name | length) > 0 and
(.head.ref | type) == "string" and (.head.ref | length) > 0 and
(.head.sha | type) == "string" and (.head.sha | test("^[0-9a-fA-F]{40}$"))
)
' <<<"$open_pr_payload_json" >/dev/null; then
echo "::warning::Current-HEAD cancellation skipped for ${repo_full_name}: an open PR has malformed head repository/ref/SHA metadata. No run will be cancelled from incomplete evidence."
queue_hygiene_ready=false
else
open_pr_heads_json="$(
jq -c '
reduce .[] as $pr ({};
. + {(($pr.head.repo.full_name + ":" + $pr.head.ref)): $pr.head.sha}
)
' <<<"$open_pr_payload_json"
)"
fi
else
echo "::warning::Current-HEAD cancellation skipped for ${repo_full_name}: open PR head refs could not be read safely. No run will be cancelled from incomplete evidence."
open_pr_heads_json="{}"
queue_hygiene_ready=false
fi
if ! current_default_sha="$(
Expand All @@ -1129,6 +1132,10 @@ jobs:
echo "::warning::Current-HEAD cancellation skipped for ${repo_full_name}: default-branch HEAD could not be read safely. No run will be cancelled from incomplete evidence."
current_default_sha=""
queue_hygiene_ready=false
elif ! [[ "$current_default_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::warning::Current-HEAD cancellation skipped for ${repo_full_name}: default-branch HEAD is malformed. No run will be cancelled from incomplete evidence."
current_default_sha=""
queue_hygiene_ready=false
fi
if ! active_runs_json="$(
for active_status in queued in_progress; do
Expand Down Expand Up @@ -1187,13 +1194,17 @@ jobs:
fi
superseded_count="$(jq 'length' <<<"$superseded_runs_json")"
if [ "$superseded_count" -gt 0 ]; then
echo "Cancelling ${superseded_count} queued/in-progress run(s) that do not match an open PR or default-branch Current HEAD:"
jq -r '.[] | " run \(.id) [\(.name)] status=\(.status) event=\(.event) branch=\(.head_branch) run_head=\(.run_head) current_head=\(.current_head // "closed-or-no-open-pr")"' <<<"$superseded_runs_json"
echo "Revalidating ${superseded_count} queued/in-progress run(s) classified as not matching an open PR or default-branch Current HEAD:"
jq -r '.[] | " run \(.id) [\(.name)] status=\(.status) event=\(.event) branch=\(.head_branch) run_head=\(.run_head) classified_head=\(.current_head // "closed-or-no-open-pr")"' <<<"$superseded_runs_json"
if [ "$DRY_RUN" != "true" ]; then
while IFS= read -r run_id; do
if ! gh api -X POST "/repos/${repo_full_name}/actions/runs/${run_id}/cancel" >/dev/null; then
echo "Could not cancel superseded run ${run_id} in ${repo_full_name}; it may have finished already."
fi
scripts/ci/revalidate_queue_cancellation.sh \
"$repo_full_name" \
"$run_id" \
"$default_branch" \
"$current_default_sha" \
"$open_pr_heads_json" \
"superseded"
done < <(jq -r '.[].id' <<<"$superseded_runs_json")
fi
fi
Expand All @@ -1202,6 +1213,7 @@ jobs:
# runs that are not tied to a currently open PR head. This catches
# orphaned manual/workflow-chain runs without cancelling a valid
# current-head PR check merely because runner capacity was scarce.
# The helper re-checks late PR association/live refs before mutation.
stale_runs_json="[]"
if [ "$queue_hygiene_ready" = "true" ]; then
stale_cutoff="$(date -u -d "${ORG_SWEEP_STALE_QUEUE_HOURS} hours ago" +%Y-%m-%dT%H:%M:%SZ)"
Expand All @@ -1224,13 +1236,17 @@ jobs:
fi
stale_count="$(jq 'length' <<<"$stale_runs_json")"
if [ "$stale_count" -gt 0 ]; then
echo "Cancelling ${stale_count} queued run(s) older than ${ORG_SWEEP_STALE_QUEUE_HOURS}h:"
echo "Revalidating ${stale_count} queued run(s) older than ${ORG_SWEEP_STALE_QUEUE_HOURS}h:"
jq -r '.[] | " run \(.id) [\(.name)] on \(.head_branch) queued since \(.created_at)"' <<<"$stale_runs_json"
if [ "$DRY_RUN" != "true" ]; then
while IFS= read -r run_id; do
if ! gh api -X POST "/repos/${repo_full_name}/actions/runs/${run_id}/cancel" >/dev/null; then
echo "Could not cancel run ${run_id} in ${repo_full_name}; it may have started or finished already."
fi
scripts/ci/revalidate_queue_cancellation.sh \
"$repo_full_name" \
"$run_id" \
"$default_branch" \
"$current_default_sha" \
"$open_pr_heads_json" \
"aged-orphan"
done < <(jq -r '.[].id' <<<"$stale_runs_json")
fi
fi
Expand Down
25 changes: 25 additions & 0 deletions docs/doctoring/queue-hygiene-live-ref-race.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Queue-hygiene live-ref race doctoring

## Incident

The organization queue sweep classified queued/in-progress Actions runs against a pull-request list snapshot and later cancelled the selected run IDs. A PR head can advance after that snapshot but before the destructive cancellation. GitHub's run and PR payloads may also lag the branch ref. Trusting either predecessor snapshot as final authority can therefore cancel the sole current-head review/check evidence and amplify Actions-capacity saturation.

## Owner and boundary

`ContextualWisdomLab/.github` owns this defect because the destructive organization queue hygiene and required review/merge scheduler are central control-plane behavior. Leaf repositories must not duplicate cancellation policy. The scheduler may use cheap PR payloads to classify candidates, but every destructive cancellation must revalidate the live run and its authoritative current ref immediately before the mutation.

## Contract

The repaired scheduler keeps a bounded initial snapshot and delegates every selected cancellation to `scripts/ci/revalidate_queue_cancellation.sh`. The helper fails closed when run/PR/ref evidence cannot be read or is malformed. For an attached PR it re-fetches the PR and resolves the head branch through the Git ref endpoint. For an Actions PR run whose `pull_requests` association is still empty, it re-fetches open PRs only to discover a matching head repository/ref and then resolves that branch ref; the payload SHA is explicitly non-authoritative. If the live ref equals the run head, the run is preserved. Default-branch push/schedule candidates are similarly revalidated against the live protected-branch head.

The final design intentionally removes the earlier serial live-ref lookup for every open PR and its repository-wide lookup ceiling. Live-ref traffic is proportional to destructive candidates, so a large open-PR queue cannot disable all cleanup merely by exceeding a fanout cap.

## Reconciliation and one-shot retirement

PR #1348 diverged while protected `main` advanced. The reconciliation tree is based on the live protected-main tree and preserves the later scheduler fixes: hourly organization sweep cadence, explicit Ubuntu 24.04 queue-draining runners, and review-event dispatch after thread updates. The obsolete `_temp_pr1348_final_revalidation_repair.yml` source-fix workflow is not carried forward. The production helper is executable in the Git tree and is covered by focused executable regressions, including the stale-PR-payload/live-ref race.

## Evidence

`tests/test_queue_cancellation_revalidation.py` covers post-classification head movement, current-head preservation, fail-closed API/ref failures, predecessor cancellation, and aged-orphan behavior. `tests/test_queue_cancellation_open_pr_revalidation.py` specifically proves that a stale open-PR payload SHA cannot authorize cancellation when the authoritative live branch ref still points at the queued run. `tests/test_queue_cancellation_scheduler_contract.py` proves the scheduler routes both cancellation modes through the helper, removes serial upfront ref fanout and the lookup ceiling, preserves current-main scheduler fixes, keeps the helper executable, and retires the temporary writer workflow.

Hosted exact-head CI, security, coverage and review evidence remain authoritative before merge; this doctoring note does not substitute for those gates.
178 changes: 178 additions & 0 deletions scripts/ci/revalidate_queue_cancellation.sh
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 6 ]; then
echo "usage: $0 <repo> <run-id> <default-branch> <classified-default-sha> <classified-open-pr-heads-json> <superseded|aged-orphan>" >&2
exit 2
fi

repo_full_name="$1"
run_id="$2"
default_branch="$3"
classified_default_sha="$4"
classified_open_pr_heads_json="$5"
cancellation_mode="$6"

case "$cancellation_mode" in
superseded|aged-orphan) ;;
*)
echo "invalid cancellation mode: ${cancellation_mode}" >&2
exit 2
;;
esac

warn_preserve() {
echo "::warning::Preserving run ${run_id} in ${repo_full_name}: $1"
exit 0
}

encode_ref_path() {
jq -rn --arg value "$1" '$value | split("/") | map(@uri) | join("/")'
}

if ! run_json="$(gh api -H "Accept: application/vnd.github+json" "/repos/${repo_full_name}/actions/runs/${run_id}")"; then
warn_preserve "live run metadata could not be re-fetched before cancellation."
fi

event="$(jq -r '.event // empty' <<<"$run_json")"
status="$(jq -r '.status // empty' <<<"$run_json")"
run_head="$(jq -r '.head_sha // empty' <<<"$run_json")"
run_branch="$(jq -r '.head_branch // empty' <<<"$run_json")"
run_head_repo="$(jq -r '.head_repository.full_name // empty' <<<"$run_json")"
if ! [[ "$run_head" =~ ^[0-9a-fA-F]{40}$ ]]; then
warn_preserve "live run head is malformed."
fi

if [ "$cancellation_mode" = "aged-orphan" ]; then
if [ "$status" != "queued" ]; then
warn_preserve "aged-orphan candidate is no longer queued (status=${status:-<missing>})."
fi
elif [ "$status" != "queued" ] && [ "$status" != "in_progress" ]; then
warn_preserve "superseded candidate is no longer queued or in progress (status=${status:-<missing>})."
fi

case "$event" in
pull_request|pull_request_target)
pr_number="$(jq -r '.pull_requests[0].number // empty' <<<"$run_json")"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Secondary pull requests lose current checks

For runs associated with several pull requests, pull_requests[0] validates only the first. Another open pull request can lose its current-head check.

Prompt for agents
Update scripts/ci/revalidate_queue_cancellation.sh to evaluate every pull request listed in the live workflow-run metadata, not only pull_requests[0]. Before cancellation, preserve the run if any associated open PR resolves to the run head. Fail closed if any relevant PR or head ref cannot be read or validated. Add regression coverage for a run associated with multiple PRs where the first is closed or superseded and a later open PR has the run's authoritative current head.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

if ! [[ "$pr_number" =~ ^[1-9][0-9]*$ ]]; then
if [ "$cancellation_mode" = "aged-orphan" ]; then
# Association metadata on an Actions run can lag the PR itself. Re-read
# open PRs immediately before destructive cancellation, but use that
# payload only to discover the authoritative head repository/ref. The
# payload SHA itself can be stale, so resolve a matching branch through
# the Git reference endpoint before deciding whether the run is current.
if [ -z "$run_head_repo" ] || [ -z "$run_branch" ]; then
warn_preserve "unassociated PR run has no authoritative head repository/ref."
fi
if ! fresh_open_pr_refs_json="$(
gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${repo_full_name}/pulls?state=open&per_page=100" \
--paginate \
| jq -sc '[.[] | .[] | {
repo: (.head.repo.full_name // null),
ref: (.head.ref // null)
}]'
)"; then
warn_preserve "open PR heads could not be re-fetched for an unassociated PR run."
fi
if ! jq -e '
all(.[];
(.repo | type) == "string" and (.repo | length) > 0 and
(.ref | type) == "string" and (.ref | length) > 0
)
' <<<"$fresh_open_pr_refs_json" >/dev/null; then
warn_preserve "fresh open PR head evidence is malformed."
fi
if jq -e \
--arg repo "$run_head_repo" \
--arg ref "$run_branch" \
'any(.[]; .repo == $repo and .ref == $ref)' \
<<<"$fresh_open_pr_refs_json" >/dev/null; then
encoded_run_ref="$(encode_ref_path "$run_branch")"
if ! final_ref_sha="$(
gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${run_head_repo}/git/ref/heads/${encoded_run_ref}" \
--jq '.object.sha // empty'
)"; then
warn_preserve "live ref for newly associated PR head could not be re-fetched before cancellation."
fi
if ! [[ "$final_ref_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
warn_preserve "live ref for newly associated PR head is malformed."
fi
if [ "$run_head" = "$final_ref_sha" ]; then
warn_preserve "run became associated with an open PR at its authoritative current head after queue classification."
fi
Comment on lines +67 to +106

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Unassociated runs avoid stale payloads

Late association matches repository and branch identity, then resolves the Git ref. A stale pull-request list SHA cannot authorize cancellation.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

fi
else
warn_preserve "no authoritative PR identity is attached to the live run."
fi
else
if ! pr_json="$(gh api -H "Accept: application/vnd.github+json" "/repos/${repo_full_name}/pulls/${pr_number}")"; then
warn_preserve "live PR ${pr_number} could not be re-fetched before cancellation."
fi
live_state="$(jq -r '.state // empty' <<<"$pr_json")"
if [ "$live_state" = "open" ]; then
live_head_repo="$(jq -r '.head.repo.full_name // empty' <<<"$pr_json")"
live_head_ref="$(jq -r '.head.ref // empty' <<<"$pr_json")"
live_head_sha="$(jq -r '.head.sha // empty' <<<"$pr_json")"
if [ -z "$live_head_repo" ] || [ -z "$live_head_ref" ] || ! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
warn_preserve "live PR ${pr_number} head metadata is malformed."
fi
encoded_head_ref="$(encode_ref_path "$live_head_ref")"
if ! final_ref_sha="$(gh api -H "Accept: application/vnd.github+json" "/repos/${live_head_repo}/git/ref/heads/${encoded_head_ref}" --jq '.object.sha // empty')"; then
warn_preserve "live ref for PR ${pr_number} could not be re-fetched before cancellation."
fi
if ! [[ "$final_ref_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
warn_preserve "live ref for PR ${pr_number} is malformed."
fi
classified_sha="$(jq -r --arg key "${live_head_repo}:${live_head_ref}" '.[$key] // empty' <<<"$classified_open_pr_heads_json")"
if ! [[ "$classified_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
warn_preserve "the classification snapshot has no valid head for PR ${pr_number}."
fi
if [ "$live_head_sha" != "$classified_sha" ] || [ "$final_ref_sha" != "$classified_sha" ]; then
warn_preserve "PR ${pr_number} moved after queue classification."
fi
Comment on lines +134 to +136

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Head movement defers cleanup safely

Any post-classification head mismatch preserves the candidate. A genuinely obsolete run can survive one sweep, avoiding cancellation from inconsistent intermediate evidence.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

if [ "$run_head" = "$final_ref_sha" ]; then
echo "Preserving run ${run_id} in ${repo_full_name}: authoritative current-head evidence for PR ${pr_number}."
exit 0
fi
elif [ "$live_state" != "closed" ]; then
warn_preserve "live PR ${pr_number} state is malformed."
fi
# A closed PR cannot supply current merge evidence. If the run is still
# active and was selected from the trusted snapshot, closure remains an
# authoritative reason to retire it.
fi
;;
push|schedule)
if [ "$run_branch" = "$default_branch" ] || [ "$cancellation_mode" = "superseded" ]; then
if ! live_default_sha="$(gh api -H "Accept: application/vnd.github+json" "/repos/${repo_full_name}/commits/${default_branch}" --jq '.sha // empty')"; then
warn_preserve "live default-branch HEAD could not be re-fetched before cancellation."
fi
if ! [[ "$live_default_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
warn_preserve "live default-branch HEAD is malformed."
fi
if [ "$live_default_sha" != "$classified_default_sha" ]; then
warn_preserve "default branch moved after queue classification."
fi
if [ "$run_head" = "$live_default_sha" ]; then
echo "Preserving run ${run_id} in ${repo_full_name}: authoritative current default-branch evidence."
exit 0
fi
fi
;;
*)
if [ "$cancellation_mode" = "superseded" ]; then
warn_preserve "event ${event:-<missing>} is outside the authoritative superseded-run contract."
fi
# Aged-orphan mode intentionally retains the legacy cleanup contract for
# workflow_dispatch, workflow_run, repository_dispatch, and other queued
# events that the trusted initial snapshot proved were not current PR heads.
;;
esac

if ! gh api -X POST "/repos/${repo_full_name}/actions/runs/${run_id}/cancel" >/dev/null; then
echo "Could not cancel ${cancellation_mode} run ${run_id} in ${repo_full_name}; it may have started or finished already."
fi
Comment on lines +176 to +178

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Cancellation remains best effort

A failed cancellation returns success after logging. The next hourly sweep can retry the still-active candidate without failing unrelated repository processing.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Loading
Loading