Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
846cee7
fix(scheduler): do not cancel same-SHA scan on review
seonghobae Sep 19, 2026
17be694
test(scheduler): require pending exact-head queue preservation
seonghobae Sep 19, 2026
ce3a4d9
fix(scheduler): preserve pending exact-head scans
seonghobae Sep 19, 2026
be51220
docs(gap): record scheduler pending preservation authority
seonghobae Sep 19, 2026
6784c19
test(scheduler): pin same-SHA and new-head scan preservation
seonghobae Sep 19, 2026
e2647c5
test(scheduler): require exact-head admission and stale-run retirement
seonghobae Sep 19, 2026
739a34c
fix(scheduler): retire revalidated predecessor head runs
seonghobae Sep 19, 2026
648674c
test(scheduler): bind cleanup to pull request head authority
seonghobae Sep 19, 2026
406c717
fix(scheduler): compare associated pull request head
seonghobae Sep 19, 2026
873a35f
merge: stack scheduler repair on canonical OpenCode admission owner
seonghobae Sep 19, 2026
10dc596
docs(gap): record stacked exact-head scheduler repair
seonghobae Sep 19, 2026
8067fb6
fix(scheduler): verify superseded-run cancellation
seonghobae Sep 19, 2026
c39f57d
docs(gap): bind scheduler cancellation proof
seonghobae Sep 19, 2026
2a81604
test(scheduler): reject stale targeted-dispatch head
seonghobae Sep 19, 2026
4767b64
fix(scheduler): bind targeted dispatch to live head
seonghobae Sep 19, 2026
95fa1d6
docs(gap): bind targeted-dispatch head proof
seonghobae Sep 19, 2026
021ffb2
test(scheduler): align layered concurrency oracles
seonghobae Sep 19, 2026
e2b699a
chore(stack): adopt current OpenCode admission parent
seonghobae Sep 20, 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
153 changes: 149 additions & 4 deletions .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,17 @@ concurrency:
github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.pr_number != '' && format('target-{0}-pr-{1}', github.event.client_payload.target_repository, github.event.client_payload.pr_number) ||
github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && format('pr-{0}', github.event.client_payload.pr_number) ||
github.event_name == 'repository_dispatch' && format('repo-dispatch-{0}', github.repository) ||
github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}
github.ref }}-${{
github.event_name == 'pull_request_target' && github.event.action == 'closed' && format('head-{0}-closed', github.event.pull_request.head.sha) ||
github.event_name == 'pull_request_target' && format('head-{0}', github.event.pull_request.head.sha) ||
github.event_name == 'pull_request_review' && format('head-{0}', github.event.pull_request.head.sha) ||
github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' && format('head-{0}', github.event.client_payload.pr_head_sha) ||
'no-head' }}
# GitHub's native FIFO queue preserves duplicate admissions for one exact
# head. A successor head receives a distinct group, then the bounded cleanup
# job below retires only revalidated predecessor runs. Close uses a distinct
# suffix so it can retire an active scan for the same final head.
queue: max

# Scorecard Token-Permissions (alert #9): declare a least-privilege default at
# the workflow level. The scan-pr-queue job that actually needs write access
Expand All @@ -98,6 +107,136 @@ permissions:
contents: read

jobs:
cancel-superseded-pr-runs:
if: >-
github.event_name == 'pull_request_target' &&
(
github.event.action == 'synchronize' ||
github.event.action == 'closed'
)
runs-on: ubuntu-24.04
permissions:
actions: write
contents: read
pull-requests: read
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPOSITORY: ${{ github.repository }}
TARGET_PR_NUMBER: ${{ github.event.pull_request.number }}
TARGET_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
TARGET_ACTION: ${{ github.event.action }}
steps:
- name: Cancel revalidated predecessor scheduler runs
shell: bash
run: |
set -euo pipefail

