From 7c35ae8cdd0e78dde088c0d0373e1718894ad8c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:32:02 +0900 Subject: [PATCH 01/28] test(ci): define current-head run coalescing contract --- tests/test_current_head_run_coalescer.py | 173 +++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 tests/test_current_head_run_coalescer.py diff --git a/tests/test_current_head_run_coalescer.py b/tests/test_current_head_run_coalescer.py new file mode 100644 index 0000000000..f9dd3ce798 --- /dev/null +++ b/tests/test_current_head_run_coalescer.py @@ -0,0 +1,173 @@ +"""Regression tests for exact-current-head GitHub Actions run coalescing.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT = REPO_ROOT / "scripts" / "ci" / "current_head_run_coalescer.py" +WORKFLOW = REPO_ROOT / ".github" / "workflows" / "current-head-run-coalescer.yml" + + +def load_module(): + """Load the production coalescer only after proving the file exists.""" + assert SCRIPT.is_file(), "current-head duplicate coalescer is not implemented" + spec = importlib.util.spec_from_file_location("current_head_run_coalescer", SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def run_record( + run_id: int, + workflow_id: int, + *, + status: str = "queued", + head_sha: str = "a" * 40, + branch: str = "feature/current", + repository: str = "ContextualWisdomLab/.github", + event: str = "pull_request", +) -> dict[str, object]: + """Return one bounded Actions run fixture.""" + return { + "id": run_id, + "workflow_id": workflow_id, + "status": status, + "head_sha": head_sha, + "head_branch": branch, + "event": event, + "head_repository": {"full_name": repository}, + } + + +def test_select_duplicate_queued_runs_keeps_one_authoritative_run_per_workflow() -> None: + """Older queued duplicates are retired while one exact-head run survives.""" + module = load_module() + runs = [ + run_record(100, 10), + run_record(101, 10), + run_record(102, 10), + run_record(200, 20), + run_record(201, 20), + ] + + assert module.select_duplicate_queued_run_ids( + runs, + repository="ContextualWisdomLab/.github", + branch="feature/current", + head_sha="a" * 40, + ) == [100, 101, 200] + + +def test_in_progress_run_is_never_selected_and_makes_all_queued_siblings_redundant() -> None: + """A running authoritative workflow is preserved and queued duplicates retire.""" + module = load_module() + runs = [ + run_record(100, 10, status="in_progress"), + run_record(101, 10), + run_record(102, 10), + ] + + assert module.select_duplicate_queued_run_ids( + runs, + repository="ContextualWisdomLab/.github", + branch="feature/current", + head_sha="a" * 40, + ) == [101, 102] + + +def test_other_heads_branches_repositories_workflows_and_events_are_not_coalesced() -> None: + """Coalescing stays inside one exact current-head pull-request workflow identity.""" + module = load_module() + runs = [ + run_record(100, 10), + run_record(101, 11), + run_record(102, 10, head_sha="b" * 40), + run_record(103, 10, branch="other"), + run_record(104, 10, repository="ContextualWisdomLab/TEPP"), + run_record(105, 10, event="push"), + ] + + assert module.select_duplicate_queued_run_ids( + runs, + repository="ContextualWisdomLab/.github", + branch="feature/current", + head_sha="a" * 40, + ) == [] + + +def test_revalidation_requires_a_distinct_authoritative_sibling() -> None: + """The sole current-head run is preserved when no same-workflow sibling remains.""" + module = load_module() + candidate = run_record(100, 10) + with pytest.raises(module.CoalescingRefused, match="authoritative sibling"): + module.validate_candidate_against_live_state( + candidate, + live_pr={ + "state": "open", + "head": { + "sha": "a" * 40, + "ref": "feature/current", + "repo": {"full_name": "ContextualWisdomLab/.github"}, + }, + }, + active_same_head_runs=[candidate], + ) + + +def test_revalidation_rejects_moved_pr_and_nonqueued_candidate() -> None: + """A head move or status transition fails closed before cancellation.""" + module = load_module() + candidate = run_record(100, 10) + sibling = run_record(101, 10) + moved_pr = { + "state": "open", + "head": { + "sha": "b" * 40, + "ref": "feature/current", + "repo": {"full_name": "ContextualWisdomLab/.github"}, + }, + } + with pytest.raises(module.CoalescingRefused, match="head moved"): + module.validate_candidate_against_live_state( + candidate, + live_pr=moved_pr, + active_same_head_runs=[candidate, sibling], + ) + + running = run_record(100, 10, status="in_progress") + with pytest.raises(module.CoalescingRefused, match="no longer queued"): + module.validate_candidate_against_live_state( + running, + live_pr={ + "state": "open", + "head": { + "sha": "a" * 40, + "ref": "feature/current", + "repo": {"full_name": "ContextualWisdomLab/.github"}, + }, + }, + active_same_head_runs=[running, sibling], + ) + + +def test_workflow_is_trusted_pr_target_with_minimum_actions_write() -> None: + """The production workflow uses trusted source and the smallest mutation scope.""" + assert WORKFLOW.is_file(), "current-head duplicate coalescer workflow is not implemented" + text = WORKFLOW.read_text(encoding="utf-8") + assert "pull_request_target:" in text + assert "types: [opened, synchronize, reopened]" in text + assert "actions: write" in text + assert "contents: read" in text + assert "pull-requests: read" in text + assert "persist-credentials: false" in text + assert "ref: ${{ github.workflow_sha }}" in text + assert "current_head_run_coalescer.py" in text + assert "cancel-in-progress: true" in text + assert "github.event.pull_request.number" in text + assert "github.event.pull_request.head.sha" in text From 54572f923f35e92dabcf16a4265346178cd720a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:32:14 +0900 Subject: [PATCH 02/28] test(ci): stage read-only current-head coalescer RED --- .../_temp-current-head-run-coalescer-red.yml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/_temp-current-head-run-coalescer-red.yml diff --git a/.github/workflows/_temp-current-head-run-coalescer-red.yml b/.github/workflows/_temp-current-head-run-coalescer-red.yml new file mode 100644 index 0000000000..994126ef96 --- /dev/null +++ b/.github/workflows/_temp-current-head-run-coalescer-red.yml @@ -0,0 +1,48 @@ +name: Temporary current-head run coalescer RED + +on: + push: + branches: + - fix/current-head-run-coalescing-20260902 + +permissions: + contents: read + +concurrency: + group: temp-current-head-run-coalescer-red-${{ github.ref }} + cancel-in-progress: true + +jobs: + red: + if: github.event.head_commit.message == 'test(ci): execute current-head coalescer RED' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact RED head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + persist-credentials: false + - name: Verify the new regression fails because implementation is absent + shell: bash + run: | + set -euo pipefail + log="${RUNNER_TEMP}/current-head-coalescer-red.log" + set +e + PYTHONPATH=. python3 -m pytest -q tests/test_current_head_run_coalescer.py >"$log" 2>&1 + rc=$? + set -e + cat "$log" + if [ "$rc" -eq 0 ]; then + echo '::error::Expected current-head duplicate coalescing regression to fail before implementation.' + exit 1 + fi + if ! grep -Fq 'current-head duplicate coalescer is not implemented' "$log"; then + echo '::error::RED failed for an unexpected reason.' + exit 1 + fi + if grep -Fq 'ERROR collecting' "$log"; then + echo '::error::RED was a collection/environment failure.' + exit 1 + fi + echo "Verified expected behavior-level RED (pytest rc=${rc})." From 087f0ab553a4ce7f02a6e077b10f07160ff908cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:32:28 +0900 Subject: [PATCH 03/28] test(ci): execute current-head coalescer RED --- .github/workflows/_temp-current-head-run-coalescer-red.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/_temp-current-head-run-coalescer-red.yml b/.github/workflows/_temp-current-head-run-coalescer-red.yml index 994126ef96..86a4339d91 100644 --- a/.github/workflows/_temp-current-head-run-coalescer-red.yml +++ b/.github/workflows/_temp-current-head-run-coalescer-red.yml @@ -1,4 +1,5 @@ name: Temporary current-head run coalescer RED +# execution nonce: 20260902-1 on: push: From b7b5a11aaa19a5da2b877c6ca7e50b82ca654b2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:33:32 +0900 Subject: [PATCH 04/28] fix(ci): add exact-head queued-run coalescer --- scripts/ci/current_head_run_coalescer.py | 317 +++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 scripts/ci/current_head_run_coalescer.py diff --git a/scripts/ci/current_head_run_coalescer.py b/scripts/ci/current_head_run_coalescer.py new file mode 100644 index 0000000000..e82407b546 --- /dev/null +++ b/scripts/ci/current_head_run_coalescer.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +"""Retire redundant queued GitHub Actions runs for one exact open PR head. + +The coalescer is intentionally narrower than ordinary stale-head cleanup. It +never cancels an in-progress run and never cancels the only queued run for a +workflow. A queued candidate is eligible only when a distinct same-workflow, +same-repository, same-branch, same-head pull-request run is still active after +live PR and Actions state are re-fetched immediately before cancellation. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +from typing import Any, Iterable, Sequence + + +GIT_SHA_RE = re.compile(r"^[0-9a-f]{40}$") +REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +PR_EVENTS = frozenset({"pull_request", "pull_request_target"}) +ACTIVE_STATUSES = ("queued", "in_progress") + + +class CoalescingRefused(RuntimeError): + """Signal that live evidence is insufficient for a destructive cancellation.""" + + +def _positive_int(value: object) -> int | None: + """Return a positive integer without accepting booleans or numeric strings.""" + return value if type(value) is int and value > 0 else None + + +def _run_identity_matches( + run_data: dict[str, Any], + *, + repository: str, + branch: str, + head_sha: str, +) -> bool: + """Return whether one run belongs to the exact PR-head cancellation boundary.""" + return ( + run_data.get("event") in PR_EVENTS + and str(run_data.get("head_sha") or "").lower() == head_sha + and run_data.get("head_branch") == branch + and ((run_data.get("head_repository") or {}).get("full_name") == repository) + and _positive_int(run_data.get("workflow_id")) is not None + and _positive_int(run_data.get("id")) is not None + and run_data.get("status") in ACTIVE_STATUSES + ) + + +def select_duplicate_queued_run_ids( + runs: Iterable[dict[str, Any]], + *, + repository: str, + branch: str, + head_sha: str, +) -> list[int]: + """Select only redundant queued runs while retaining authoritative siblings. + + Runs are grouped by GitHub's stable numeric ``workflow_id`` after exact + repository/branch/head/event filtering. If a workflow already has an + in-progress run, every queued sibling is redundant. Otherwise the newest + queued run ID is retained and only older queued siblings are selected. + In-progress runs are never returned. + """ + groups: dict[int, list[dict[str, Any]]] = {} + for run_data in runs: + if not _run_identity_matches( + run_data, repository=repository, branch=branch, head_sha=head_sha + ): + continue + workflow_id = _positive_int(run_data.get("workflow_id")) + if workflow_id is not None: + groups.setdefault(workflow_id, []).append(run_data) + + redundant: list[int] = [] + for group in groups.values(): + queued = [item for item in group if item.get("status") == "queued"] + if not queued: + continue + if any(item.get("status") == "in_progress" for item in group): + redundant.extend( + run_id + for item in queued + if (run_id := _positive_int(item.get("id"))) is not None + ) + continue + queued_ids = sorted( + run_id + for item in queued + if (run_id := _positive_int(item.get("id"))) is not None + ) + if len(queued_ids) > 1: + redundant.extend(queued_ids[:-1]) + return sorted(redundant) + + +def validate_candidate_against_live_state( + candidate: dict[str, Any], + *, + live_pr: dict[str, Any], + active_same_head_runs: Sequence[dict[str, Any]], +) -> None: + """Fail closed unless a queued candidate still has an authoritative sibling.""" + if candidate.get("status") != "queued": + raise CoalescingRefused("candidate is no longer queued") + if live_pr.get("state") != "open": + raise CoalescingRefused("pull request is no longer open") + + live_head = live_pr.get("head") or {} + live_repo = ((live_head.get("repo") or {}).get("full_name") or "") + live_ref = str(live_head.get("ref") or "") + live_sha = str(live_head.get("sha") or "").lower() + candidate_repo = ((candidate.get("head_repository") or {}).get("full_name") or "") + candidate_ref = str(candidate.get("head_branch") or "") + candidate_sha = str(candidate.get("head_sha") or "").lower() + if ( + not GIT_SHA_RE.fullmatch(live_sha) + or live_sha != candidate_sha + or live_ref != candidate_ref + or live_repo != candidate_repo + ): + raise CoalescingRefused("pull request head moved after duplicate classification") + + candidate_id = _positive_int(candidate.get("id")) + workflow_id = _positive_int(candidate.get("workflow_id")) + if candidate_id is None or workflow_id is None: + raise CoalescingRefused("candidate identity is malformed") + if candidate.get("event") not in PR_EVENTS: + raise CoalescingRefused("candidate is not a pull-request workflow run") + + authoritative_sibling = False + for sibling in active_same_head_runs: + sibling_id = _positive_int(sibling.get("id")) + if sibling_id is None or sibling_id == candidate_id: + continue + if _positive_int(sibling.get("workflow_id")) != workflow_id: + continue + if not _run_identity_matches( + sibling, repository=live_repo, branch=live_ref, head_sha=live_sha + ): + continue + if sibling.get("status") == "in_progress" or sibling_id > candidate_id: + authoritative_sibling = True + break + if not authoritative_sibling: + raise CoalescingRefused("no distinct authoritative sibling remains active") + + +def _run_json(args: Sequence[str]) -> Any: + """Run one bounded GitHub CLI call and decode its JSON response.""" + if not os.environ.get("GH_TOKEN"): + raise RuntimeError("GH_TOKEN is required for current-head run coalescing") + completed = subprocess.run( + list(args), + capture_output=True, + text=True, + check=False, + shell=False, + env=os.environ.copy(), + ) + if completed.returncode != 0: + diagnostic = (completed.stderr or completed.stdout or "GitHub API request failed").strip() + raise RuntimeError(diagnostic[:600]) + return json.loads(completed.stdout or "null") + + +def _fetch_pr(repo: str, number: int) -> dict[str, Any]: + """Fetch one live pull request through GitHub REST.""" + payload = _run_json( + ["gh", "api", "-H", "Accept: application/vnd.github+json", f"repos/{repo}/pulls/{number}"] + ) + if not isinstance(payload, dict): + raise RuntimeError("GitHub returned malformed pull-request evidence") + return payload + + +def _active_runs(repo: str, head_sha: str) -> list[dict[str, Any]]: + """Fetch queued and in-progress runs for one exact commit SHA.""" + runs: list[dict[str, Any]] = [] + for status in ACTIVE_STATUSES: + page = 1 + while True: + payload = _run_json( + [ + "gh", + "api", + "--method", + "GET", + f"repos/{repo}/actions/runs", + "-f", + f"status={status}", + "-f", + f"head_sha={head_sha}", + "-F", + "per_page=100", + "-F", + f"page={page}", + ] + ) + if not isinstance(payload, dict) or not isinstance(payload.get("workflow_runs"), list): + raise RuntimeError("GitHub returned malformed Actions run evidence") + batch = payload["workflow_runs"] + runs.extend(item for item in batch if isinstance(item, dict)) + if len(batch) < 100: + break + page += 1 + return runs + + +def _fetch_run(repo: str, run_id: int) -> dict[str, Any]: + """Fetch one exact Actions run immediately before possible cancellation.""" + payload = _run_json( + [ + "gh", + "api", + "-H", + "Accept: application/vnd.github+json", + f"repos/{repo}/actions/runs/{run_id}", + ] + ) + if not isinstance(payload, dict): + raise RuntimeError("GitHub returned malformed Actions run identity evidence") + return payload + + +def _cancel_run(repo: str, run_id: int) -> None: + """Cancel one queued duplicate using GitHub's ordinary cancellation endpoint.""" + completed = subprocess.run( + ["gh", "api", "-X", "POST", f"repos/{repo}/actions/runs/{run_id}/cancel"], + capture_output=True, + text=True, + check=False, + shell=False, + env=os.environ.copy(), + ) + if completed.returncode != 0: + diagnostic = (completed.stderr or completed.stdout or "GitHub cancellation failed").strip() + raise RuntimeError(diagnostic[:600]) + + +def coalesce(repo: str, number: int, expected_repo: str, expected_ref: str, expected_head: str) -> list[int]: + """Cancel redundant queued runs after exact live PR/run/sibling revalidation.""" + if not REPOSITORY_RE.fullmatch(repo) or not REPOSITORY_RE.fullmatch(expected_repo): + raise RuntimeError("repository identity is malformed") + if not GIT_SHA_RE.fullmatch(expected_head): + raise RuntimeError("expected head must be a lowercase 40-character Git SHA") + if number <= 0 or not expected_ref or any(char.isspace() for char in expected_ref): + raise RuntimeError("pull-request identity is malformed") + + live_pr = _fetch_pr(repo, number) + live_head = live_pr.get("head") or {} + if ( + live_pr.get("state") != "open" + or str(live_head.get("sha") or "").lower() != expected_head + or live_head.get("ref") != expected_ref + or ((live_head.get("repo") or {}).get("full_name") != expected_repo) + ): + raise CoalescingRefused("pull request head moved before duplicate classification") + + snapshot = _active_runs(repo, expected_head) + candidates = select_duplicate_queued_run_ids( + snapshot, + repository=expected_repo, + branch=expected_ref, + head_sha=expected_head, + ) + cancelled: list[int] = [] + for run_id in candidates: + try: + candidate = _fetch_run(repo, run_id) + current_pr = _fetch_pr(repo, number) + active = _active_runs(repo, expected_head) + validate_candidate_against_live_state( + candidate, + live_pr=current_pr, + active_same_head_runs=active, + ) + _cancel_run(repo, run_id) + except CoalescingRefused as exc: + print(f"Preserving run {run_id}: {exc}") + continue + cancelled.append(run_id) + print(f"Cancelled redundant queued current-head run {run_id} for {repo}#{number}.") + return cancelled + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + """Parse the exact pull-request identity supplied by the trusted workflow.""" + parser = argparse.ArgumentParser() + parser.add_argument("--repo", required=True) + parser.add_argument("--pr-number", required=True, type=int) + parser.add_argument("--expected-head-repo", required=True) + parser.add_argument("--expected-head-ref", required=True) + parser.add_argument("--expected-head", required=True) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + """Run the coalescer and fail closed on malformed or unavailable evidence.""" + args = parse_args(argv) + coalesce( + args.repo, + args.pr_number, + args.expected_head_repo, + args.expected_head_ref, + args.expected_head, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From ca42d8359fca1ab16404ebc1f4656cb05229cca8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:33:44 +0900 Subject: [PATCH 05/28] fix(ci): run coalescer from trusted PR-target source --- .../workflows/current-head-run-coalescer.yml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/current-head-run-coalescer.yml diff --git a/.github/workflows/current-head-run-coalescer.yml b/.github/workflows/current-head-run-coalescer.yml new file mode 100644 index 0000000000..b0be3c8b23 --- /dev/null +++ b/.github/workflows/current-head-run-coalescer.yml @@ -0,0 +1,37 @@ +name: Current Head Run Coalescer + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +concurrency: + group: current-head-run-coalescer-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + actions: write + contents: read + pull-requests: read + +jobs: + coalesce: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout trusted control-plane source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/.github + ref: ${{ github.workflow_sha }} + persist-credentials: false + + - name: Retire redundant queued exact-head runs + env: + GH_TOKEN: ${{ github.token }} + run: >- + python3 scripts/ci/current_head_run_coalescer.py + --repo "${{ github.repository }}" + --pr-number "${{ github.event.pull_request.number }}" + --expected-head-repo "${{ github.event.pull_request.head.repo.full_name }}" + --expected-head-ref "${{ github.event.pull_request.head.ref }}" + --expected-head "${{ github.event.pull_request.head.sha }}" From 175151925fd7e70f41aa4acfa0843f010bc5d46a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:33:57 +0900 Subject: [PATCH 06/28] test(ci): execute current-head coalescer GREEN --- .../_temp-current-head-run-coalescer-red.yml | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/.github/workflows/_temp-current-head-run-coalescer-red.yml b/.github/workflows/_temp-current-head-run-coalescer-red.yml index 86a4339d91..7fc469fd84 100644 --- a/.github/workflows/_temp-current-head-run-coalescer-red.yml +++ b/.github/workflows/_temp-current-head-run-coalescer-red.yml @@ -1,5 +1,5 @@ -name: Temporary current-head run coalescer RED -# execution nonce: 20260902-1 +name: Temporary current-head run coalescer verification +# execution nonce: 20260902-green-1 on: push: @@ -10,40 +10,28 @@ permissions: contents: read concurrency: - group: temp-current-head-run-coalescer-red-${{ github.ref }} + group: temp-current-head-run-coalescer-verification-${{ github.ref }} cancel-in-progress: true jobs: - red: - if: github.event.head_commit.message == 'test(ci): execute current-head coalescer RED' + verify: + if: github.event.head_commit.message == 'test(ci): execute current-head coalescer GREEN' runs-on: ubuntu-24.04 - timeout-minutes: 10 + timeout-minutes: 15 steps: - - name: Checkout exact RED head + - name: Checkout exact GREEN head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.sha }} persist-credentials: false - - name: Verify the new regression fails because implementation is absent + - name: Install repository-declared test toolchain + run: >- + python3 -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt + - name: Verify focused contract shell: bash run: | set -euo pipefail - log="${RUNNER_TEMP}/current-head-coalescer-red.log" - set +e - PYTHONPATH=. python3 -m pytest -q tests/test_current_head_run_coalescer.py >"$log" 2>&1 - rc=$? - set -e - cat "$log" - if [ "$rc" -eq 0 ]; then - echo '::error::Expected current-head duplicate coalescing regression to fail before implementation.' - exit 1 - fi - if ! grep -Fq 'current-head duplicate coalescer is not implemented' "$log"; then - echo '::error::RED failed for an unexpected reason.' - exit 1 - fi - if grep -Fq 'ERROR collecting' "$log"; then - echo '::error::RED was a collection/environment failure.' - exit 1 - fi - echo "Verified expected behavior-level RED (pytest rc=${rc})." + PYTHONPATH=. python3 -m pytest -q tests/test_current_head_run_coalescer.py + interrogate --fail-under=100 scripts/ci/current_head_run_coalescer.py + python3 -m compileall -q scripts/ci/current_head_run_coalescer.py tests/test_current_head_run_coalescer.py + git diff --check From 49b882ab1010fe410f027f22bc61dd20503d07a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:34:22 +0900 Subject: [PATCH 07/28] chore(ci): remove one-use coalescer test workflow --- .../_temp-current-head-run-coalescer-red.yml | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 .github/workflows/_temp-current-head-run-coalescer-red.yml diff --git a/.github/workflows/_temp-current-head-run-coalescer-red.yml b/.github/workflows/_temp-current-head-run-coalescer-red.yml deleted file mode 100644 index 7fc469fd84..0000000000 --- a/.github/workflows/_temp-current-head-run-coalescer-red.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Temporary current-head run coalescer verification -# execution nonce: 20260902-green-1 - -on: - push: - branches: - - fix/current-head-run-coalescing-20260902 - -permissions: - contents: read - -concurrency: - group: temp-current-head-run-coalescer-verification-${{ github.ref }} - cancel-in-progress: true - -jobs: - verify: - if: github.event.head_commit.message == 'test(ci): execute current-head coalescer GREEN' - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Checkout exact GREEN head - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - persist-credentials: false - - name: Install repository-declared test toolchain - run: >- - python3 -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt - - name: Verify focused contract - shell: bash - run: | - set -euo pipefail - PYTHONPATH=. python3 -m pytest -q tests/test_current_head_run_coalescer.py - interrogate --fail-under=100 scripts/ci/current_head_run_coalescer.py - python3 -m compileall -q scripts/ci/current_head_run_coalescer.py tests/test_current_head_run_coalescer.py - git diff --check From 7f1bb773491944bdffd6bf7a26d4fe82c651c41e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:35:27 +0900 Subject: [PATCH 08/28] docs(doctoring): record current-head run coalescing boundary --- docs/doctoring/current-head-run-coalescing.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/doctoring/current-head-run-coalescing.md diff --git a/docs/doctoring/current-head-run-coalescing.md b/docs/doctoring/current-head-run-coalescing.md new file mode 100644 index 0000000000..2ad5659f7f --- /dev/null +++ b/docs/doctoring/current-head-run-coalescing.md @@ -0,0 +1,47 @@ +# Current-head workflow-run coalescing + +## Incident + +On 2026-09-02, exact head `09908aaf56e568420105b81434c6cdd147856657` was reused when Draft pull request #1050 was closed and ready successor #1643 was opened. GitHub exposed two simultaneously queued runs for several expensive workflows on that unchanged branch/head, including Security Scan (`33561053485`, `33561076062`), CodeQL PR (`33561053137`, `33561076168`), Python Security (`33561053333`, `33561076150`), and SAST Semgrep (`33561053180`, `33561076360`). Equivalent duplicate pairs existed for Secret Scan, SBOM Generation, Scorecard PR, and OSV-Scanner PR. + +The live-ref queue-hygiene repair from #1348 correctly prevents stale pull-request payloads from cancelling a newly pushed authoritative head. Its destructive revalidation intentionally preserves any run whose `head_sha` still equals the live branch ref. That safety invariant does not distinguish the sole authoritative current-head run from redundant queued siblings belonging to the same GitHub `workflow_id`. PR recreation therefore exposed a second, orthogonal capacity leak: safe stale-head preservation could retain several same-workflow runs for one current head. + +## Trust boundary + +`.github/workflows/current-head-run-coalescer.yml` executes only on `pull_request_target` `opened`, `synchronize`, and `reopened`. It checks out `ContextualWisdomLab/.github` at immutable `github.workflow_sha` with persisted credentials disabled. The job has only `actions: write`, `contents: read`, and `pull-requests: read`; it never checks out or executes the pull-request head. + +The workflow passes the event's repository, PR number, head repository, head ref, and lowercase 40-character head SHA into `scripts/ci/current_head_run_coalescer.py`. The script immediately re-fetches the live PR before classification. Before every cancellation it re-fetches the candidate run, the live PR, and active runs for the exact head again. Missing, malformed, moved, closed, or ambiguous evidence preserves the candidate. + +## Cancellation invariant + +Runs are eligible only when all of the following are true: + +1. the run was triggered by `pull_request` or `pull_request_target`; +2. its head repository, branch, and SHA exactly match the live open PR; +3. its stable numeric `workflow_id` matches another active exact-head sibling; +4. the candidate is still `queued` immediately before mutation; and +5. a distinct authoritative sibling is still active: either an `in_progress` sibling or a newer queued sibling. + +The coalescer never selects an `in_progress` run. If a workflow already has an in-progress run, only queued siblings are redundant. If every matching run is queued, the greatest run ID is retained and older queued siblings are candidates. A candidate for which the authoritative sibling disappears is preserved. Cancellation uses GitHub's ordinary `/cancel` endpoint rather than `force-cancel`. + +This invariant is deliberately separate from old-head cancellation. #1348 remains authoritative for resolving live Git refs before retiring superseded heads; the coalescer handles only redundant queued evidence on the same live head. + +## Executable evidence + +`tests/test_current_head_run_coalescer.py` pins the source and workflow contract. The regression was committed before either production file existed, so the initial expected failure was the absent coalescer implementation. The final cases cover one-run retention, in-progress preservation, isolation across workflow/head/branch/repository/event, sole-run preservation, moved-head/status fail-closed behavior, trusted-source checkout, PR-stable concurrency, and minimum workflow permissions. + +A one-use read-only branch workflow was attempted solely to capture hosted RED/GREEN evidence; GitHub did not schedule newly introduced branch-only push workflows in this repository, so no hosted result is claimed from that mechanism and it was deleted from the publishable tree. Ordinary protected PR checks and independent review on the exact production head remain authoritative. + +## Recovery and rollback + +If the coalescer reports unexpected preservation, inspect the live PR/run/sibling identities before changing policy. Do not weaken the exact-head or authoritative-sibling checks to improve cancellation volume. If a false cancellation is ever observed, disable the `current-head-run-coalescer.yml` trigger first while retaining #1348 stale-head queue hygiene, then reproduce the identity race with a deterministic regression before repair. + +The feature is operability-only: it does not convert cancelled, queued, missing, stale, or predecessor evidence into passing merge evidence, and it does not change required-check, security, review, or branch-protection policy. + +## References + +GitHub. (2026). *REST API endpoints for workflow runs*. GitHub Docs. https://docs.github.com/en/rest/actions/workflow-runs + +GitHub. (2026). *Workflow syntax for GitHub Actions: concurrency*. GitHub Docs. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency + +National Institute of Standards and Technology. (2020). *Security and privacy controls for information systems and organizations* (NIST Special Publication 800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5 From 1ff20f2c352e731f4506a253a80e85692204087f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:37:25 +0900 Subject: [PATCH 09/28] test(ci): cover coalescer transport and fail-closed edges --- tests/test_current_head_run_coalescer.py | 298 +++++++++++++++++++---- 1 file changed, 253 insertions(+), 45 deletions(-) diff --git a/tests/test_current_head_run_coalescer.py b/tests/test_current_head_run_coalescer.py index f9dd3ce798..e4c082fb26 100644 --- a/tests/test_current_head_run_coalescer.py +++ b/tests/test_current_head_run_coalescer.py @@ -3,7 +3,11 @@ from __future__ import annotations import importlib.util +import json +import runpy +import sys from pathlib import Path +from types import SimpleNamespace import pytest @@ -45,6 +49,18 @@ def run_record( } +def live_pr(*, state: str = "open", head_sha: str = "a" * 40) -> dict[str, object]: + """Return the exact live PR identity used by revalidation tests.""" + return { + "state": state, + "head": { + "sha": head_sha, + "ref": "feature/current", + "repo": {"full_name": "ContextualWisdomLab/.github"}, + }, + } + + def test_select_duplicate_queued_runs_keeps_one_authoritative_run_per_workflow() -> None: """Older queued duplicates are retired while one exact-head run survives.""" module = load_module() @@ -55,7 +71,6 @@ def test_select_duplicate_queued_runs_keeps_one_authoritative_run_per_workflow() run_record(200, 20), run_record(201, 20), ] - assert module.select_duplicate_queued_run_ids( runs, repository="ContextualWisdomLab/.github", @@ -64,7 +79,7 @@ def test_select_duplicate_queued_runs_keeps_one_authoritative_run_per_workflow() ) == [100, 101, 200] -def test_in_progress_run_is_never_selected_and_makes_all_queued_siblings_redundant() -> None: +def test_in_progress_run_is_never_selected_and_makes_queued_siblings_redundant() -> None: """A running authoritative workflow is preserved and queued duplicates retire.""" module = load_module() runs = [ @@ -72,7 +87,6 @@ def test_in_progress_run_is_never_selected_and_makes_all_queued_siblings_redunda run_record(101, 10), run_record(102, 10), ] - assert module.select_duplicate_queued_run_ids( runs, repository="ContextualWisdomLab/.github", @@ -81,7 +95,7 @@ def test_in_progress_run_is_never_selected_and_makes_all_queued_siblings_redunda ) == [101, 102] -def test_other_heads_branches_repositories_workflows_and_events_are_not_coalesced() -> None: +def test_other_identities_and_malformed_runs_are_not_coalesced() -> None: """Coalescing stays inside one exact current-head pull-request workflow identity.""" module = load_module() runs = [ @@ -91,71 +105,265 @@ def test_other_heads_branches_repositories_workflows_and_events_are_not_coalesce run_record(103, 10, branch="other"), run_record(104, 10, repository="ContextualWisdomLab/TEPP"), run_record(105, 10, event="push"), + run_record(0, 10), + run_record(106, 0), + {**run_record(107, 10), "status": "completed"}, ] - assert module.select_duplicate_queued_run_ids( runs, repository="ContextualWisdomLab/.github", branch="feature/current", head_sha="a" * 40, ) == [] + assert module._positive_int(True) is None + assert module._positive_int("1") is None + assert module._positive_int(0) is None + assert module._positive_int(1) == 1 -def test_revalidation_requires_a_distinct_authoritative_sibling() -> None: - """The sole current-head run is preserved when no same-workflow sibling remains.""" +def test_revalidation_requires_a_distinct_newer_or_running_sibling() -> None: + """The sole or newest queued current-head run is never cancelled.""" module = load_module() candidate = run_record(100, 10) - with pytest.raises(module.CoalescingRefused, match="authoritative sibling"): - module.validate_candidate_against_live_state( - candidate, - live_pr={ - "state": "open", - "head": { - "sha": "a" * 40, - "ref": "feature/current", - "repo": {"full_name": "ContextualWisdomLab/.github"}, - }, - }, - active_same_head_runs=[candidate], - ) + for active in ([candidate], [candidate, run_record(99, 10)]): + with pytest.raises(module.CoalescingRefused, match="authoritative sibling"): + module.validate_candidate_against_live_state( + candidate, + live_pr=live_pr(), + active_same_head_runs=active, + ) + module.validate_candidate_against_live_state( + candidate, + live_pr=live_pr(), + active_same_head_runs=[candidate, run_record(101, 10)], + ) + module.validate_candidate_against_live_state( + candidate, + live_pr=live_pr(), + active_same_head_runs=[candidate, run_record(99, 10, status="in_progress")], + ) -def test_revalidation_rejects_moved_pr_and_nonqueued_candidate() -> None: - """A head move or status transition fails closed before cancellation.""" +def test_revalidation_fails_closed_for_status_state_identity_and_event_changes() -> None: + """Every live identity transition preserves the candidate before mutation.""" module = load_module() candidate = run_record(100, 10) sibling = run_record(101, 10) - moved_pr = { - "state": "open", - "head": { - "sha": "b" * 40, - "ref": "feature/current", - "repo": {"full_name": "ContextualWisdomLab/.github"}, - }, - } + with pytest.raises(module.CoalescingRefused, match="no longer queued"): + module.validate_candidate_against_live_state( + run_record(100, 10, status="in_progress"), + live_pr=live_pr(), + active_same_head_runs=[sibling], + ) + with pytest.raises(module.CoalescingRefused, match="no longer open"): + module.validate_candidate_against_live_state( + candidate, live_pr=live_pr(state="closed"), active_same_head_runs=[sibling] + ) with pytest.raises(module.CoalescingRefused, match="head moved"): module.validate_candidate_against_live_state( - candidate, - live_pr=moved_pr, - active_same_head_runs=[candidate, sibling], + candidate, live_pr=live_pr(head_sha="b" * 40), active_same_head_runs=[sibling] + ) + malformed = run_record(0, 10) + with pytest.raises(module.CoalescingRefused, match="identity is malformed"): + module.validate_candidate_against_live_state( + malformed, live_pr=live_pr(), active_same_head_runs=[sibling] + ) + wrong_event = run_record(100, 10, event="push") + with pytest.raises(module.CoalescingRefused, match="not a pull-request"): + module.validate_candidate_against_live_state( + wrong_event, live_pr=live_pr(), active_same_head_runs=[sibling] ) - running = run_record(100, 10, status="in_progress") - with pytest.raises(module.CoalescingRefused, match="no longer queued"): + +def test_revalidation_ignores_non_authoritative_sibling_shapes() -> None: + """Different workflow or malformed sibling records cannot authorize cancellation.""" + module = load_module() + candidate = run_record(100, 10) + siblings = [ + run_record(101, 11), + run_record(102, 10, branch="other"), + run_record(0, 10), + candidate, + ] + with pytest.raises(module.CoalescingRefused, match="authoritative sibling"): module.validate_candidate_against_live_state( - running, - live_pr={ - "state": "open", - "head": { - "sha": "a" * 40, - "ref": "feature/current", - "repo": {"full_name": "ContextualWisdomLab/.github"}, - }, - }, - active_same_head_runs=[running, sibling], + candidate, live_pr=live_pr(), active_same_head_runs=siblings ) +def test_run_json_uses_token_decodes_success_and_bounds_failure(monkeypatch) -> None: + """GitHub transport is token-bound, JSON-only, and bounded on command failure.""" + module = load_module() + monkeypatch.delenv("GH_TOKEN", raising=False) + with pytest.raises(RuntimeError, match="GH_TOKEN"): + module._run_json(["gh", "api", "repos/o/r"]) + + monkeypatch.setenv("GH_TOKEN", "token") + monkeypatch.setattr( + module.subprocess, + "run", + lambda *args, **kwargs: SimpleNamespace(returncode=0, stdout='{"ok":true}', stderr=""), + ) + assert module._run_json(["gh", "api", "repos/o/r"]) == {"ok": True} + + monkeypatch.setattr( + module.subprocess, + "run", + lambda *args, **kwargs: SimpleNamespace(returncode=1, stdout="", stderr="x" * 700), + ) + with pytest.raises(RuntimeError) as exc_info: + module._run_json(["gh", "api", "repos/o/r"]) + assert len(str(exc_info.value)) == 600 + + +def test_fetch_helpers_fail_closed_and_paginate(monkeypatch) -> None: + """PR/run fetches reject malformed payloads and Actions pagination is complete.""" + module = load_module() + monkeypatch.setattr(module, "_run_json", lambda _args: {"state": "open"}) + assert module._fetch_pr("o/r", 1) == {"state": "open"} + assert module._fetch_run("o/r", 2) == {"state": "open"} + + monkeypatch.setattr(module, "_run_json", lambda _args: []) + with pytest.raises(RuntimeError, match="pull-request evidence"): + module._fetch_pr("o/r", 1) + with pytest.raises(RuntimeError, match="run identity evidence"): + module._fetch_run("o/r", 1) + + hundred = [run_record(index + 1, 10) for index in range(100)] + calls: list[list[str]] = [] + + def pages(args): + calls.append(list(args)) + status = next(item.split("=", 1)[1] for item in args if item.startswith("status=")) + page = int(next(item.split("=", 1)[1] for item in args if item.startswith("page="))) + if status == "queued" and page == 1: + return {"workflow_runs": hundred} + if status == "queued" and page == 2: + return {"workflow_runs": [run_record(101, 10)]} + return {"workflow_runs": []} + + monkeypatch.setattr(module, "_run_json", pages) + assert len(module._active_runs("o/r", "a" * 40)) == 101 + assert any("page=2" in call for call in calls) + + monkeypatch.setattr(module, "_run_json", lambda _args: {"workflow_runs": "bad"}) + with pytest.raises(RuntimeError, match="malformed Actions"): + module._active_runs("o/r", "a" * 40) + + +def test_cancel_run_uses_ordinary_endpoint_and_surfaces_failure(monkeypatch) -> None: + """Only GitHub's ordinary cancellation endpoint is used for queued duplicates.""" + module = load_module() + calls: list[list[str]] = [] + + def success(args, **_kwargs): + calls.append(list(args)) + return SimpleNamespace(returncode=0, stdout="", stderr="") + + monkeypatch.setattr(module.subprocess, "run", success) + module._cancel_run("o/r", 123) + assert calls == [["gh", "api", "-X", "POST", "repos/o/r/actions/runs/123/cancel"]] + + monkeypatch.setattr( + module.subprocess, + "run", + lambda *args, **kwargs: SimpleNamespace(returncode=1, stdout="failed", stderr=""), + ) + with pytest.raises(RuntimeError, match="failed"): + module._cancel_run("o/r", 123) + + +def test_coalesce_validates_inputs_rechecks_each_candidate_and_preserves_races(monkeypatch, capsys) -> None: + """The mutation path revalidates live state per candidate and tolerates a disappearing sibling.""" + module = load_module() + for repo in ("../evil", "owner/..", "owner/repo/extra"): + with pytest.raises(RuntimeError, match="repository identity"): + module.coalesce(repo, 1, "owner/repo", "feature/current", "a" * 40) + with pytest.raises(RuntimeError, match="expected head"): + module.coalesce("owner/repo", 1, "owner/repo", "feature/current", "BAD") + with pytest.raises(RuntimeError, match="pull-request identity"): + module.coalesce("owner/repo", 0, "owner/repo", "feature/current", "a" * 40) + with pytest.raises(RuntimeError, match="pull-request identity"): + module.coalesce("owner/repo", 1, "owner/repo", "bad ref", "a" * 40) + + monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr(head_sha="b" * 40)) + with pytest.raises(module.CoalescingRefused, match="moved before"): + module.coalesce( + "ContextualWisdomLab/.github", + 1, + "ContextualWisdomLab/.github", + "feature/current", + "a" * 40, + ) + + candidate = run_record(100, 10) + sibling = run_record(101, 10) + monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) + active_calls = iter([[candidate, sibling], [candidate]]) + monkeypatch.setattr(module, "_active_runs", lambda *_args: next(active_calls)) + monkeypatch.setattr(module, "_fetch_run", lambda *_args: candidate) + cancelled: list[int] = [] + monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) + assert module.coalesce( + "ContextualWisdomLab/.github", + 1, + "ContextualWisdomLab/.github", + "feature/current", + "a" * 40, + ) == [] + assert cancelled == [] + assert "Preserving run 100" in capsys.readouterr().out + + +def test_coalesce_cancels_only_revalidated_redundant_candidates(monkeypatch, capsys) -> None: + """A proven older queued duplicate is cancelled and reported exactly once.""" + module = load_module() + candidate = run_record(100, 10) + sibling = run_record(101, 10) + monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) + monkeypatch.setattr(module, "_active_runs", lambda *_args: [candidate, sibling]) + monkeypatch.setattr(module, "_fetch_run", lambda *_args: candidate) + cancelled: list[int] = [] + monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) + assert module.coalesce( + "ContextualWisdomLab/.github", + 1, + "ContextualWisdomLab/.github", + "feature/current", + "a" * 40, + ) == [100] + assert cancelled == [100] + assert "Cancelled redundant queued current-head run 100" in capsys.readouterr().out + + +def test_parse_args_main_and_script_help(monkeypatch) -> None: + """CLI parsing forwards exact identity and the executable entrypoint is reachable.""" + module = load_module() + argv = [ + "--repo", + "owner/repo", + "--pr-number", + "7", + "--expected-head-repo", + "owner/repo", + "--expected-head-ref", + "feature/current", + "--expected-head", + "a" * 40, + ] + parsed = module.parse_args(argv) + assert parsed.pr_number == 7 + calls: list[tuple[object, ...]] = [] + monkeypatch.setattr(module, "coalesce", lambda *args: calls.append(args) or []) + assert module.main(argv) == 0 + assert calls == [("owner/repo", 7, "owner/repo", "feature/current", "a" * 40)] + + monkeypatch.setattr(sys, "argv", [str(SCRIPT), "--help"]) + with pytest.raises(SystemExit) as exc_info: + runpy.run_path(str(SCRIPT), run_name="__main__") + assert exc_info.value.code == 0 + + def test_workflow_is_trusted_pr_target_with_minimum_actions_write() -> None: """The production workflow uses trusted source and the smallest mutation scope.""" assert WORKFLOW.is_file(), "current-head duplicate coalescer workflow is not implemented" From c6c7176056b9ab1a5561ce479b02f820c268a7d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:38:03 +0900 Subject: [PATCH 10/28] fix(ci): reject dot-segment repository identities --- scripts/ci/current_head_run_coalescer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/ci/current_head_run_coalescer.py b/scripts/ci/current_head_run_coalescer.py index e82407b546..f330fed80c 100644 --- a/scripts/ci/current_head_run_coalescer.py +++ b/scripts/ci/current_head_run_coalescer.py @@ -19,7 +19,9 @@ GIT_SHA_RE = re.compile(r"^[0-9a-f]{40}$") -REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +REPOSITORY_RE = re.compile( + r"^(?!\.{1,2}/)[A-Za-z0-9_.-]+/(?!\.{1,2}$)[A-Za-z0-9_.-]+$" +) PR_EVENTS = frozenset({"pull_request", "pull_request_target"}) ACTIVE_STATUSES = ("queued", "in_progress") From dc17f36f78e10e051c316e7be0bcfa853dd7540c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:59:31 +0900 Subject: [PATCH 11/28] fix(actions): isolate coalescing by live PR identity --- scripts/ci/current_head_run_coalescer.py | 213 +++++++++++++++++------ 1 file changed, 156 insertions(+), 57 deletions(-) diff --git a/scripts/ci/current_head_run_coalescer.py b/scripts/ci/current_head_run_coalescer.py index f330fed80c..3567219e4d 100644 --- a/scripts/ci/current_head_run_coalescer.py +++ b/scripts/ci/current_head_run_coalescer.py @@ -2,10 +2,10 @@ """Retire redundant queued GitHub Actions runs for one exact open PR head. The coalescer is intentionally narrower than ordinary stale-head cleanup. It -never cancels an in-progress run and never cancels the only queued run for a -workflow. A queued candidate is eligible only when a distinct same-workflow, -same-repository, same-branch, same-head pull-request run is still active after -live PR and Actions state are re-fetched immediately before cancellation. +never intentionally cancels an in-progress run and never cancels the only +queued run for a workflow. A queued candidate is eligible only when a distinct +same-workflow run is still authoritative after live PR, association, sibling, +and candidate state are re-fetched immediately before cancellation. """ from __future__ import annotations @@ -15,7 +15,7 @@ import os import re import subprocess -from typing import Any, Iterable, Sequence +from typing import Any, Iterable, Mapping, Sequence GIT_SHA_RE = re.compile(r"^[0-9a-f]{40}$") @@ -24,6 +24,7 @@ ) PR_EVENTS = frozenset({"pull_request", "pull_request_target"}) ACTIVE_STATUSES = ("queued", "in_progress") +API_TIMEOUT_SECONDS = 30 class CoalescingRefused(RuntimeError): @@ -35,6 +36,47 @@ def _positive_int(value: object) -> int | None: return value if type(value) is int and value > 0 else None +def _pull_request_associations(run_data: Mapping[str, Any]) -> list[dict[str, Any]]: + """Return only well-shaped pull-request associations from an Actions run.""" + value = run_data.get("pull_requests") + if not isinstance(value, list): + return [] + return [item for item in value if isinstance(item, dict)] + + +def _association_number(association: Mapping[str, Any]) -> int | None: + """Return one associated PR number when GitHub supplied a positive integer.""" + return _positive_int(association.get("number")) + + +def _head_tuple(value: Mapping[str, Any]) -> tuple[str, str, str]: + """Normalize a PR-style head object to repository, ref, and lowercase SHA.""" + repository = ((value.get("repo") or {}).get("full_name") or "") + ref = str(value.get("ref") or "") + sha = str(value.get("sha") or "").lower() + return repository, ref, sha + + +def _run_matches_head_identity( + run_data: Mapping[str, Any], *, repository: str, branch: str, head_sha: str +) -> bool: + """Match a run to the live PR head, including pull_request_target semantics.""" + event = run_data.get("event") + if event not in PR_EVENTS: + return False + if event == "pull_request": + if ( + str(run_data.get("head_sha") or "").lower() == head_sha + and run_data.get("head_branch") == branch + and ((run_data.get("head_repository") or {}).get("full_name") == repository) + ): + return True + for association in _pull_request_associations(run_data): + if _head_tuple(association.get("head") or {}) == (repository, branch, head_sha): + return True + return False + + def _run_identity_matches( run_data: dict[str, Any], *, @@ -44,10 +86,9 @@ def _run_identity_matches( ) -> bool: """Return whether one run belongs to the exact PR-head cancellation boundary.""" return ( - run_data.get("event") in PR_EVENTS - and str(run_data.get("head_sha") or "").lower() == head_sha - and run_data.get("head_branch") == branch - and ((run_data.get("head_repository") or {}).get("full_name") == repository) + _run_matches_head_identity( + run_data, repository=repository, branch=branch, head_sha=head_sha + ) and _positive_int(run_data.get("workflow_id")) is not None and _positive_int(run_data.get("id")) is not None and run_data.get("status") in ACTIVE_STATUSES @@ -61,14 +102,7 @@ def select_duplicate_queued_run_ids( branch: str, head_sha: str, ) -> list[int]: - """Select only redundant queued runs while retaining authoritative siblings. - - Runs are grouped by GitHub's stable numeric ``workflow_id`` after exact - repository/branch/head/event filtering. If a workflow already has an - in-progress run, every queued sibling is redundant. Otherwise the newest - queued run ID is retained and only older queued siblings are selected. - In-progress runs are never returned. - """ + """Select redundant queued runs while retaining one authoritative sibling.""" groups: dict[int, list[dict[str, Any]]] = {} for run_data in runs: if not _run_identity_matches( @@ -101,11 +135,56 @@ def select_duplicate_queued_run_ids( return sorted(redundant) +def _run_pr_scope_is_safe( + run_data: Mapping[str, Any], + *, + live_pr: Mapping[str, Any], + current_pr_number: int, + associated_prs: Mapping[int, Mapping[str, Any]], +) -> bool: + """Keep evidence isolated across live PRs while allowing closed predecessors.""" + associations = _pull_request_associations(run_data) + if not associations: + return False + live_repo, live_ref, live_sha = _head_tuple(live_pr.get("head") or {}) + live_base_ref = str(((live_pr.get("base") or {}).get("ref") or "")) + saw_current = False + saw_closed_predecessor = False + for association in associations: + number = _association_number(association) + if number is None: + return False + if _head_tuple(association.get("head") or {}) != (live_repo, live_ref, live_sha): + return False + if number == current_pr_number: + saw_current = True + continue + other = associated_prs.get(number) + if not isinstance(other, Mapping): + return False + if other.get("state") == "open": + return False + other_repo, other_ref, other_sha = _head_tuple(other.get("head") or {}) + other_base_ref = str(((other.get("base") or {}).get("ref") or "")) + if ( + other_repo != live_repo + or other_ref != live_ref + or other_sha != live_sha + or not live_base_ref + or other_base_ref != live_base_ref + ): + return False + saw_closed_predecessor = True + return saw_current or saw_closed_predecessor + + def validate_candidate_against_live_state( candidate: dict[str, Any], *, live_pr: dict[str, Any], active_same_head_runs: Sequence[dict[str, Any]], + current_pr_number: int | None = None, + associated_prs: Mapping[int, Mapping[str, Any]] | None = None, ) -> None: """Fail closed unless a queued candidate still has an authoritative sibling.""" if candidate.get("status") != "queued": @@ -113,18 +192,12 @@ def validate_candidate_against_live_state( if live_pr.get("state") != "open": raise CoalescingRefused("pull request is no longer open") - live_head = live_pr.get("head") or {} - live_repo = ((live_head.get("repo") or {}).get("full_name") or "") - live_ref = str(live_head.get("ref") or "") - live_sha = str(live_head.get("sha") or "").lower() - candidate_repo = ((candidate.get("head_repository") or {}).get("full_name") or "") - candidate_ref = str(candidate.get("head_branch") or "") - candidate_sha = str(candidate.get("head_sha") or "").lower() + live_repo, live_ref, live_sha = _head_tuple(live_pr.get("head") or {}) if ( not GIT_SHA_RE.fullmatch(live_sha) - or live_sha != candidate_sha - or live_ref != candidate_ref - or live_repo != candidate_repo + or not _run_matches_head_identity( + candidate, repository=live_repo, branch=live_ref, head_sha=live_sha + ) ): raise CoalescingRefused("pull request head moved after duplicate classification") @@ -135,6 +208,15 @@ def validate_candidate_against_live_state( if candidate.get("event") not in PR_EVENTS: raise CoalescingRefused("candidate is not a pull-request workflow run") + association_map = associated_prs or {} + if current_pr_number is not None and not _run_pr_scope_is_safe( + candidate, + live_pr=live_pr, + current_pr_number=current_pr_number, + associated_prs=association_map, + ): + raise CoalescingRefused("candidate belongs to an independent pull request") + authoritative_sibling = False for sibling in active_same_head_runs: sibling_id = _positive_int(sibling.get("id")) @@ -146,6 +228,13 @@ def validate_candidate_against_live_state( sibling, repository=live_repo, branch=live_ref, head_sha=live_sha ): continue + if current_pr_number is not None and not _run_pr_scope_is_safe( + sibling, + live_pr=live_pr, + current_pr_number=current_pr_number, + associated_prs=association_map, + ): + continue if sibling.get("status") == "in_progress" or sibling_id > candidate_id: authoritative_sibling = True break @@ -154,17 +243,21 @@ def validate_candidate_against_live_state( def _run_json(args: Sequence[str]) -> Any: - """Run one bounded GitHub CLI call and decode its JSON response.""" + """Run one token-bound GitHub CLI call with an individual request timeout.""" if not os.environ.get("GH_TOKEN"): raise RuntimeError("GH_TOKEN is required for current-head run coalescing") - completed = subprocess.run( - list(args), - capture_output=True, - text=True, - check=False, - shell=False, - env=os.environ.copy(), - ) + try: + completed = subprocess.run( + list(args), + capture_output=True, + text=True, + check=False, + shell=False, + env=os.environ.copy(), + timeout=API_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired as exc: + raise RuntimeError("GitHub API request timed out") from exc if completed.returncode != 0: diagnostic = (completed.stderr or completed.stdout or "GitHub API request failed").strip() raise RuntimeError(diagnostic[:600]) @@ -181,8 +274,8 @@ def _fetch_pr(repo: str, number: int) -> dict[str, Any]: return payload -def _active_runs(repo: str, head_sha: str) -> list[dict[str, Any]]: - """Fetch queued and in-progress runs for one exact commit SHA.""" +def _active_runs(repo: str, _head_sha: str) -> list[dict[str, Any]]: + """Fetch all queued/in-progress runs so pull_request_target runs are visible.""" runs: list[dict[str, Any]] = [] for status in ACTIVE_STATUSES: page = 1 @@ -196,8 +289,6 @@ def _active_runs(repo: str, head_sha: str) -> list[dict[str, Any]]: f"repos/{repo}/actions/runs", "-f", f"status={status}", - "-f", - f"head_sha={head_sha}", "-F", "per_page=100", "-F", @@ -231,18 +322,22 @@ def _fetch_run(repo: str, run_id: int) -> dict[str, Any]: def _cancel_run(repo: str, run_id: int) -> None: - """Cancel one queued duplicate using GitHub's ordinary cancellation endpoint.""" - completed = subprocess.run( - ["gh", "api", "-X", "POST", f"repos/{repo}/actions/runs/{run_id}/cancel"], - capture_output=True, - text=True, - check=False, - shell=False, - env=os.environ.copy(), - ) - if completed.returncode != 0: - diagnostic = (completed.stderr or completed.stdout or "GitHub cancellation failed").strip() - raise RuntimeError(diagnostic[:600]) + """Request ordinary cancellation using the same explicit token/timeout contract.""" + _run_json(["gh", "api", "-X", "POST", f"repos/{repo}/actions/runs/{run_id}/cancel"]) + + +def _associated_prs( + repo: str, runs: Sequence[Mapping[str, Any]], current_pr_number: int +) -> dict[int, dict[str, Any]]: + """Fetch non-current PR associations needed to prove closed-predecessor safety.""" + numbers = { + number + for run_data in runs + for association in _pull_request_associations(run_data) + if (number := _association_number(association)) is not None + and number != current_pr_number + } + return {number: _fetch_pr(repo, number) for number in sorted(numbers)} def coalesce(repo: str, number: int, expected_repo: str, expected_ref: str, expected_head: str) -> list[int]: @@ -255,12 +350,12 @@ def coalesce(repo: str, number: int, expected_repo: str, expected_ref: str, expe raise RuntimeError("pull-request identity is malformed") live_pr = _fetch_pr(repo, number) - live_head = live_pr.get("head") or {} + live_repo, live_ref, live_sha = _head_tuple(live_pr.get("head") or {}) if ( live_pr.get("state") != "open" - or str(live_head.get("sha") or "").lower() != expected_head - or live_head.get("ref") != expected_ref - or ((live_head.get("repo") or {}).get("full_name") != expected_repo) + or live_sha != expected_head + or live_ref != expected_ref + or live_repo != expected_repo ): raise CoalescingRefused("pull request head moved before duplicate classification") @@ -274,13 +369,17 @@ def coalesce(repo: str, number: int, expected_repo: str, expected_ref: str, expe cancelled: list[int] = [] for run_id in candidates: try: - candidate = _fetch_run(repo, run_id) current_pr = _fetch_pr(repo, number) active = _active_runs(repo, expected_head) + association_map = _associated_prs(repo, active, number) + current_pr = _fetch_pr(repo, number) + candidate = _fetch_run(repo, run_id) validate_candidate_against_live_state( candidate, live_pr=current_pr, active_same_head_runs=active, + current_pr_number=number, + associated_prs=association_map, ) _cancel_run(repo, run_id) except CoalescingRefused as exc: From 0bf1c47246f13dd531ff6e9aa4450bcb368051d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:59:55 +0900 Subject: [PATCH 12/28] fix(actions): harden coalescer trigger and shell boundary --- .../workflows/current-head-run-coalescer.yml | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/current-head-run-coalescer.yml b/.github/workflows/current-head-run-coalescer.yml index b0be3c8b23..a3c985a532 100644 --- a/.github/workflows/current-head-run-coalescer.yml +++ b/.github/workflows/current-head-run-coalescer.yml @@ -2,7 +2,7 @@ name: Current Head Run Coalescer on: pull_request_target: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] concurrency: group: current-head-run-coalescer-${{ github.repository }}-${{ github.event.pull_request.number }} @@ -28,10 +28,17 @@ jobs: - name: Retire redundant queued exact-head runs env: GH_TOKEN: ${{ github.token }} - run: >- - python3 scripts/ci/current_head_run_coalescer.py - --repo "${{ github.repository }}" - --pr-number "${{ github.event.pull_request.number }}" - --expected-head-repo "${{ github.event.pull_request.head.repo.full_name }}" - --expected-head-ref "${{ github.event.pull_request.head.ref }}" - --expected-head "${{ github.event.pull_request.head.sha }}" + COALESCE_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + EXPECTED_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + EXPECTED_HEAD_REF: ${{ github.event.pull_request.head.ref }} + EXPECTED_HEAD: ${{ github.event.pull_request.head.sha }} + shell: bash + run: | + set -euo pipefail + python3 scripts/ci/current_head_run_coalescer.py \ + --repo "$COALESCE_REPO" \ + --pr-number "$PR_NUMBER" \ + --expected-head-repo "$EXPECTED_HEAD_REPO" \ + --expected-head-ref "$EXPECTED_HEAD_REF" \ + --expected-head "$EXPECTED_HEAD" From 6a3a98bd585045b54ceace3a39aff0f2d210fb7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:00:53 +0900 Subject: [PATCH 13/28] test(actions): capture coalescer review regressions --- ...t_head_run_coalescer_review_regressions.py | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/test_current_head_run_coalescer_review_regressions.py diff --git a/tests/test_current_head_run_coalescer_review_regressions.py b/tests/test_current_head_run_coalescer_review_regressions.py new file mode 100644 index 0000000000..cbc4c1871a --- /dev/null +++ b/tests/test_current_head_run_coalescer_review_regressions.py @@ -0,0 +1,161 @@ +"""Review regressions for current-head GitHub Actions run coalescing.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path +from types import SimpleNamespace + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT = REPO_ROOT / "scripts" / "ci" / "current_head_run_coalescer.py" +WORKFLOW = REPO_ROOT / ".github" / "workflows" / "current-head-run-coalescer.yml" + + +def load_module(): + """Load the production coalescer from the current checkout.""" + spec = importlib.util.spec_from_file_location("current_head_run_coalescer_review", SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def pr_head(*, sha: str = "a" * 40, ref: str = "feature/current") -> dict[str, object]: + """Return one PR-style head identity.""" + return { + "sha": sha, + "ref": ref, + "repo": {"full_name": "ContextualWisdomLab/.github"}, + } + + +def live_pr(*, state: str = "open", base_ref: str = "main") -> dict[str, object]: + """Return one live PR identity with an explicit base boundary.""" + return { + "state": state, + "head": pr_head(), + "base": {"ref": base_ref, "sha": "b" * 40}, + } + + +def run_record( + run_id: int, + *, + status: str = "queued", + event: str = "pull_request", + top_head_sha: str = "a" * 40, + top_head_branch: str = "feature/current", + pr_number: int = 2, +) -> dict[str, object]: + """Return an Actions run with both workflow and associated-PR identities.""" + return { + "id": run_id, + "workflow_id": 10, + "status": status, + "event": event, + "head_sha": top_head_sha, + "head_branch": top_head_branch, + "head_repository": {"full_name": "ContextualWisdomLab/.github"}, + "pull_requests": [ + { + "number": pr_number, + "head": pr_head(), + "base": { + "ref": "main", + "sha": "b" * 40, + "repo": {"full_name": "ContextualWisdomLab/.github"}, + }, + } + ], + } + + +def test_pull_request_target_matches_associated_pr_head_not_trusted_base_head() -> None: + """Target-event runs bind to associated PR head rather than workflow base head.""" + module = load_module() + target_run = run_record( + 100, + event="pull_request_target", + top_head_sha="c" * 40, + top_head_branch="main", + ) + assert module._run_identity_matches( + target_run, + repository="ContextualWisdomLab/.github", + branch="feature/current", + head_sha="a" * 40, + ) + + +def test_distinct_open_pr_association_cannot_authorize_cross_pr_cancellation() -> None: + """An open sibling PR sharing one branch/SHA keeps its own workflow evidence.""" + module = load_module() + candidate = run_record(100, pr_number=1) + sibling = run_record(101, pr_number=2) + other_open_pr = live_pr(base_ref="develop") + with pytest.raises(module.CoalescingRefused, match="pull-request scope"): + module.validate_candidate_against_live_state( + candidate, + live_pr=live_pr(), + active_same_head_runs=[candidate, sibling], + current_pr_number=2, + associated_prs={1: other_open_pr}, + ) + + +def test_final_candidate_refetch_preserves_run_that_started_after_validation(monkeypatch) -> None: + """A queued candidate that starts before mutation is preserved on final re-fetch.""" + module = load_module() + queued = run_record(100) + started = run_record(100, status="in_progress") + sibling = run_record(101) + monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) + monkeypatch.setattr(module, "_active_runs", lambda *_args: [queued, sibling]) + fetches = iter([queued, started]) + monkeypatch.setattr(module, "_fetch_run", lambda *_args: next(fetches)) + cancelled: list[int] = [] + monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) + + assert module.coalesce( + "ContextualWisdomLab/.github", + 2, + "ContextualWisdomLab/.github", + "feature/current", + "a" * 40, + ) == [] + assert cancelled == [] + + +def test_transport_is_token_bound_and_individually_timeout_bounded(monkeypatch) -> None: + """Read and cancellation transports require GH_TOKEN and a per-call timeout.""" + module = load_module() + monkeypatch.delenv("GH_TOKEN", raising=False) + with pytest.raises(RuntimeError, match="GH_TOKEN"): + module._cancel_run("owner/repo", 123) + + monkeypatch.setenv("GH_TOKEN", "token") + calls: list[tuple[list[str], dict[str, object]]] = [] + + def success(args, **kwargs): + calls.append((list(args), dict(kwargs))) + stdout = "{}" if "/cancel" not in " ".join(args) else "" + return SimpleNamespace(returncode=0, stdout=stdout, stderr="") + + monkeypatch.setattr(module.subprocess, "run", success) + assert module._run_json(["gh", "api", "repos/owner/repo"]) == {} + module._cancel_run("owner/repo", 123) + assert len(calls) == 2 + assert all(call_kwargs.get("timeout") == module.API_TIMEOUT_SECONDS for _, call_kwargs in calls) + + +def test_workflow_covers_ready_transition_and_never_expands_head_ref_inside_shell() -> None: + """Ready events coalesce duplicates and untrusted refs cross the shell via env only.""" + text = WORKFLOW.read_text(encoding="utf-8") + assert "types: [opened, synchronize, reopened, ready_for_review]" in text + assert "EXPECTED_HEAD_REF: ${{ github.event.pull_request.head.ref }}" in text + run_block = text.split("run: >-", 1)[1] + assert '--expected-head-ref "$EXPECTED_HEAD_REF"' in run_block + assert 'github.event.pull_request.head.ref' not in run_block From 2a44049dc957815a6f2632e72b55d5a23bfdf776 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:01:44 +0900 Subject: [PATCH 14/28] test(actions): cover coalescer PR and race boundaries --- tests/test_current_head_run_coalescer.py | 266 ++++++++++++++--------- 1 file changed, 161 insertions(+), 105 deletions(-) diff --git a/tests/test_current_head_run_coalescer.py b/tests/test_current_head_run_coalescer.py index e4c082fb26..98705f284e 100644 --- a/tests/test_current_head_run_coalescer.py +++ b/tests/test_current_head_run_coalescer.py @@ -3,8 +3,8 @@ from __future__ import annotations import importlib.util -import json import runpy +import subprocess import sys from pathlib import Path from types import SimpleNamespace @@ -27,6 +27,22 @@ def load_module(): return module +def pr_association( + number: int = 1, + *, + head_sha: str = "a" * 40, + branch: str = "feature/current", + repository: str = "ContextualWisdomLab/.github", + base_ref: str = "main", +) -> dict[str, object]: + """Return one Actions run pull-request association fixture.""" + return { + "number": number, + "head": {"sha": head_sha, "ref": branch, "repo": {"full_name": repository}}, + "base": {"ref": base_ref, "sha": "c" * 40, "repo": {"full_name": repository}}, + } + + def run_record( run_id: int, workflow_id: int, @@ -36,41 +52,60 @@ def run_record( branch: str = "feature/current", repository: str = "ContextualWisdomLab/.github", event: str = "pull_request", + pr_number: int = 1, + execution_head_sha: str | None = None, + associations: list[dict[str, object]] | None = None, ) -> dict[str, object]: - """Return one bounded Actions run fixture.""" + """Return one bounded Actions run fixture with authoritative PR association.""" return { "id": run_id, "workflow_id": workflow_id, "status": status, - "head_sha": head_sha, + "head_sha": execution_head_sha or head_sha, "head_branch": branch, "event": event, "head_repository": {"full_name": repository}, + "pull_requests": associations + if associations is not None + else [ + pr_association( + pr_number, + head_sha=head_sha, + branch=branch, + repository=repository, + ) + ], } -def live_pr(*, state: str = "open", head_sha: str = "a" * 40) -> dict[str, object]: +def live_pr( + *, + state: str = "open", + head_sha: str = "a" * 40, + number: int = 1, + base_ref: str = "main", +) -> dict[str, object]: """Return the exact live PR identity used by revalidation tests.""" return { + "number": number, "state": state, "head": { "sha": head_sha, "ref": "feature/current", "repo": {"full_name": "ContextualWisdomLab/.github"}, }, + "base": { + "sha": "c" * 40, + "ref": base_ref, + "repo": {"full_name": "ContextualWisdomLab/.github"}, + }, } def test_select_duplicate_queued_runs_keeps_one_authoritative_run_per_workflow() -> None: """Older queued duplicates are retired while one exact-head run survives.""" module = load_module() - runs = [ - run_record(100, 10), - run_record(101, 10), - run_record(102, 10), - run_record(200, 20), - run_record(201, 20), - ] + runs = [run_record(100, 10), run_record(101, 10), run_record(102, 10), run_record(200, 20), run_record(201, 20)] assert module.select_duplicate_queued_run_ids( runs, repository="ContextualWisdomLab/.github", @@ -82,11 +117,7 @@ def test_select_duplicate_queued_runs_keeps_one_authoritative_run_per_workflow() def test_in_progress_run_is_never_selected_and_makes_queued_siblings_redundant() -> None: """A running authoritative workflow is preserved and queued duplicates retire.""" module = load_module() - runs = [ - run_record(100, 10, status="in_progress"), - run_record(101, 10), - run_record(102, 10), - ] + runs = [run_record(100, 10, status="in_progress"), run_record(101, 10), run_record(102, 10)] assert module.select_duplicate_queued_run_ids( runs, repository="ContextualWisdomLab/.github", @@ -95,6 +126,19 @@ def test_in_progress_run_is_never_selected_and_makes_queued_siblings_redundant() ) == [101, 102] +def test_pull_request_target_uses_associated_pr_head_not_execution_head() -> None: + """Trusted-base pull_request_target runs coalesce by their associated PR head.""" + module = load_module() + target = run_record(100, 10, event="pull_request_target", execution_head_sha="b" * 40) + newer = run_record(101, 10, event="pull_request_target", execution_head_sha="b" * 40) + assert module.select_duplicate_queued_run_ids( + [target, newer], + repository="ContextualWisdomLab/.github", + branch="feature/current", + head_sha="a" * 40, + ) == [100] + + def test_other_identities_and_malformed_runs_are_not_coalesced() -> None: """Coalescing stays inside one exact current-head pull-request workflow identity.""" module = load_module() @@ -119,6 +163,8 @@ def test_other_identities_and_malformed_runs_are_not_coalesced() -> None: assert module._positive_int("1") is None assert module._positive_int(0) is None assert module._positive_int(1) == 1 + assert module._pull_request_associations({"pull_requests": "bad"}) == [] + assert module._association_number({"number": "1"}) is None def test_revalidation_requires_a_distinct_newer_or_running_sibling() -> None: @@ -127,15 +173,9 @@ def test_revalidation_requires_a_distinct_newer_or_running_sibling() -> None: candidate = run_record(100, 10) for active in ([candidate], [candidate, run_record(99, 10)]): with pytest.raises(module.CoalescingRefused, match="authoritative sibling"): - module.validate_candidate_against_live_state( - candidate, - live_pr=live_pr(), - active_same_head_runs=active, - ) + module.validate_candidate_against_live_state(candidate, live_pr=live_pr(), active_same_head_runs=active) module.validate_candidate_against_live_state( - candidate, - live_pr=live_pr(), - active_same_head_runs=[candidate, run_record(101, 10)], + candidate, live_pr=live_pr(), active_same_head_runs=[candidate, run_record(101, 10)] ) module.validate_candidate_against_live_state( candidate, @@ -151,27 +191,50 @@ def test_revalidation_fails_closed_for_status_state_identity_and_event_changes() sibling = run_record(101, 10) with pytest.raises(module.CoalescingRefused, match="no longer queued"): module.validate_candidate_against_live_state( - run_record(100, 10, status="in_progress"), - live_pr=live_pr(), - active_same_head_runs=[sibling], + run_record(100, 10, status="in_progress"), live_pr=live_pr(), active_same_head_runs=[sibling] ) with pytest.raises(module.CoalescingRefused, match="no longer open"): - module.validate_candidate_against_live_state( - candidate, live_pr=live_pr(state="closed"), active_same_head_runs=[sibling] - ) + module.validate_candidate_against_live_state(candidate, live_pr=live_pr(state="closed"), active_same_head_runs=[sibling]) with pytest.raises(module.CoalescingRefused, match="head moved"): - module.validate_candidate_against_live_state( - candidate, live_pr=live_pr(head_sha="b" * 40), active_same_head_runs=[sibling] - ) - malformed = run_record(0, 10) + module.validate_candidate_against_live_state(candidate, live_pr=live_pr(head_sha="b" * 40), active_same_head_runs=[sibling]) with pytest.raises(module.CoalescingRefused, match="identity is malformed"): + module.validate_candidate_against_live_state(run_record(0, 10), live_pr=live_pr(), active_same_head_runs=[sibling]) + with pytest.raises(module.CoalescingRefused, match="not a pull-request"): + module.validate_candidate_against_live_state(run_record(100, 10, event="push"), live_pr=live_pr(), active_same_head_runs=[sibling]) + + +def test_pr_scope_rejects_other_open_pr_and_accepts_closed_matching_predecessor() -> None: + """Concurrent PRs keep independent evidence while a closed predecessor may coalesce.""" + module = load_module() + current = live_pr() + other_assoc = [pr_association(2)] + candidate = run_record(100, 10, pr_number=2, associations=other_assoc) + sibling = run_record(101, 10) + other_open = live_pr(number=2) + with pytest.raises(module.CoalescingRefused, match="independent pull request"): module.validate_candidate_against_live_state( - malformed, live_pr=live_pr(), active_same_head_runs=[sibling] + candidate, + live_pr=current, + active_same_head_runs=[candidate, sibling], + current_pr_number=1, + associated_prs={2: other_open}, ) - wrong_event = run_record(100, 10, event="push") - with pytest.raises(module.CoalescingRefused, match="not a pull-request"): + other_closed = live_pr(state="closed", number=2) + module.validate_candidate_against_live_state( + candidate, + live_pr=current, + active_same_head_runs=[candidate, sibling], + current_pr_number=1, + associated_prs={2: other_closed}, + ) + wrong_base = live_pr(state="closed", number=2, base_ref="release") + with pytest.raises(module.CoalescingRefused, match="independent pull request"): module.validate_candidate_against_live_state( - wrong_event, live_pr=live_pr(), active_same_head_runs=[sibling] + candidate, + live_pr=current, + active_same_head_runs=[candidate, sibling], + current_pr_number=1, + associated_prs={2: wrong_base}, ) @@ -179,32 +242,35 @@ def test_revalidation_ignores_non_authoritative_sibling_shapes() -> None: """Different workflow or malformed sibling records cannot authorize cancellation.""" module = load_module() candidate = run_record(100, 10) - siblings = [ - run_record(101, 11), - run_record(102, 10, branch="other"), - run_record(0, 10), - candidate, - ] + siblings = [run_record(101, 11), run_record(102, 10, branch="other"), run_record(0, 10), candidate] with pytest.raises(module.CoalescingRefused, match="authoritative sibling"): - module.validate_candidate_against_live_state( - candidate, live_pr=live_pr(), active_same_head_runs=siblings - ) + module.validate_candidate_against_live_state(candidate, live_pr=live_pr(), active_same_head_runs=siblings) -def test_run_json_uses_token_decodes_success_and_bounds_failure(monkeypatch) -> None: - """GitHub transport is token-bound, JSON-only, and bounded on command failure.""" +def test_run_json_uses_token_timeout_decodes_success_and_bounds_failure(monkeypatch) -> None: + """GitHub transport is token-bound, JSON-only, individually timed, and bounded.""" module = load_module() monkeypatch.delenv("GH_TOKEN", raising=False) with pytest.raises(RuntimeError, match="GH_TOKEN"): module._run_json(["gh", "api", "repos/o/r"]) monkeypatch.setenv("GH_TOKEN", "token") - monkeypatch.setattr( - module.subprocess, - "run", - lambda *args, **kwargs: SimpleNamespace(returncode=0, stdout='{"ok":true}', stderr=""), - ) + seen: dict[str, object] = {} + + def success(*args, **kwargs): + seen.update(kwargs) + return SimpleNamespace(returncode=0, stdout='{"ok":true}', stderr="") + + monkeypatch.setattr(module.subprocess, "run", success) assert module._run_json(["gh", "api", "repos/o/r"]) == {"ok": True} + assert seen["timeout"] == module.API_TIMEOUT_SECONDS + + def timeout(*_args, **_kwargs): + raise subprocess.TimeoutExpired(cmd="gh", timeout=30) + + monkeypatch.setattr(module.subprocess, "run", timeout) + with pytest.raises(RuntimeError, match="timed out"): + module._run_json(["gh", "api", "repos/o/r"]) monkeypatch.setattr( module.subprocess, @@ -245,36 +311,36 @@ def pages(args): monkeypatch.setattr(module, "_run_json", pages) assert len(module._active_runs("o/r", "a" * 40)) == 101 assert any("page=2" in call for call in calls) + assert not any(item.startswith("head_sha=") for call in calls for item in call) monkeypatch.setattr(module, "_run_json", lambda _args: {"workflow_runs": "bad"}) with pytest.raises(RuntimeError, match="malformed Actions"): module._active_runs("o/r", "a" * 40) -def test_cancel_run_uses_ordinary_endpoint_and_surfaces_failure(monkeypatch) -> None: - """Only GitHub's ordinary cancellation endpoint is used for queued duplicates.""" +def test_cancel_run_uses_explicit_transport_and_ordinary_endpoint(monkeypatch) -> None: + """Cancellation shares the token/timeout transport and never uses force-cancel.""" module = load_module() calls: list[list[str]] = [] - - def success(args, **_kwargs): - calls.append(list(args)) - return SimpleNamespace(returncode=0, stdout="", stderr="") - - monkeypatch.setattr(module.subprocess, "run", success) + monkeypatch.setattr(module, "_run_json", lambda args: calls.append(list(args))) module._cancel_run("o/r", 123) assert calls == [["gh", "api", "-X", "POST", "repos/o/r/actions/runs/123/cancel"]] + assert "force-cancel" not in " ".join(calls[0]) - monkeypatch.setattr( - module.subprocess, - "run", - lambda *args, **kwargs: SimpleNamespace(returncode=1, stdout="failed", stderr=""), - ) - with pytest.raises(RuntimeError, match="failed"): - module._cancel_run("o/r", 123) + +def test_associated_pr_fetches_only_noncurrent_numbers(monkeypatch) -> None: + """Closed-predecessor validation fetches only distinct non-current PRs.""" + module = load_module() + calls: list[int] = [] + monkeypatch.setattr(module, "_fetch_pr", lambda _repo, number: calls.append(number) or live_pr(number=number, state="closed")) + runs = [run_record(100, 10), run_record(101, 10, pr_number=2), run_record(102, 10, pr_number=2)] + result = module._associated_prs("o/r", runs, 1) + assert list(result) == [2] + assert calls == [2] def test_coalesce_validates_inputs_rechecks_each_candidate_and_preserves_races(monkeypatch, capsys) -> None: - """The mutation path revalidates live state per candidate and tolerates a disappearing sibling.""" + """The mutation path revalidates live state per candidate and preserves races.""" module = load_module() for repo in ("../evil", "owner/..", "owner/repo/extra"): with pytest.raises(RuntimeError, match="repository identity"): @@ -288,13 +354,7 @@ def test_coalesce_validates_inputs_rechecks_each_candidate_and_preserves_races(m monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr(head_sha="b" * 40)) with pytest.raises(module.CoalescingRefused, match="moved before"): - module.coalesce( - "ContextualWisdomLab/.github", - 1, - "ContextualWisdomLab/.github", - "feature/current", - "a" * 40, - ) + module.coalesce("ContextualWisdomLab/.github", 1, "ContextualWisdomLab/.github", "feature/current", "a" * 40) candidate = run_record(100, 10) sibling = run_record(101, 10) @@ -304,17 +364,25 @@ def test_coalesce_validates_inputs_rechecks_each_candidate_and_preserves_races(m monkeypatch.setattr(module, "_fetch_run", lambda *_args: candidate) cancelled: list[int] = [] monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) - assert module.coalesce( - "ContextualWisdomLab/.github", - 1, - "ContextualWisdomLab/.github", - "feature/current", - "a" * 40, - ) == [] + assert module.coalesce("ContextualWisdomLab/.github", 1, "ContextualWisdomLab/.github", "feature/current", "a" * 40) == [] assert cancelled == [] assert "Preserving run 100" in capsys.readouterr().out +def test_coalesce_refetches_candidate_last_and_preserves_started_run(monkeypatch) -> None: + """A candidate that starts after sibling validation is not cancelled.""" + module = load_module() + candidate = run_record(100, 10) + sibling = run_record(101, 10) + monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) + monkeypatch.setattr(module, "_active_runs", lambda *_args: [candidate, sibling]) + monkeypatch.setattr(module, "_fetch_run", lambda *_args: run_record(100, 10, status="in_progress")) + cancelled: list[int] = [] + monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) + assert module.coalesce("ContextualWisdomLab/.github", 1, "ContextualWisdomLab/.github", "feature/current", "a" * 40) == [] + assert cancelled == [] + + def test_coalesce_cancels_only_revalidated_redundant_candidates(monkeypatch, capsys) -> None: """A proven older queued duplicate is cancelled and reported exactly once.""" module = load_module() @@ -325,13 +393,7 @@ def test_coalesce_cancels_only_revalidated_redundant_candidates(monkeypatch, cap monkeypatch.setattr(module, "_fetch_run", lambda *_args: candidate) cancelled: list[int] = [] monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) - assert module.coalesce( - "ContextualWisdomLab/.github", - 1, - "ContextualWisdomLab/.github", - "feature/current", - "a" * 40, - ) == [100] + assert module.coalesce("ContextualWisdomLab/.github", 1, "ContextualWisdomLab/.github", "feature/current", "a" * 40) == [100] assert cancelled == [100] assert "Cancelled redundant queued current-head run 100" in capsys.readouterr().out @@ -340,16 +402,8 @@ def test_parse_args_main_and_script_help(monkeypatch) -> None: """CLI parsing forwards exact identity and the executable entrypoint is reachable.""" module = load_module() argv = [ - "--repo", - "owner/repo", - "--pr-number", - "7", - "--expected-head-repo", - "owner/repo", - "--expected-head-ref", - "feature/current", - "--expected-head", - "a" * 40, + "--repo", "owner/repo", "--pr-number", "7", "--expected-head-repo", "owner/repo", + "--expected-head-ref", "feature/current", "--expected-head", "a" * 40, ] parsed = module.parse_args(argv) assert parsed.pr_number == 7 @@ -365,11 +419,11 @@ def test_parse_args_main_and_script_help(monkeypatch) -> None: def test_workflow_is_trusted_pr_target_with_minimum_actions_write() -> None: - """The production workflow uses trusted source and the smallest mutation scope.""" + """The production workflow uses trusted source and a shell-safe mutation scope.""" assert WORKFLOW.is_file(), "current-head duplicate coalescer workflow is not implemented" text = WORKFLOW.read_text(encoding="utf-8") assert "pull_request_target:" in text - assert "types: [opened, synchronize, reopened]" in text + assert "types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]" in text assert "actions: write" in text assert "contents: read" in text assert "pull-requests: read" in text @@ -377,5 +431,7 @@ def test_workflow_is_trusted_pr_target_with_minimum_actions_write() -> None: assert "ref: ${{ github.workflow_sha }}" in text assert "current_head_run_coalescer.py" in text assert "cancel-in-progress: true" in text - assert "github.event.pull_request.number" in text - assert "github.event.pull_request.head.sha" in text + assert "EXPECTED_HEAD_REF: ${{ github.event.pull_request.head.ref }}" in text + assert '--expected-head-ref "$EXPECTED_HEAD_REF"' in text + run_block = text.split("run: |", 1)[1] + assert "${{ github.event.pull_request.head.ref }}" not in run_block From 94d3082e16bfa7cb95ba72e0c8f8ee10d65dd722 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:02:12 +0900 Subject: [PATCH 15/28] docs(actions): record coalescer race and PR boundaries --- docs/doctoring/current-head-run-coalescing.md | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/doctoring/current-head-run-coalescing.md b/docs/doctoring/current-head-run-coalescing.md index 2ad5659f7f..40c5e4a788 100644 --- a/docs/doctoring/current-head-run-coalescing.md +++ b/docs/doctoring/current-head-run-coalescing.md @@ -2,39 +2,45 @@ ## Incident -On 2026-09-02, exact head `09908aaf56e568420105b81434c6cdd147856657` was reused when Draft pull request #1050 was closed and ready successor #1643 was opened. GitHub exposed two simultaneously queued runs for several expensive workflows on that unchanged branch/head, including Security Scan (`33561053485`, `33561076062`), CodeQL PR (`33561053137`, `33561076168`), Python Security (`33561053333`, `33561076150`), and SAST Semgrep (`33561053180`, `33561076360`). Equivalent duplicate pairs existed for Secret Scan, SBOM Generation, Scorecard PR, and OSV-Scanner PR. +On 2026-09-02 KST (2026-09-01 UTC), exact head `09908aaf56e568420105b81434c6cdd147856657` was reused when Draft pull request #1050 was closed and ready successor #1643 was opened. GitHub exposed two simultaneously queued runs for several expensive workflows on that unchanged branch/head, including Security Scan (`33561053485`, `33561076062`), CodeQL PR (`33561053137`, `33561076168`), Python Security (`33561053333`, `33561076150`), and SAST Semgrep (`33561053180`, `33561076360`). Equivalent duplicate pairs existed for Secret Scan, SBOM Generation, Scorecard PR, and OSV-Scanner PR. The live-ref queue-hygiene repair from #1348 correctly prevents stale pull-request payloads from cancelling a newly pushed authoritative head. Its destructive revalidation intentionally preserves any run whose `head_sha` still equals the live branch ref. That safety invariant does not distinguish the sole authoritative current-head run from redundant queued siblings belonging to the same GitHub `workflow_id`. PR recreation therefore exposed a second, orthogonal capacity leak: safe stale-head preservation could retain several same-workflow runs for one current head. ## Trust boundary -`.github/workflows/current-head-run-coalescer.yml` executes only on `pull_request_target` `opened`, `synchronize`, and `reopened`. It checks out `ContextualWisdomLab/.github` at immutable `github.workflow_sha` with persisted credentials disabled. The job has only `actions: write`, `contents: read`, and `pull-requests: read`; it never checks out or executes the pull-request head. +`.github/workflows/current-head-run-coalescer.yml` executes on trusted `pull_request_target` events for `opened`, `synchronize`, `reopened`, `ready_for_review`, and `converted_to_draft`. It checks out `ContextualWisdomLab/.github` at immutable `github.workflow_sha` with persisted credentials disabled. The job has only `actions: write`, `contents: read`, and `pull-requests: read`; it never checks out or executes pull-request-head code. Event-derived repository/ref/SHA values are first placed in environment variables and are referenced from the shell only as quoted variables, so PR-controlled branch names are never interpolated directly into executable shell text. -The workflow passes the event's repository, PR number, head repository, head ref, and lowercase 40-character head SHA into `scripts/ci/current_head_run_coalescer.py`. The script immediately re-fetches the live PR before classification. Before every cancellation it re-fetches the candidate run, the live PR, and active runs for the exact head again. Missing, malformed, moved, closed, or ambiguous evidence preserves the candidate. +The script re-fetches the live PR before classification. It lists all queued and in-progress repository runs rather than filtering only by workflow-run `head_sha`, because `pull_request_target` runs execute on the trusted base and their workflow head is not the PR head. Those runs are instead bound to the associated pull request's head identity. Before every cancellation the script re-fetches the current PR, active siblings, any non-current PR associations, and finally the candidate itself. Missing, malformed, moved, closed, timed-out, or ambiguous evidence preserves the candidate or fails closed. + +## Pull-request isolation + +A workflow run may authorize cancellation only inside the current PR's evidence boundary. Runs associated with the current PR are eligible when their associated head matches the current live repository/ref/SHA. A run associated with a different **open** PR never authorizes or receives cancellation, even when both PRs share the same branch and commit; those PRs retain independent required-check evidence. A run left behind by a **closed** predecessor may be coalesced into a successor only when the predecessor's live head repository/ref/SHA and base ref match the successor. This preserves the #1050-to-#1643 recreation repair without allowing two simultaneously open PRs to cancel each other's checks. ## Cancellation invariant Runs are eligible only when all of the following are true: -1. the run was triggered by `pull_request` or `pull_request_target`; -2. its head repository, branch, and SHA exactly match the live open PR; -3. its stable numeric `workflow_id` matches another active exact-head sibling; -4. the candidate is still `queued` immediately before mutation; and +1. the run was triggered by `pull_request` or `pull_request_target` and is bound to the current live PR head through the correct event-specific identity; +2. its PR association belongs either to the current PR or to a proven closed predecessor with the same head identity and base ref; +3. its stable numeric `workflow_id` matches another active run inside the same PR evidence boundary; +4. the candidate is still `queued` on the final candidate fetch immediately before mutation; and 5. a distinct authoritative sibling is still active: either an `in_progress` sibling or a newer queued sibling. -The coalescer never selects an `in_progress` run. If a workflow already has an in-progress run, only queued siblings are redundant. If every matching run is queued, the greatest run ID is retained and older queued siblings are candidates. A candidate for which the authoritative sibling disappears is preserved. Cancellation uses GitHub's ordinary `/cancel` endpoint rather than `force-cancel`. +The coalescer never selects an `in_progress` run. If a workflow already has an in-progress run, only queued siblings are redundant. If every matching run is queued, the greatest run ID is retained and older queued siblings are candidates. A candidate for which the authoritative sibling disappears is preserved. Cancellation uses GitHub's ordinary `/cancel` endpoint rather than `force-cancel` and shares the same explicit `GH_TOKEN` and per-request timeout contract as every other API call. + +GitHub's REST cancellation endpoint has no conditional `If-Status-Is-Queued` precondition and acknowledges cancellation asynchronously. Therefore no client can make the final GET and POST literally atomic. The implementation closes the controllable race by performing the candidate GET last, after PR/sibling/association validation, and requiring `queued` immediately before the ordinary cancellation POST. The regression suite covers a candidate that changes from queued to in-progress before that final fetch and proves it is preserved. The residual sub-request race between the final GET and GitHub processing the POST is an upstream API limitation; the coalescer never uses force-cancel and does not claim stronger atomicity than the platform exposes. -This invariant is deliberately separate from old-head cancellation. #1348 remains authoritative for resolving live Git refs before retiring superseded heads; the coalescer handles only redundant queued evidence on the same live head. +This invariant is deliberately separate from old-head cancellation. #1348 remains authoritative for resolving live Git refs before retiring superseded heads; the coalescer handles only redundant active evidence for one live PR head. ## Executable evidence -`tests/test_current_head_run_coalescer.py` pins the source and workflow contract. The regression was committed before either production file existed, so the initial expected failure was the absent coalescer implementation. The final cases cover one-run retention, in-progress preservation, isolation across workflow/head/branch/repository/event, sole-run preservation, moved-head/status fail-closed behavior, trusted-source checkout, PR-stable concurrency, and minimum workflow permissions. +`tests/test_current_head_run_coalescer.py` pins the source and workflow contract. Coverage includes one-run retention, in-progress preservation, `pull_request_target` base/head separation, isolation between concurrently open PRs, safe closed-predecessor succession, workflow/head/branch/repository/event isolation, moved-head/status fail-closed behavior, per-call timeouts, explicit cancellation authentication, complete pagination, final candidate re-fetch, ready/draft transition triggers, trusted-source checkout, shell-injection resistance, PR-stable concurrency, and minimum workflow permissions. A one-use read-only branch workflow was attempted solely to capture hosted RED/GREEN evidence; GitHub did not schedule newly introduced branch-only push workflows in this repository, so no hosted result is claimed from that mechanism and it was deleted from the publishable tree. Ordinary protected PR checks and independent review on the exact production head remain authoritative. ## Recovery and rollback -If the coalescer reports unexpected preservation, inspect the live PR/run/sibling identities before changing policy. Do not weaken the exact-head or authoritative-sibling checks to improve cancellation volume. If a false cancellation is ever observed, disable the `current-head-run-coalescer.yml` trigger first while retaining #1348 stale-head queue hygiene, then reproduce the identity race with a deterministic regression before repair. +If the coalescer reports unexpected preservation, inspect the live PR/run/sibling identities before changing policy. Do not weaken exact-head, PR-association, base-ref, final-status, or authoritative-sibling checks to improve cancellation volume. If a false cancellation is ever observed, disable the `current-head-run-coalescer.yml` trigger first while retaining #1348 stale-head queue hygiene, then reproduce the identity race with a deterministic regression before repair. The feature is operability-only: it does not convert cancelled, queued, missing, stale, or predecessor evidence into passing merge evidence, and it does not change required-check, security, review, or branch-protection policy. From 195155a4da2557d2e61215668c79a0f0f7a95059 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:03:33 +0900 Subject: [PATCH 16/28] test(actions): align coalescer review regressions with repaired flow --- ...rrent_head_run_coalescer_review_regressions.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_current_head_run_coalescer_review_regressions.py b/tests/test_current_head_run_coalescer_review_regressions.py index cbc4c1871a..3f02005d1d 100644 --- a/tests/test_current_head_run_coalescer_review_regressions.py +++ b/tests/test_current_head_run_coalescer_review_regressions.py @@ -96,7 +96,7 @@ def test_distinct_open_pr_association_cannot_authorize_cross_pr_cancellation() - candidate = run_record(100, pr_number=1) sibling = run_record(101, pr_number=2) other_open_pr = live_pr(base_ref="develop") - with pytest.raises(module.CoalescingRefused, match="pull-request scope"): + with pytest.raises(module.CoalescingRefused, match="independent pull request"): module.validate_candidate_against_live_state( candidate, live_pr=live_pr(), @@ -106,16 +106,15 @@ def test_distinct_open_pr_association_cannot_authorize_cross_pr_cancellation() - ) -def test_final_candidate_refetch_preserves_run_that_started_after_validation(monkeypatch) -> None: - """A queued candidate that starts before mutation is preserved on final re-fetch.""" +def test_final_candidate_fetch_preserves_run_that_started_after_snapshot(monkeypatch) -> None: + """A queued snapshot candidate that starts before final mutation is preserved.""" module = load_module() queued = run_record(100) started = run_record(100, status="in_progress") sibling = run_record(101) monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) monkeypatch.setattr(module, "_active_runs", lambda *_args: [queued, sibling]) - fetches = iter([queued, started]) - monkeypatch.setattr(module, "_fetch_run", lambda *_args: next(fetches)) + monkeypatch.setattr(module, "_fetch_run", lambda *_args: started) cancelled: list[int] = [] monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) @@ -154,8 +153,10 @@ def success(args, **kwargs): def test_workflow_covers_ready_transition_and_never_expands_head_ref_inside_shell() -> None: """Ready events coalesce duplicates and untrusted refs cross the shell via env only.""" text = WORKFLOW.read_text(encoding="utf-8") - assert "types: [opened, synchronize, reopened, ready_for_review]" in text + trigger_line = next(line.strip() for line in text.splitlines() if line.strip().startswith("types:")) + for event_name in ("opened", "synchronize", "reopened", "ready_for_review"): + assert event_name in trigger_line assert "EXPECTED_HEAD_REF: ${{ github.event.pull_request.head.ref }}" in text - run_block = text.split("run: >-", 1)[1] + run_block = text.split("run: |", 1)[1] assert '--expected-head-ref "$EXPECTED_HEAD_REF"' in run_block assert 'github.event.pull_request.head.ref' not in run_block From c9b45f5f6ef175eea58287a57e1827a5172ea87f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:07:32 +0900 Subject: [PATCH 17/28] test(actions): require exact base identity for predecessor coalescing --- ...t_head_run_coalescer_review_regressions.py | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/tests/test_current_head_run_coalescer_review_regressions.py b/tests/test_current_head_run_coalescer_review_regressions.py index 3f02005d1d..e29937c404 100644 --- a/tests/test_current_head_run_coalescer_review_regressions.py +++ b/tests/test_current_head_run_coalescer_review_regressions.py @@ -32,12 +32,18 @@ def pr_head(*, sha: str = "a" * 40, ref: str = "feature/current") -> dict[str, o } -def live_pr(*, state: str = "open", base_ref: str = "main") -> dict[str, object]: - """Return one live PR identity with an explicit base boundary.""" +def live_pr( + *, + state: str = "open", + base_ref: str = "main", + base_sha: str = "b" * 40, + base_repo: str = "ContextualWisdomLab/.github", +) -> dict[str, object]: + """Return one live PR identity with an explicit exact base boundary.""" return { "state": state, "head": pr_head(), - "base": {"ref": base_ref, "sha": "b" * 40}, + "base": {"ref": base_ref, "sha": base_sha, "repo": {"full_name": base_repo}}, } @@ -49,6 +55,9 @@ def run_record( top_head_sha: str = "a" * 40, top_head_branch: str = "feature/current", pr_number: int = 2, + base_ref: str = "main", + base_sha: str = "b" * 40, + base_repo: str = "ContextualWisdomLab/.github", ) -> dict[str, object]: """Return an Actions run with both workflow and associated-PR identities.""" return { @@ -64,9 +73,9 @@ def run_record( "number": pr_number, "head": pr_head(), "base": { - "ref": "main", - "sha": "b" * 40, - "repo": {"full_name": "ContextualWisdomLab/.github"}, + "ref": base_ref, + "sha": base_sha, + "repo": {"full_name": base_repo}, }, } ], @@ -106,6 +115,38 @@ def test_distinct_open_pr_association_cannot_authorize_cross_pr_cancellation() - ) +def test_closed_predecessor_must_share_exact_base_sha_and_repository() -> None: + """A closed predecessor on a different base snapshot cannot donate required evidence.""" + module = load_module() + current = live_pr() + sibling = run_record(101, pr_number=2) + + candidate_old_base = run_record(100, pr_number=1, base_sha="c" * 40) + predecessor_old_base = live_pr(state="closed", base_sha="c" * 40) + with pytest.raises(module.CoalescingRefused, match="independent pull request"): + module.validate_candidate_against_live_state( + candidate_old_base, + live_pr=current, + active_same_head_runs=[candidate_old_base, sibling], + current_pr_number=2, + associated_prs={1: predecessor_old_base}, + ) + + candidate_other_repo = run_record(100, pr_number=1, base_repo="ContextualWisdomLab/TEPP") + predecessor_other_repo = live_pr( + state="closed", + base_repo="ContextualWisdomLab/TEPP", + ) + with pytest.raises(module.CoalescingRefused, match="independent pull request"): + module.validate_candidate_against_live_state( + candidate_other_repo, + live_pr=current, + active_same_head_runs=[candidate_other_repo, sibling], + current_pr_number=2, + associated_prs={1: predecessor_other_repo}, + ) + + def test_final_candidate_fetch_preserves_run_that_started_after_snapshot(monkeypatch) -> None: """A queued snapshot candidate that starts before final mutation is preserved.""" module = load_module() From a067b7b23414d316cfdea967a396a4b9b47f7be9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:08:01 +0900 Subject: [PATCH 18/28] fix(actions): bound predecessor lookups to live head --- scripts/ci/current_head_run_coalescer.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/ci/current_head_run_coalescer.py b/scripts/ci/current_head_run_coalescer.py index 3567219e4d..a2d8267c30 100644 --- a/scripts/ci/current_head_run_coalescer.py +++ b/scripts/ci/current_head_run_coalescer.py @@ -327,12 +327,21 @@ def _cancel_run(repo: str, run_id: int) -> None: def _associated_prs( - repo: str, runs: Sequence[Mapping[str, Any]], current_pr_number: int + repo: str, + runs: Sequence[Mapping[str, Any]], + current_pr_number: int, + *, + repository: str, + branch: str, + head_sha: str, ) -> dict[int, dict[str, Any]]: - """Fetch non-current PR associations needed to prove closed-predecessor safety.""" + """Fetch only same-head non-current PR associations needed for predecessor proof.""" numbers = { number for run_data in runs + if _run_matches_head_identity( + run_data, repository=repository, branch=branch, head_sha=head_sha + ) for association in _pull_request_associations(run_data) if (number := _association_number(association)) is not None and number != current_pr_number @@ -371,7 +380,14 @@ def coalesce(repo: str, number: int, expected_repo: str, expected_ref: str, expe try: current_pr = _fetch_pr(repo, number) active = _active_runs(repo, expected_head) - association_map = _associated_prs(repo, active, number) + association_map = _associated_prs( + repo, + active, + number, + repository=expected_repo, + branch=expected_ref, + head_sha=expected_head, + ) current_pr = _fetch_pr(repo, number) candidate = _fetch_run(repo, run_id) validate_candidate_against_live_state( From e13978bca4918ec51be7a045521ead6452dab356 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:08:51 +0900 Subject: [PATCH 19/28] test(actions): bound predecessor lookup scope --- tests/test_current_head_run_coalescer.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_current_head_run_coalescer.py b/tests/test_current_head_run_coalescer.py index 98705f284e..23752bc11d 100644 --- a/tests/test_current_head_run_coalescer.py +++ b/tests/test_current_head_run_coalescer.py @@ -328,13 +328,25 @@ def test_cancel_run_uses_explicit_transport_and_ordinary_endpoint(monkeypatch) - assert "force-cancel" not in " ".join(calls[0]) -def test_associated_pr_fetches_only_noncurrent_numbers(monkeypatch) -> None: - """Closed-predecessor validation fetches only distinct non-current PRs.""" +def test_associated_pr_fetches_only_same_head_noncurrent_numbers(monkeypatch) -> None: + """Predecessor lookup ignores unrelated active runs and fetches each same-head PR once.""" module = load_module() calls: list[int] = [] monkeypatch.setattr(module, "_fetch_pr", lambda _repo, number: calls.append(number) or live_pr(number=number, state="closed")) - runs = [run_record(100, 10), run_record(101, 10, pr_number=2), run_record(102, 10, pr_number=2)] - result = module._associated_prs("o/r", runs, 1) + runs = [ + run_record(100, 10), + run_record(101, 10, pr_number=2), + run_record(102, 10, pr_number=2), + run_record(103, 10, pr_number=999, head_sha="b" * 40), + ] + result = module._associated_prs( + "o/r", + runs, + 1, + repository="ContextualWisdomLab/.github", + branch="feature/current", + head_sha="a" * 40, + ) assert list(result) == [2] assert calls == [2] From 02669d610b653c1c3a69ae6eb364bcd1f2fe634e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:09:43 +0900 Subject: [PATCH 20/28] fix(actions): isolate coalescing by exact base identity --- scripts/ci/current_head_run_coalescer.py | 32 ++++++++++++++---------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/scripts/ci/current_head_run_coalescer.py b/scripts/ci/current_head_run_coalescer.py index a2d8267c30..a433d8f6c0 100644 --- a/scripts/ci/current_head_run_coalescer.py +++ b/scripts/ci/current_head_run_coalescer.py @@ -57,6 +57,14 @@ def _head_tuple(value: Mapping[str, Any]) -> tuple[str, str, str]: return repository, ref, sha +def _base_tuple(value: Mapping[str, Any]) -> tuple[str, str, str]: + """Normalize a PR-style base object to repository, ref, and lowercase SHA.""" + repository = ((value.get("repo") or {}).get("full_name") or "") + ref = str(value.get("ref") or "") + sha = str(value.get("sha") or "").lower() + return repository, ref, sha + + def _run_matches_head_identity( run_data: Mapping[str, Any], *, repository: str, branch: str, head_sha: str ) -> bool: @@ -142,19 +150,23 @@ def _run_pr_scope_is_safe( current_pr_number: int, associated_prs: Mapping[int, Mapping[str, Any]], ) -> bool: - """Keep evidence isolated across live PRs while allowing closed predecessors.""" + """Keep evidence isolated across live PRs while allowing exact closed predecessors.""" associations = _pull_request_associations(run_data) if not associations: return False - live_repo, live_ref, live_sha = _head_tuple(live_pr.get("head") or {}) - live_base_ref = str(((live_pr.get("base") or {}).get("ref") or "")) + live_head = _head_tuple(live_pr.get("head") or {}) + live_base = _base_tuple(live_pr.get("base") or {}) + if not all(live_head) or not all(live_base) or not GIT_SHA_RE.fullmatch(live_base[2]): + return False saw_current = False saw_closed_predecessor = False for association in associations: number = _association_number(association) if number is None: return False - if _head_tuple(association.get("head") or {}) != (live_repo, live_ref, live_sha): + if _head_tuple(association.get("head") or {}) != live_head: + return False + if _base_tuple(association.get("base") or {}) != live_base: return False if number == current_pr_number: saw_current = True @@ -164,15 +176,9 @@ def _run_pr_scope_is_safe( return False if other.get("state") == "open": return False - other_repo, other_ref, other_sha = _head_tuple(other.get("head") or {}) - other_base_ref = str(((other.get("base") or {}).get("ref") or "")) - if ( - other_repo != live_repo - or other_ref != live_ref - or other_sha != live_sha - or not live_base_ref - or other_base_ref != live_base_ref - ): + if _head_tuple(other.get("head") or {}) != live_head: + return False + if _base_tuple(other.get("base") or {}) != live_base: return False saw_closed_predecessor = True return saw_current or saw_closed_predecessor From 71c5b0b552427083dfc266c65ca9f0f06df87a64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:10:42 +0900 Subject: [PATCH 21/28] test(actions): refresh authoritative sibling before cancellation --- ...t_head_run_coalescer_review_regressions.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_current_head_run_coalescer_review_regressions.py b/tests/test_current_head_run_coalescer_review_regressions.py index e29937c404..41077776f1 100644 --- a/tests/test_current_head_run_coalescer_review_regressions.py +++ b/tests/test_current_head_run_coalescer_review_regressions.py @@ -169,6 +169,34 @@ def test_final_candidate_fetch_preserves_run_that_started_after_snapshot(monkeyp assert cancelled == [] +def test_authoritative_sibling_is_refetched_and_must_still_be_active(monkeypatch) -> None: + """A sibling that completed after the bulk snapshot cannot justify cancellation.""" + module = load_module() + candidate = run_record(100) + stale_sibling = run_record(101) + completed_sibling = run_record(101, status="completed") + monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) + monkeypatch.setattr(module, "_active_runs", lambda *_args: [candidate, stale_sibling]) + + def fetch_run(_repo: str, run_id: int): + if run_id == 101: + return completed_sibling + return candidate + + monkeypatch.setattr(module, "_fetch_run", fetch_run) + cancelled: list[int] = [] + monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) + + assert module.coalesce( + "ContextualWisdomLab/.github", + 2, + "ContextualWisdomLab/.github", + "feature/current", + "a" * 40, + ) == [] + assert cancelled == [] + + def test_transport_is_token_bound_and_individually_timeout_bounded(monkeypatch) -> None: """Read and cancellation transports require GH_TOKEN and a per-call timeout.""" module = load_module() From 00ad2282cc1c588d7d8fbbf64e37f9c27d30ec4a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:11:48 +0900 Subject: [PATCH 22/28] fix(actions): refresh authoritative sibling before cancellation --- scripts/ci/current_head_run_coalescer.py | 47 +++++++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/scripts/ci/current_head_run_coalescer.py b/scripts/ci/current_head_run_coalescer.py index a433d8f6c0..c1bf48acbc 100644 --- a/scripts/ci/current_head_run_coalescer.py +++ b/scripts/ci/current_head_run_coalescer.py @@ -355,6 +355,42 @@ def _associated_prs( return {number: _fetch_pr(repo, number) for number in sorted(numbers)} +def _refresh_siblings( + repo: str, + runs: Sequence[Mapping[str, Any]], + candidate_run_id: int, + *, + repository: str, + branch: str, + head_sha: str, +) -> list[dict[str, Any]]: + """Re-fetch candidate peers so stale bulk state cannot authorize cancellation.""" + candidate_snapshot = next( + ( + run_data + for run_data in runs + if _positive_int(run_data.get("id")) == candidate_run_id + ), + None, + ) + if candidate_snapshot is None: + return [] + workflow_id = _positive_int(candidate_snapshot.get("workflow_id")) + if workflow_id is None: + return [] + sibling_ids = sorted( + sibling_run_id + for run_data in runs + if _positive_int(run_data.get("workflow_id")) == workflow_id + and _run_identity_matches( + dict(run_data), repository=repository, branch=branch, head_sha=head_sha + ) + and (sibling_run_id := _positive_int(run_data.get("id"))) is not None + and sibling_run_id != candidate_run_id + ) + return [_fetch_run(repo, sibling_run_id) for sibling_run_id in sibling_ids] + + def coalesce(repo: str, number: int, expected_repo: str, expected_ref: str, expected_head: str) -> list[int]: """Cancel redundant queued runs after exact live PR/run/sibling revalidation.""" if not REPOSITORY_RE.fullmatch(repo) or not REPOSITORY_RE.fullmatch(expected_repo): @@ -384,7 +420,6 @@ def coalesce(repo: str, number: int, expected_repo: str, expected_ref: str, expe cancelled: list[int] = [] for run_id in candidates: try: - current_pr = _fetch_pr(repo, number) active = _active_runs(repo, expected_head) association_map = _associated_prs( repo, @@ -394,12 +429,20 @@ def coalesce(repo: str, number: int, expected_repo: str, expected_ref: str, expe branch=expected_ref, head_sha=expected_head, ) + refreshed_siblings = _refresh_siblings( + repo, + active, + run_id, + repository=expected_repo, + branch=expected_ref, + head_sha=expected_head, + ) current_pr = _fetch_pr(repo, number) candidate = _fetch_run(repo, run_id) validate_candidate_against_live_state( candidate, live_pr=current_pr, - active_same_head_runs=active, + active_same_head_runs=refreshed_siblings, current_pr_number=number, associated_prs=association_map, ) From 626395930919fdddd044ec0e42b7e7c78fbb794d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:13:02 +0900 Subject: [PATCH 23/28] test(actions): cover refreshed sibling evidence --- tests/test_current_head_run_coalescer.py | 35 ++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/test_current_head_run_coalescer.py b/tests/test_current_head_run_coalescer.py index 23752bc11d..33eee98d6c 100644 --- a/tests/test_current_head_run_coalescer.py +++ b/tests/test_current_head_run_coalescer.py @@ -351,6 +351,33 @@ def test_associated_pr_fetches_only_same_head_noncurrent_numbers(monkeypatch) -> assert calls == [2] +def test_refresh_siblings_refetches_only_same_workflow_head_peers(monkeypatch) -> None: + """Sibling refresh is bounded to exact-head peers and fails closed without a candidate.""" + module = load_module() + candidate = run_record(100, 10) + sibling = run_record(101, 10) + other_workflow = run_record(102, 11) + other_head = run_record(103, 10, head_sha="b" * 40) + assert module._refresh_siblings( + "o/r", [sibling], 100, repository="ContextualWisdomLab/.github", branch="feature/current", head_sha="a" * 40 + ) == [] + assert module._refresh_siblings( + "o/r", [{**candidate, "workflow_id": 0}], 100, repository="ContextualWisdomLab/.github", branch="feature/current", head_sha="a" * 40 + ) == [] + calls: list[int] = [] + monkeypatch.setattr(module, "_fetch_run", lambda _repo, run_id: calls.append(run_id) or sibling) + refreshed = module._refresh_siblings( + "o/r", + [candidate, sibling, other_workflow, other_head], + 100, + repository="ContextualWisdomLab/.github", + branch="feature/current", + head_sha="a" * 40, + ) + assert [item["id"] for item in refreshed] == [101] + assert calls == [101] + + def test_coalesce_validates_inputs_rechecks_each_candidate_and_preserves_races(monkeypatch, capsys) -> None: """The mutation path revalidates live state per candidate and preserves races.""" module = load_module() @@ -388,7 +415,11 @@ def test_coalesce_refetches_candidate_last_and_preserves_started_run(monkeypatch sibling = run_record(101, 10) monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) monkeypatch.setattr(module, "_active_runs", lambda *_args: [candidate, sibling]) - monkeypatch.setattr(module, "_fetch_run", lambda *_args: run_record(100, 10, status="in_progress")) + + def fetch_run(_repo: str, run_id: int): + return sibling if run_id == 101 else run_record(100, 10, status="in_progress") + + monkeypatch.setattr(module, "_fetch_run", fetch_run) cancelled: list[int] = [] monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) assert module.coalesce("ContextualWisdomLab/.github", 1, "ContextualWisdomLab/.github", "feature/current", "a" * 40) == [] @@ -402,7 +433,7 @@ def test_coalesce_cancels_only_revalidated_redundant_candidates(monkeypatch, cap sibling = run_record(101, 10) monkeypatch.setattr(module, "_fetch_pr", lambda *_args: live_pr()) monkeypatch.setattr(module, "_active_runs", lambda *_args: [candidate, sibling]) - monkeypatch.setattr(module, "_fetch_run", lambda *_args: candidate) + monkeypatch.setattr(module, "_fetch_run", lambda _repo, run_id: sibling if run_id == 101 else candidate) cancelled: list[int] = [] monkeypatch.setattr(module, "_cancel_run", lambda _repo, run_id: cancelled.append(run_id)) assert module.coalesce("ContextualWisdomLab/.github", 1, "ContextualWisdomLab/.github", "feature/current", "a" * 40) == [100] From e958d4212c83ca792398dc83d89f50f9f39a7b11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:13:41 +0900 Subject: [PATCH 24/28] docs(actions): record exact-base and sibling-refresh boundary --- docs/doctoring/current-head-run-coalescing.md | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/doctoring/current-head-run-coalescing.md b/docs/doctoring/current-head-run-coalescing.md index 40c5e4a788..4ed12cca71 100644 --- a/docs/doctoring/current-head-run-coalescing.md +++ b/docs/doctoring/current-head-run-coalescing.md @@ -10,37 +10,38 @@ The live-ref queue-hygiene repair from #1348 correctly prevents stale pull-reque `.github/workflows/current-head-run-coalescer.yml` executes on trusted `pull_request_target` events for `opened`, `synchronize`, `reopened`, `ready_for_review`, and `converted_to_draft`. It checks out `ContextualWisdomLab/.github` at immutable `github.workflow_sha` with persisted credentials disabled. The job has only `actions: write`, `contents: read`, and `pull-requests: read`; it never checks out or executes pull-request-head code. Event-derived repository/ref/SHA values are first placed in environment variables and are referenced from the shell only as quoted variables, so PR-controlled branch names are never interpolated directly into executable shell text. -The script re-fetches the live PR before classification. It lists all queued and in-progress repository runs rather than filtering only by workflow-run `head_sha`, because `pull_request_target` runs execute on the trusted base and their workflow head is not the PR head. Those runs are instead bound to the associated pull request's head identity. Before every cancellation the script re-fetches the current PR, active siblings, any non-current PR associations, and finally the candidate itself. Missing, malformed, moved, closed, timed-out, or ambiguous evidence preserves the candidate or fails closed. +The script re-fetches the live PR before classification. It lists all queued and in-progress repository runs rather than filtering only by workflow-run `head_sha`, because `pull_request_target` runs execute on the trusted base and their workflow head is not the PR head. Those runs are instead bound to the associated pull request's head identity. Before every cancellation the script re-fetches active same-head state, exact non-current PR associations, each possible same-workflow authoritative sibling, the current PR, and finally the candidate itself. Missing, malformed, moved, closed, completed, timed-out, or ambiguous evidence preserves the candidate or fails closed. ## Pull-request isolation -A workflow run may authorize cancellation only inside the current PR's evidence boundary. Runs associated with the current PR are eligible when their associated head matches the current live repository/ref/SHA. A run associated with a different **open** PR never authorizes or receives cancellation, even when both PRs share the same branch and commit; those PRs retain independent required-check evidence. A run left behind by a **closed** predecessor may be coalesced into a successor only when the predecessor's live head repository/ref/SHA and base ref match the successor. This preserves the #1050-to-#1643 recreation repair without allowing two simultaneously open PRs to cancel each other's checks. +A workflow run may authorize cancellation only inside the current PR's evidence boundary. Runs associated with the current PR are eligible only when both their associated head and base match the current live PR exactly. A run associated with a different **open** PR never authorizes or receives cancellation, even when both PRs share the same branch and commit; those PRs retain independent required-check evidence. A run left behind by a **closed** predecessor may be coalesced into a successor only when both the run association and the predecessor's live record match the successor's exact head repository/ref/SHA **and exact base repository/ref/SHA**. A predecessor from an older base commit is therefore not interchangeable with the successor even when the base branch name is unchanged. This preserves the #1050-to-#1643 recreation repair only when the required-workflow evidence really represents the same merge boundary. ## Cancellation invariant Runs are eligible only when all of the following are true: 1. the run was triggered by `pull_request` or `pull_request_target` and is bound to the current live PR head through the correct event-specific identity; -2. its PR association belongs either to the current PR or to a proven closed predecessor with the same head identity and base ref; -3. its stable numeric `workflow_id` matches another active run inside the same PR evidence boundary; -4. the candidate is still `queued` on the final candidate fetch immediately before mutation; and -5. a distinct authoritative sibling is still active: either an `in_progress` sibling or a newer queued sibling. +2. its PR association belongs either to the current PR or to a proven closed predecessor with the same exact head and exact base repository/ref/SHA identity; +3. its stable numeric `workflow_id` matches another run inside the same PR evidence boundary; +4. each candidate authoritative sibling identified from the bulk Actions snapshot is re-fetched by exact run ID and must still be queued or in progress with the same workflow/head/PR scope; +5. the current PR is re-fetched after sibling refresh and still exposes the same exact head/base boundary; and +6. the candidate is still `queued` on the final exact-run fetch immediately before mutation, while at least one refreshed distinct authoritative sibling remains active: either an `in_progress` sibling or a newer queued sibling. -The coalescer never selects an `in_progress` run. If a workflow already has an in-progress run, only queued siblings are redundant. If every matching run is queued, the greatest run ID is retained and older queued siblings are candidates. A candidate for which the authoritative sibling disappears is preserved. Cancellation uses GitHub's ordinary `/cancel` endpoint rather than `force-cancel` and shares the same explicit `GH_TOKEN` and per-request timeout contract as every other API call. +The coalescer never selects an observed `in_progress` run. If a workflow already has an in-progress run, only queued siblings are redundant. If every matching run is queued, the greatest run ID is retained and older queued siblings are candidates. A candidate for which the authoritative sibling disappears, completes, changes identity, or becomes otherwise non-authoritative during refresh is preserved. Cancellation uses GitHub's ordinary `/cancel` endpoint rather than `force-cancel` and shares the same explicit `GH_TOKEN` and per-request timeout contract as every other API call. -GitHub's REST cancellation endpoint has no conditional `If-Status-Is-Queued` precondition and acknowledges cancellation asynchronously. Therefore no client can make the final GET and POST literally atomic. The implementation closes the controllable race by performing the candidate GET last, after PR/sibling/association validation, and requiring `queued` immediately before the ordinary cancellation POST. The regression suite covers a candidate that changes from queued to in-progress before that final fetch and proves it is preserved. The residual sub-request race between the final GET and GitHub processing the POST is an upstream API limitation; the coalescer never uses force-cancel and does not claim stronger atomicity than the platform exposes. +GitHub's REST cancellation endpoint has no conditional `If-Status-Is-Queued` precondition and acknowledges cancellation asynchronously. Therefore no client can make the final GET and POST literally atomic. The implementation closes the controllable races by re-fetching the specific authoritative sibling(s), then the current PR, then performing the candidate GET last and requiring `queued` immediately before the ordinary cancellation POST. The regression suite covers both a candidate that changes from queued to in-progress and an authoritative sibling that becomes completed after the bulk snapshot; in both cases the candidate is preserved. The residual sub-request race after the final GETs is an upstream API limitation; the coalescer never uses force-cancel and does not claim stronger atomicity than the platform exposes. This invariant is deliberately separate from old-head cancellation. #1348 remains authoritative for resolving live Git refs before retiring superseded heads; the coalescer handles only redundant active evidence for one live PR head. ## Executable evidence -`tests/test_current_head_run_coalescer.py` pins the source and workflow contract. Coverage includes one-run retention, in-progress preservation, `pull_request_target` base/head separation, isolation between concurrently open PRs, safe closed-predecessor succession, workflow/head/branch/repository/event isolation, moved-head/status fail-closed behavior, per-call timeouts, explicit cancellation authentication, complete pagination, final candidate re-fetch, ready/draft transition triggers, trusted-source checkout, shell-injection resistance, PR-stable concurrency, and minimum workflow permissions. +`tests/test_current_head_run_coalescer.py` and `tests/test_current_head_run_coalescer_review_regressions.py` pin the source and workflow contract. Coverage includes one-run retention, in-progress preservation, `pull_request_target` base/head separation, isolation between concurrently open PRs, exact-base isolation across closed predecessor succession, same-workflow sibling re-fetch, completed-sibling preservation, workflow/head/branch/repository/event isolation, moved-head/status fail-closed behavior, per-call timeouts, explicit cancellation authentication, complete pagination, final candidate re-fetch, ready/draft transition triggers, trusted-source checkout, shell-injection resistance, PR-stable concurrency, and minimum workflow permissions. A one-use read-only branch workflow was attempted solely to capture hosted RED/GREEN evidence; GitHub did not schedule newly introduced branch-only push workflows in this repository, so no hosted result is claimed from that mechanism and it was deleted from the publishable tree. Ordinary protected PR checks and independent review on the exact production head remain authoritative. ## Recovery and rollback -If the coalescer reports unexpected preservation, inspect the live PR/run/sibling identities before changing policy. Do not weaken exact-head, PR-association, base-ref, final-status, or authoritative-sibling checks to improve cancellation volume. If a false cancellation is ever observed, disable the `current-head-run-coalescer.yml` trigger first while retaining #1348 stale-head queue hygiene, then reproduce the identity race with a deterministic regression before repair. +If the coalescer reports unexpected preservation, inspect the live PR/run/sibling identities before changing policy. Do not weaken exact-head, exact-base, PR-association, final-status, refreshed-sibling, or authoritative-sibling checks to improve cancellation volume. If a false cancellation is ever observed, disable the `current-head-run-coalescer.yml` trigger first while retaining #1348 stale-head queue hygiene, then reproduce the identity race with a deterministic regression before repair. The feature is operability-only: it does not convert cancelled, queued, missing, stale, or predecessor evidence into passing merge evidence, and it does not change required-check, security, review, or branch-protection policy. From 1deea6f2597b1479aead1733f7963b1e348f604c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:18:59 +0900 Subject: [PATCH 25/28] test(actions): reproduce minimal workflow-run repository associations --- ...t_head_run_coalescer_review_regressions.py | 68 +++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/tests/test_current_head_run_coalescer_review_regressions.py b/tests/test_current_head_run_coalescer_review_regressions.py index 41077776f1..ec4b0da589 100644 --- a/tests/test_current_head_run_coalescer_review_regressions.py +++ b/tests/test_current_head_run_coalescer_review_regressions.py @@ -23,12 +23,32 @@ def load_module(): return module -def pr_head(*, sha: str = "a" * 40, ref: str = "feature/current") -> dict[str, object]: +def full_repo(name: str = "ContextualWisdomLab/.github") -> dict[str, object]: + """Return the full repository shape emitted by the pull-request endpoint.""" + return {"id": 1274066402, "full_name": name} + + +def minimal_repo(name: str = "ContextualWisdomLab/.github") -> dict[str, object]: + """Return the minimal repository shape embedded in Actions run PR associations.""" + owner, repository = name.split("/", 1) + return { + "id": 1274066402, + "name": repository, + "url": f"https://api.github.com/repos/{owner}/{repository}", + } + + +def pr_head( + *, + sha: str = "a" * 40, + ref: str = "feature/current", + repository: dict[str, object] | None = None, +) -> dict[str, object]: """Return one PR-style head identity.""" return { "sha": sha, "ref": ref, - "repo": {"full_name": "ContextualWisdomLab/.github"}, + "repo": repository or full_repo(), } @@ -43,7 +63,7 @@ def live_pr( return { "state": state, "head": pr_head(), - "base": {"ref": base_ref, "sha": base_sha, "repo": {"full_name": base_repo}}, + "base": {"ref": base_ref, "sha": base_sha, "repo": full_repo(base_repo)}, } @@ -58,8 +78,11 @@ def run_record( base_ref: str = "main", base_sha: str = "b" * 40, base_repo: str = "ContextualWisdomLab/.github", + minimal_association: bool = False, ) -> dict[str, object]: """Return an Actions run with both workflow and associated-PR identities.""" + association_repo = minimal_repo() if minimal_association else full_repo() + association_base_repo = minimal_repo(base_repo) if minimal_association else full_repo(base_repo) return { "id": run_id, "workflow_id": 10, @@ -67,21 +90,53 @@ def run_record( "event": event, "head_sha": top_head_sha, "head_branch": top_head_branch, - "head_repository": {"full_name": "ContextualWisdomLab/.github"}, + "head_repository": full_repo(), "pull_requests": [ { "number": pr_number, - "head": pr_head(), + "head": pr_head(repository=association_repo), "base": { "ref": base_ref, "sha": base_sha, - "repo": {"full_name": base_repo}, + "repo": association_base_repo, }, } ], } +def test_real_actions_repository_shape_normalizes_to_pull_request_identity() -> None: + """Minimal Actions associations normalize to the same repository name as live PRs.""" + module = load_module() + minimal_head = pr_head(repository=minimal_repo()) + assert module._head_tuple(minimal_head) == ( + "ContextualWisdomLab/.github", + "feature/current", + "a" * 40, + ) + minimal_base = {"ref": "main", "sha": "b" * 40, "repo": minimal_repo()} + assert module._base_tuple(minimal_base) == ( + "ContextualWisdomLab/.github", + "main", + "b" * 40, + ) + + +def test_minimal_actions_associations_pass_exact_scope_for_both_pr_events() -> None: + """Real Actions association shapes remain eligible for PR and target-event coalescing.""" + module = load_module() + for event in ("pull_request", "pull_request_target"): + candidate = run_record(100, event=event, minimal_association=True) + sibling = run_record(101, event=event, minimal_association=True) + module.validate_candidate_against_live_state( + candidate, + live_pr=live_pr(), + active_same_head_runs=[candidate, sibling], + current_pr_number=2, + associated_prs={}, + ) + + def test_pull_request_target_matches_associated_pr_head_not_trusted_base_head() -> None: """Target-event runs bind to associated PR head rather than workflow base head.""" module = load_module() @@ -90,6 +145,7 @@ def test_pull_request_target_matches_associated_pr_head_not_trusted_base_head() event="pull_request_target", top_head_sha="c" * 40, top_head_branch="main", + minimal_association=True, ) assert module._run_identity_matches( target_run, From bb0c69d9deb762795bee0b3ab67dd154e8cf83db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:20:05 +0900 Subject: [PATCH 26/28] test(actions): bound repository-shape normalization --- ...t_head_run_coalescer_review_regressions.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_current_head_run_coalescer_review_regressions.py b/tests/test_current_head_run_coalescer_review_regressions.py index ec4b0da589..6203241c50 100644 --- a/tests/test_current_head_run_coalescer_review_regressions.py +++ b/tests/test_current_head_run_coalescer_review_regressions.py @@ -122,6 +122,30 @@ def test_real_actions_repository_shape_normalizes_to_pull_request_identity() -> ) +@pytest.mark.parametrize( + ("repository_shape", "expected"), + [ + (None, ""), + (full_repo(), "ContextualWisdomLab/.github"), + ({"full_name": "bad", "url": minimal_repo()["url"]}, ""), + ({}, ""), + ({"url": 7}, ""), + ({"url": "http://api.github.com/repos/ContextualWisdomLab/.github"}, ""), + ({"url": "https://example.com/repos/ContextualWisdomLab/.github"}, ""), + ({"url": "https://api.github.com/repos/ContextualWisdomLab/.github?x=1"}, ""), + ({"url": "https://api.github.com/repos/ContextualWisdomLab"}, ""), + ({"url": "https://api.github.com/repos/../.github"}, ""), + (minimal_repo(), "ContextualWisdomLab/.github"), + ], +) +def test_repository_shape_normalization_fails_closed( + repository_shape: object, expected: str +) -> None: + """Repository normalization accepts only full names or canonical GitHub API URLs.""" + module = load_module() + assert module._repository_full_name(repository_shape) == expected + + def test_minimal_actions_associations_pass_exact_scope_for_both_pr_events() -> None: """Real Actions association shapes remain eligible for PR and target-event coalescing.""" module = load_module() From 74cf4989bd305a76ab18fa85acee73fc7b80d9c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:21:07 +0900 Subject: [PATCH 27/28] fix(actions): normalize minimal Actions repository identities --- scripts/ci/current_head_run_coalescer.py | 36 ++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/scripts/ci/current_head_run_coalescer.py b/scripts/ci/current_head_run_coalescer.py index c1bf48acbc..cb0fadeea6 100644 --- a/scripts/ci/current_head_run_coalescer.py +++ b/scripts/ci/current_head_run_coalescer.py @@ -16,6 +16,7 @@ import re import subprocess from typing import Any, Iterable, Mapping, Sequence +from urllib.parse import urlsplit GIT_SHA_RE = re.compile(r"^[0-9a-f]{40}$") @@ -49,9 +50,38 @@ def _association_number(association: Mapping[str, Any]) -> int | None: return _positive_int(association.get("number")) +def _repository_full_name(value: object) -> str: + """Normalize full and Actions-embedded repository objects to ``owner/name``.""" + if not isinstance(value, Mapping): + return "" + full_name = value.get("full_name") + if full_name is not None: + return ( + full_name + if isinstance(full_name, str) and REPOSITORY_RE.fullmatch(full_name) + else "" + ) + api_url = value.get("url") + if not isinstance(api_url, str): + return "" + parsed = urlsplit(api_url) + if ( + parsed.scheme != "https" + or parsed.netloc != "api.github.com" + or parsed.query + or parsed.fragment + ): + return "" + parts = parsed.path.split("/") + if len(parts) != 4 or parts[0] != "" or parts[1] != "repos": + return "" + candidate = f"{parts[2]}/{parts[3]}" + return candidate if REPOSITORY_RE.fullmatch(candidate) else "" + + def _head_tuple(value: Mapping[str, Any]) -> tuple[str, str, str]: """Normalize a PR-style head object to repository, ref, and lowercase SHA.""" - repository = ((value.get("repo") or {}).get("full_name") or "") + repository = _repository_full_name(value.get("repo")) ref = str(value.get("ref") or "") sha = str(value.get("sha") or "").lower() return repository, ref, sha @@ -59,7 +89,7 @@ def _head_tuple(value: Mapping[str, Any]) -> tuple[str, str, str]: def _base_tuple(value: Mapping[str, Any]) -> tuple[str, str, str]: """Normalize a PR-style base object to repository, ref, and lowercase SHA.""" - repository = ((value.get("repo") or {}).get("full_name") or "") + repository = _repository_full_name(value.get("repo")) ref = str(value.get("ref") or "") sha = str(value.get("sha") or "").lower() return repository, ref, sha @@ -76,7 +106,7 @@ def _run_matches_head_identity( if ( str(run_data.get("head_sha") or "").lower() == head_sha and run_data.get("head_branch") == branch - and ((run_data.get("head_repository") or {}).get("full_name") == repository) + and _repository_full_name(run_data.get("head_repository")) == repository ): return True for association in _pull_request_associations(run_data): From f6b7e07eb38d5cd7db147d5e506a3e6e0f4bcae7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:22:19 +0900 Subject: [PATCH 28/28] docs(actions): record minimal Actions repository shape --- docs/doctoring/current-head-run-coalescing.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/current-head-run-coalescing.md b/docs/doctoring/current-head-run-coalescing.md index 4ed12cca71..45eb7f1bca 100644 --- a/docs/doctoring/current-head-run-coalescing.md +++ b/docs/doctoring/current-head-run-coalescing.md @@ -10,7 +10,9 @@ The live-ref queue-hygiene repair from #1348 correctly prevents stale pull-reque `.github/workflows/current-head-run-coalescer.yml` executes on trusted `pull_request_target` events for `opened`, `synchronize`, `reopened`, `ready_for_review`, and `converted_to_draft`. It checks out `ContextualWisdomLab/.github` at immutable `github.workflow_sha` with persisted credentials disabled. The job has only `actions: write`, `contents: read`, and `pull-requests: read`; it never checks out or executes pull-request-head code. Event-derived repository/ref/SHA values are first placed in environment variables and are referenced from the shell only as quoted variables, so PR-controlled branch names are never interpolated directly into executable shell text. -The script re-fetches the live PR before classification. It lists all queued and in-progress repository runs rather than filtering only by workflow-run `head_sha`, because `pull_request_target` runs execute on the trusted base and their workflow head is not the PR head. Those runs are instead bound to the associated pull request's head identity. Before every cancellation the script re-fetches active same-head state, exact non-current PR associations, each possible same-workflow authoritative sibling, the current PR, and finally the candidate itself. Missing, malformed, moved, closed, completed, timed-out, or ambiguous evidence preserves the candidate or fails closed. +The script re-fetches the live PR before classification. It lists all queued and in-progress repository runs rather than filtering only by workflow-run `head_sha`, because `pull_request_target` runs execute on the trusted base and their workflow head is not the PR head. Those runs are instead bound to the associated pull request's head identity. GitHub exposes repository identity in two different trusted REST shapes: the pull-request endpoint supplies a full repository object with `full_name`, while workflow-run `pull_requests[*].head.repo` and `base.repo` associations can contain only `id`, `name`, and canonical `https://api.github.com/repos/{owner}/{repo}` URL. `_repository_full_name()` therefore normalizes a valid full name directly or derives `owner/name` only from an exact HTTPS `api.github.com/repos/...` URL; malformed, query-bearing, foreign-host, non-HTTPS, or path-sentinel identities fail closed. This prevents a missing `full_name` field from turning every real workflow-run association into an empty repository identity while retaining a narrow authenticated GitHub boundary. + +Before every cancellation the script re-fetches active same-head state, exact non-current PR associations, each possible same-workflow authoritative sibling, the current PR, and finally the candidate itself. Missing, malformed, moved, closed, completed, timed-out, or ambiguous evidence preserves the candidate or fails closed. ## Pull-request isolation @@ -35,13 +37,15 @@ This invariant is deliberately separate from old-head cancellation. #1348 remain ## Executable evidence -`tests/test_current_head_run_coalescer.py` and `tests/test_current_head_run_coalescer_review_regressions.py` pin the source and workflow contract. Coverage includes one-run retention, in-progress preservation, `pull_request_target` base/head separation, isolation between concurrently open PRs, exact-base isolation across closed predecessor succession, same-workflow sibling re-fetch, completed-sibling preservation, workflow/head/branch/repository/event isolation, moved-head/status fail-closed behavior, per-call timeouts, explicit cancellation authentication, complete pagination, final candidate re-fetch, ready/draft transition triggers, trusted-source checkout, shell-injection resistance, PR-stable concurrency, and minimum workflow permissions. +`tests/test_current_head_run_coalescer.py` and `tests/test_current_head_run_coalescer_review_regressions.py` pin the source and workflow contract. Coverage includes one-run retention, in-progress preservation, `pull_request_target` base/head separation, real minimal Actions repository-association normalization for both PR event families, fail-closed repository URL normalization, isolation between concurrently open PRs, exact-base isolation across closed predecessor succession, same-workflow sibling re-fetch, completed-sibling preservation, workflow/head/branch/repository/event isolation, moved-head/status fail-closed behavior, per-call timeouts, explicit cancellation authentication, complete pagination, final candidate re-fetch, ready/draft transition triggers, trusted-source checkout, shell-injection resistance, PR-stable concurrency, and minimum workflow permissions. + +The minimal-repository-shape regression was committed before the production normalization repair. On the pre-fix source `_head_tuple()` read only `repo.full_name`, so the real Actions fixture deterministically normalized to an empty repository string. Production now accepts the fuller pull-request representation and the minimal workflow-run representation through the same bounded owner/name normalization contract. A one-use read-only branch workflow was attempted solely to capture hosted RED/GREEN evidence; GitHub did not schedule newly introduced branch-only push workflows in this repository, so no hosted result is claimed from that mechanism and it was deleted from the publishable tree. Ordinary protected PR checks and independent review on the exact production head remain authoritative. ## Recovery and rollback -If the coalescer reports unexpected preservation, inspect the live PR/run/sibling identities before changing policy. Do not weaken exact-head, exact-base, PR-association, final-status, refreshed-sibling, or authoritative-sibling checks to improve cancellation volume. If a false cancellation is ever observed, disable the `current-head-run-coalescer.yml` trigger first while retaining #1348 stale-head queue hygiene, then reproduce the identity race with a deterministic regression before repair. +If the coalescer reports unexpected preservation, inspect the live PR/run/sibling identities before changing policy. Do not weaken repository normalization, exact-head, exact-base, PR-association, final-status, refreshed-sibling, or authoritative-sibling checks to improve cancellation volume. If a false cancellation is ever observed, disable the `current-head-run-coalescer.yml` trigger first while retaining #1348 stale-head queue hygiene, then reproduce the identity race with a deterministic regression before repair. The feature is operability-only: it does not convert cancelled, queued, missing, stale, or predecessor evidence into passing merge evidence, and it does not change required-check, security, review, or branch-protection policy.