if ! [[ "$TARGET_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]] ||
! [[ "$TARGET_PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Superseded-run cleanup rejected malformed target identity."
exit 1
fi

live_target_matches() {
local live_pull live_repository live_number live_state live_head_sha
live_pull="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}")"
live_repository="$(jq -r '.base.repo.full_name // empty' <<<"$live_pull")"
live_number="$(jq -r '.number // 0' <<<"$live_pull")"
live_state="$(jq -r '.state // empty' <<<"$live_pull")"
live_head_sha="$(jq -r '.head.sha // empty' <<<"$live_pull")"
[ "$live_repository" = "$TARGET_REPOSITORY" ] &&
[ "$live_number" = "$TARGET_PR_NUMBER" ] &&
[ "$live_head_sha" = "$TARGET_PR_HEAD_SHA" ] && {
[ "$TARGET_ACTION:$live_state" = "synchronize:open" ] ||
[ "$TARGET_ACTION:$live_state" = "closed:closed" ]
}
}

if ! live_target_matches; then
echo "Superseded-run cleanup skipped because the event no longer matches the live pull request."
exit 0
fi

workflow_runs="$(
gh api --paginate --slurp \
"repos/${TARGET_REPOSITORY}/actions/workflows/pr-review-merge-scheduler.yml/runs?event=pull_request_target&per_page=100"
)"
mapfile -t superseded_run_ids < <(
jq -r \
--argjson pr_number "$TARGET_PR_NUMBER" \
--argjson current_run_id "$GITHUB_RUN_ID" \
--arg target_head "$TARGET_PR_HEAD_SHA" \
--arg target_action "$TARGET_ACTION" \
'
.[] | .workflow_runs[]?
| select(.id != $current_run_id)
| select(
.status == "requested" or
.status == "waiting" or
.status == "pending" or
.status == "queued" or
.status == "in_progress"
)
| select(
any(
.pull_requests[]?;
(.head.sha // "") as $run_pr_head
| .number == $pr_number
| select(
$target_action == "closed" or
(
($run_pr_head | test("^[0-9a-fA-F]{40}$")) and
$run_pr_head != $target_head
)
)
)
)
| .id
' <<<"$workflow_runs"
)

for run_id in "${superseded_run_ids[@]}"; do
if ! live_target_matches; then
echo "Superseded-run cleanup stopped because the target changed before cancellation."
exit 0
fi
if gh api -X POST \
"repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/force-cancel" \
>/dev/null 2>&1; then
cancellation_verified=false
for attempt in 1 2 3 4 5 6; do
IFS=$'\t' read -r run_status run_conclusion < <(
gh api "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}" \
--jq '[.status // "", .conclusion // ""] | @tsv'
)
if [ "$run_status" = "completed" ] &&
[ "$run_conclusion" = "cancelled" ]; then
cancellation_verified=true
break
fi
if [ "$attempt" -lt 6 ]; then
sleep 1
fi
done
if [ "$cancellation_verified" != "true" ]; then
echo "::error::Scheduler run $run_id did not reach completed/cancelled after accepted force-cancel."
exit 1
fi
echo "Verified cancelled scheduler run $run_id."
continue
fi
IFS=$'\t' read -r run_status run_conclusion < <(
gh api "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}" \
--jq '[.status // "", .conclusion // ""] | @tsv'
)
if [ "$run_status" = "completed" ]; then
continue
fi
echo "::error::Could not force-cancel nonterminal scheduler run $run_id."
exit 1
done

scan-pr-queue:
# repository_dispatch review runs do not reliably carry pull_requests metadata.
# Without this guard, one completed central review can wake a repo-wide scan.
Expand Down Expand Up @@ -212,6 +351,7 @@ jobs:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }}
TARGET_REPOSITORY_INPUT: ${{ github.event.client_payload.target_repository || '' }}
TARGET_PR_NUMBER: ${{ github.event.client_payload.pr_number || '' }}
TARGET_HEAD_SHA_INPUT: ${{ github.event.client_payload.pr_head_sha || '' }}
TARGET_BASE_BRANCH_INPUT: ${{ github.event.client_payload.base_branch || '' }}
ALLOWED_TARGET_REPOSITORIES: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS }}
run: |
Expand All @@ -231,8 +371,9 @@ jobs:
exit 1
fi
if ! [[ "$TARGET_REPOSITORY_INPUT" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
printf '::error::Targeted scheduler dispatch rejected an invalid repository or pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY_INPUT:-<empty>}" "${TARGET_PR_NUMBER:-<empty>}"
! [[ "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]] ||
! [[ "$TARGET_HEAD_SHA_INPUT" =~ ^[0-9a-fA-F]{40}$ ]]; then
printf '::error::Targeted scheduler dispatch rejected invalid repository, pull request, or head identity. target=%s pr=%s head=%s\n' "${TARGET_REPOSITORY_INPUT:-<empty>}" "${TARGET_PR_NUMBER:-<empty>}" "${TARGET_HEAD_SHA_INPUT:-<empty>}"
exit 1
fi

Expand Down Expand Up @@ -274,6 +415,10 @@ jobs:
printf '::error::Targeted scheduler dispatch base branch does not match the live PR. supplied=%s live=%s\n' "$TARGET_BASE_BRANCH_INPUT" "$live_base_branch"
exit 1
fi
if [ "${TARGET_HEAD_SHA_INPUT,,}" != "${live_head_sha,,}" ]; then
printf '::error::Targeted scheduler dispatch head does not match the live PR. supplied=%s live=%s\n' "$TARGET_HEAD_SHA_INPUT" "$live_head_sha"
exit 1
fi

{
printf 'repository=%s\n' "$TARGET_REPOSITORY_INPUT"
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.d/20260920-scheduler-cancellation-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Scheduler cleanup proves cancellation before completion

The required-review merge scheduler now revalidates the live pull request immediately before each superseded-run mutation and treats an accepted force-cancel request as incomplete until GitHub reports the run as `completed/cancelled`. Bounded executable regressions cover a concurrent head advance, a cancellation that never terminates, and a successful asynchronous cancellation.
6 changes: 6 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다.

### 2026-09-20 scheduler concurrency authority delta

| Gap ID | 상태 | exact-head evidence | causal owner / next gate |
|---|---|---|---|
| CONTROL-SCHEDULER-PENDING-PRESERVATION-01 | **Proposed / source repaired on stacked `.github#2289`; protected-main integration pending** | GitHub's native concurrency contract replaces an existing pending run under default `queue: single`. [RED `17be694a`](https://github.com/ContextualWisdomLab/.github/commit/17be694aaff421f8b48aa24250aeedd8eb2d696c) established the missing pending-preservation contract; [GREEN `ce3a4d9f`](https://github.com/ContextualWisdomLab/.github/commit/ce3a4d9f0b93584de85a076fb314840d262d3b68) adopted `queue: max`. Concurrent RED `e2647c5b` then exposed tests without production exact-head cleanup; production repair [`739a34c0`](https://github.com/ContextualWisdomLab/.github/commit/739a34c07d56f331767e6200d5172c648d0e27cf), authority RED [`648674c9`](https://github.com/ContextualWisdomLab/.github/commit/648674c92f1016f5cf8420f75d9d8679ff2dc665), and GREEN [`406c7178`](https://github.com/ContextualWisdomLab/.github/commit/406c7178974c3b887694a229a59c66a02e0810dd) bind cleanup to associated PR `head.sha`, not `pull_request_target` run `.head_sha`. Ordinary two-parent stack [`873a35f4`](https://github.com/ContextualWisdomLab/.github/commit/873a35f45c9c3b25f72797b25b6bbe98051d0f3d) preserves #2289 and canonical #2283 histories. Exact remote verification: static contracts 14/14, jq synchronize/closed fixtures 2/2, Python compile 2/2, YAML parse 1/1, cleanup Bash syntax 1/1. Cancellation-verification RED at `406c7178` failed all three executable regressions (post-selection live-head revalidation, accepted-but-nonterminal cancellation, and asynchronous terminal proof); GREEN [`8067fb65`](https://github.com/ContextualWisdomLab/.github/commit/8067fb65daa6b01265f3c0e011ffd83b7f439273), tree `004a1713`, passes those regressions 3/3 and the affected scheduler contracts 6/6 under both default and `GITHUB_ACTIONS=true` environments. Targeted cross-repository dispatch still accepted a caller-supplied `pr_head_sha` for concurrency without comparing it to the live pull request; executable RED [`2a81604d`](https://github.com/ContextualWisdomLab/.github/commit/2a81604d1be7b015d3675fa847466edb8f5101f7) rejects a stale value, and GREEN [`4767b64b`](https://github.com/ContextualWisdomLab/.github/commit/4767b64be35455fc7b36d2a656f014929343f456) requires a 40-hex identity and exact case-insensitive live-head match before scheduling. Hosted exact-head Checks remain pending. | Central `.github` owns scheduler concurrency; #2289 is stacked on #2283 rather than acting as a divergent writer. Same-head events retain native FIFO admission, successor heads enter a distinct group, and metadata-only cleanup revalidates live repository/PR/head before retiring predecessor runs. Completion requires prerequisite #2283 integration, fresh exact-head hosted Checks, no actionable review, qualifying independent approval, and ordinary protected-main merge. |

### 2026-09-13 current-head incident delta

| Gap ID | 상태 | exact-head evidence | causal owner / next gate |
Expand Down
17 changes: 15 additions & 2 deletions tests/test_close_empty_pr_queue_pressure.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,21 @@ def test_closed_pull_request_does_not_allocate_a_noop_runner(

assert "closed" in workflow
assert "github.event.pull_request.number" in concurrency
assert "github.event.pull_request.head.sha" not in concurrency
assert re.search(r"(?m)^[ \t]+cancel-in-progress:[ \t]+\S", concurrency)
if filename == "pr-review-merge-scheduler.yml":
assert "github.event.pull_request.head.sha" in concurrency
assert "queue: max" in concurrency
assert "cancel-in-progress:" not in concurrency
cleanup_job = workflow.split(
" cancel-superseded-pr-runs:", 1
)[1].split(" scan-pr-queue:", 1)[0]
assert "actions: write" in cleanup_job
assert "actions/checkout" not in cleanup_job
assert "github.event.action == 'closed'" in cleanup_job
assert "TARGET_PR_NUMBER" in cleanup_job
assert "TARGET_PR_HEAD_SHA" in cleanup_job
else:
assert "github.event.pull_request.head.sha" not in concurrency
assert re.search(r"(?m)^[ \t]+cancel-in-progress:[ \t]+\S", concurrency)
assert "cancel-closed-pr-runs:" not in workflow
assert "github.event.action != 'closed'" in workflow
assert evidence_job in workflow
24 changes: 16 additions & 8 deletions tests/test_current_head_coalescer_self_cancellation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


def test_current_head_coalescer_shares_pr_scoped_scheduler_admission() -> None:
"""The integrated step reuses PR-scoped scheduler admission and its runner."""
"""Exact-head admission and metadata cleanup share the scheduler boundary."""
workflow_text = WORKFLOW_PATH.read_text(encoding="utf-8")
coalescer = workflow_text.split("\n scan-pr-queue:\n", 1)[1]
concurrency_block = workflow_text.split("\nconcurrency:\n", 1)[1].split(
Expand All @@ -24,11 +24,19 @@ def test_current_head_coalescer_shares_pr_scoped_scheduler_admission() -> None:

assert "Retire redundant queued exact-head runs" in coalescer
assert "github.repository == 'ContextualWisdomLab/.github'" in coalescer
assert "github.event.pull_request.head.sha" not in concurrency_block
assert "github.event.pull_request.head.sha" in concurrency_block
assert "github.event.pull_request.number" in concurrency_block
assert any(
line.startswith("cancel-in-progress:")
and "github.event_name == 'pull_request_target'" in line
for line in active_lines
)
assert "queue: max" not in workflow_text
assert "github.event.client_payload.pr_head_sha" in concurrency_block
assert "queue: max" in active_lines
assert not any(line.startswith("cancel-in-progress:") for line in active_lines)

cleanup = workflow_text.split(
"\n cancel-superseded-pr-runs:\n", 1
)[1].split("\n scan-pr-queue:\n", 1)[0]
assert "actions: write" in cleanup
assert "actions/checkout" not in cleanup
assert "TARGET_REPOSITORY:" in cleanup
assert "TARGET_PR_NUMBER:" in cleanup
assert "TARGET_PR_HEAD_SHA:" in cleanup
assert "live_target_matches" in cleanup
assert 'actions/runs/${run_id}/force-cancel' in cleanup
Binary file modified tests/test_opencode_agent_contract.py
Binary file not shown.
18 changes: 18 additions & 0 deletions tests/test_opencode_workflow_shell_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path):
"DEFAULT_BRANCH": "main",
"TARGET_REPOSITORY_INPUT": "ContextualWisdomLab/naruon",
"TARGET_PR_NUMBER": "1179",
"TARGET_HEAD_SHA_INPUT": "4afd4af7ad343660356791873d940aa2846f40c2",
"TARGET_BASE_BRANCH_INPUT": "develop",
"ALLOWED_TARGET_REPOSITORIES": (
"ContextualWisdomLab/.github, ContextualWisdomLab/naruon"
Expand Down Expand Up @@ -248,6 +249,23 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path):
assert "absent from the configured exact allowlist" in rejected.stdout
assert not output.exists()

stale_head_env = {
**env,
"TARGET_HEAD_SHA_INPUT": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
stale_head = subprocess.run(
[bash],
input=script,
text=True,
capture_output=True,
check=False,
env=stale_head_env,
)

assert stale_head.returncode == 1
assert "head does not match the live PR" in stale_head.stdout
assert not output.exists()

output.unlink(missing_ok=True)
cross_repo_pull = {
**pull,
Expand Down
Loading
Loading