From aad55ed864db3466d52d24dcd88a80d15e84996d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 16:00:04 +0900 Subject: [PATCH 1/3] fix(codeql): serialize exact dispatch wakeups --- .github/workflows/codeql-scan-dispatch.yml | 70 +++++++++++-------- AGENTS.md | 5 ++ CHANGELOG.md | 5 ++ CLAUDE.md | 4 ++ ...odeql-dispatch-multi-language-wake-race.md | 34 +++++++++ ..._codeql_scan_dispatch_workflow_contract.py | 55 ++++++++------- 6 files changed, 118 insertions(+), 55 deletions(-) create mode 100644 docs/doctoring/codeql-dispatch-multi-language-wake-race.md diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index c94fdf55c2..3fa0c1d519 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -513,15 +513,24 @@ jobs: echo "::error::Could not publish the CodeQL dispatch status after all configured credentials failed; the exact required job will remain failed and will not be woken with stale or missing evidence." exit 1 - - name: Wake exact CodeQL required job - if: >- - always() - && steps.publish_status.outcome == 'success' - && needs.validate-dispatch.outputs.target_repository != '' - && needs.validate-dispatch.outputs.pr_number != '' - && needs.validate-dispatch.outputs.head_sha != '' - && needs.validate-dispatch.outputs.required_run_id != '' - && needs.validate-dispatch.outputs.required_jobs != '' + wake-exact-required-jobs: + name: Wake exact CodeQL required jobs + needs: [validate-dispatch, scan] + if: >- + always() + && needs.validate-dispatch.outputs.target_repository != '' + && needs.validate-dispatch.outputs.pr_number != '' + && needs.validate-dispatch.outputs.head_sha != '' + && needs.validate-dispatch.outputs.required_run_id != '' + && needs.validate-dispatch.outputs.required_jobs != '' + && needs.scan.result == 'success' + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + actions: write + contents: read + steps: + - name: Wake exact CodeQL required jobs env: GH_TOKEN: ${{ needs.validate-dispatch.outputs.target_repository == github.repository && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} @@ -529,7 +538,6 @@ jobs: HEAD_SHA: ${{ needs.validate-dispatch.outputs.head_sha }} REQUIRED_RUN_ID: ${{ needs.validate-dispatch.outputs.required_run_id }} REQUIRED_JOBS: ${{ needs.validate-dispatch.outputs.required_jobs }} - REQUIRED_LANGUAGE: ${{ matrix.language }} WAKE_TOKEN_SOURCE: ${{ needs.validate-dispatch.outputs.target_repository == github.repository && 'github-token' || secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || 'unavailable' }} run: | set -euo pipefail @@ -537,13 +545,8 @@ jobs: echo "::error::Actions-capable CodeQL wake credential is unavailable." exit 1 fi - REQUIRED_JOB_ID="$(printf '%s' "$REQUIRED_JOBS" | jq -r --arg lang "$REQUIRED_LANGUAGE" ' - [.[] | select(.language == $lang) | .job_id | tostring] - | if length == 1 and (.[0] | test("^[1-9][0-9]*$")) then .[0] else empty end - ')" if ! [[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]] || - ! [[ "$REQUIRED_JOB_ID" =~ ^[1-9][0-9]*$ ]] || - ! [[ "$REQUIRED_LANGUAGE" =~ ^[a-z0-9-]+$ ]]; then + [ "$(printf '%s' "$REQUIRED_JOBS" | jq 'type == "array" and length > 0' 2>/dev/null || true)" != "true" ]; then echo "::error::CodeQL wake identity is non-canonical." exit 1 fi @@ -562,23 +565,30 @@ jobs: | select(.event == "pull_request") | select(.path == ".github/workflows/codeql-pr.yml") | select(.head_sha == $head) - | .id // empty - ')" - expected_name="CodeQL compatibility analysis (${REQUIRED_LANGUAGE})" - job="$(gh api "repos/${TARGET_REPOSITORY}/actions/jobs/${REQUIRED_JOB_ID}")" - job_identity="$(printf '%s' "$job" | jq -r --arg head "$HEAD_SHA" --arg name "$expected_name" --argjson run_id "$REQUIRED_RUN_ID" --argjson job_id "$REQUIRED_JOB_ID" ' - select(.id == $job_id) - | select(.run_id == $run_id) - | select(.head_sha == $head) - | select(.name == $name) | select(.status == "completed" and .conclusion == "failure") | .id // empty ')" - if [ "$run_identity" != "$REQUIRED_RUN_ID" ] || - [ "$job_identity" != "$REQUIRED_JOB_ID" ]; then - echo "::error::CodeQL wake rejected missing or ambiguous exact run/job identity." + if [ "$run_identity" != "$REQUIRED_RUN_ID" ]; then + echo "::error::CodeQL wake rejected a missing or ambiguous exact run identity." + exit 1 + fi + + run_jobs="$(gh api --paginate "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/jobs" | jq -s '{jobs: map(.jobs[]?)}')" + expected_failed_ids="$(printf '%s' "$REQUIRED_JOBS" | jq -c ' + [ .[] + | select((.language | type) == "string") + | select(.language | test("^[a-z0-9-]+$")) + | select((.job_id | tostring) | test("^[1-9][0-9]*$")) + | .job_id | tonumber + ] | sort + ')" + observed_failed_ids="$(printf '%s' "$run_jobs" | jq -c ' + [ .jobs[] | select(.status == "completed" and .conclusion == "failure") | .id ] | sort + ')" + if [ "$expected_failed_ids" != "$observed_failed_ids" ]; then + echo "::error::CodeQL wake rejected a run whose bound failed CodeQL jobs do not exactly match its failed jobs." exit 1 fi - gh api -X POST "repos/${TARGET_REPOSITORY}/actions/jobs/${REQUIRED_JOB_ID}/rerun" >/dev/null - echo "Re-ran exact failed CodeQL job ${REQUIRED_JOB_ID} for ${REQUIRED_LANGUAGE} on ${HEAD_SHA}." + gh api -X POST "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/rerun-failed-jobs" >/dev/null + echo "Re-ran the bound failed CodeQL jobs once on ${HEAD_SHA}." diff --git a/AGENTS.md b/AGENTS.md index e955f8b36a..8581ce1abc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -192,6 +192,11 @@ them alone proves succession. for every literal you touched — event-type strings, cron expressions, environment-variable names, tuple members, pinned digests — not only the obviously named sibling test. A change can satisfy one oracle and still leave a second, independent one stale. +- For a multi-language CodeQL dispatch, do not rerun the required jobs from + matrix shards. After every shard has produced its terminal gate outcome, + verify that the original run's complete failed-job set exactly matches the + authenticated CodeQL binding, then issue one run-level failed-jobs rerun. + GitHub rejects the second concurrent job rerun with HTTP 403. - Read a stale pull request's own changes with a three-dot diff — `git diff ...` — or with `gh pr diff`, which is already three-dot. A two-dot `git diff ` renders everything the base gained since the fork point as though diff --git a/CHANGELOG.md b/CHANGELOG.md index bf192f6a9e..f2562cf487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ - Raised `hourly-review-repair.yml`'s discovery ceiling from 50 to 200 while rotating deterministic 50-PR deep-inspection windows by hourly run number. The scheduler hydrates only the selected window and stops immediately after its single dispatch, preserving access to newer PRs without quadrupling expensive review/check/comment work. See `docs/doctoring/hourly-review-repair-single-file-consolidation.md`'s 2026-09-03 follow-up. ## [Unreleased] +- Serialize multi-language CodeQL dispatch wakeups after the scan matrix has + completed. The owner now proves that the original exact-head run's entire + failed-job set equals the authenticated CodeQL job binding before issuing one + failed-jobs rerun, preventing the second shard from receiving GitHub's + `workflow run already running` 403. - Include merge-scheduler entrypoint, core, and regression-test changes in the existing runtime-quality workflow's trigger and suite selector. Scheduler workflow edits retain queue checks and also select the full review-repair diff --git a/CLAUDE.md b/CLAUDE.md index 30db1fc23b..5ffaf2d19d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,6 +137,10 @@ repeatable compile command. needs the same evidence. - **100% coverage and 100% docstrings on `scripts/ci/`** are hard gates, not aspirations. New helper code needs matching tests and docstrings. +- **Multi-language CodeQL dispatch wakeups are run-level, not shard-level.** Wait for every + matrix shard, prove the failed-job set equals the authenticated binding, then rerun failed jobs + once. Concurrent per-job reruns make the first request reactivate the run and GitHub rejects the + second with HTTP 403. - **Product hourly callers** stay thin. Do not hard-code OriginWeave, aFIPC, naruon, or Keyverse into `pr-review-fix-scheduler.yml`. The model credential remains `NVIDIA_NIM_API_KEY` on the worker, never `COPILOT_GITHUB_TOKEN`. diff --git a/docs/doctoring/codeql-dispatch-multi-language-wake-race.md b/docs/doctoring/codeql-dispatch-multi-language-wake-race.md new file mode 100644 index 0000000000..5e7ab83d98 --- /dev/null +++ b/docs/doctoring/codeql-dispatch-multi-language-wake-race.md @@ -0,0 +1,34 @@ +# CodeQL dispatch multi-language wake race + +## Incident evidence + +On 2026-09-09, `.github` PR #1857 at +`afeffe3b6a7a5494be1dae12322a0fc2a78c6efe` dispatched run `34320386978` +after both initial CodeQL required jobs reported pending. Its `actions` scan +completed successfully, but the subsequent Python wake failed with GitHub's +`The workflow run containing this job is already running` HTTP 403. The first +per-language rerun had already reactivated the shared required-workflow run. + +The scan itself was not the failing evidence: both the CodeQL analysis and the +Medium-or-higher SARIF gate completed. A status publication 403 is separately +recorded as non-terminal for a clean scan, as required by the existing status +publication contract. + +## Corrective action + +`codeql-scan-dispatch.yml` now waits for the complete matrix and uses one +run-level `rerun-failed-jobs` call. Before that mutation, it revalidates the +open PR and exact head, validates the required workflow run identity, fetches +all jobs, and refuses unless the full failed-job identifier set exactly equals +the authenticated CodeQL binding. This preserves the no-unrelated-job +invariant without relying on concurrent per-job reruns. + +## Verification and recovery + +`tests/test_codeql_scan_dispatch_workflow_contract.py` executes the wake +block against fixture-backed GitHub responses. It covers the bounded job set, +stale and closed pull requests, a mismatched or nonfailed job set, and an +already-running required workflow. Hosted exact-head evidence remains required +before any PR or consumer result is treated as successful. If the binding does +not match, do not retry a job manually; inspect the exact run and dispatch a +new current-head scan only through the owner workflow. diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index dd30c8506d..82ab1dc08b 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -36,7 +36,7 @@ "Fetch the pinned CodeQL SARIF gate script", "Materialize pull request head for CodeQL scan", "Publish CodeQL dispatch status", - "Wake exact CodeQL required job", + "Wake exact CodeQL required jobs", ) @@ -544,24 +544,22 @@ def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> assert "cancel-in-progress: true" not in publish -def test_dispatch_wakes_only_the_exact_failed_codeql_job() -> None: +def test_dispatch_wakes_all_and_only_the_exact_failed_codeql_jobs_once() -> None: workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - wake = workflow.split(" - name: Wake exact CodeQL required job\n", 1)[1].split( + wake = workflow.split(" wake-exact-required-jobs:\n", 1)[1].split( "\n\n - name:", 1 )[0] - assert "steps.publish_status.outcome == 'success'" in wake + assert "needs: [validate-dispatch, scan]" in wake + assert "needs.scan.result == 'success'" in wake assert 'gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"' in wake assert 'gh api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}"' in wake - assert 'gh api "repos/${TARGET_REPOSITORY}/actions/jobs/${REQUIRED_JOB_ID}"' in wake + assert 'actions/runs/${REQUIRED_RUN_ID}/jobs' in wake assert 'select(.event == "pull_request")' in wake assert 'select(.path == ".github/workflows/codeql-pr.yml")' in wake assert "select(.head_sha == $head)" in wake - assert "select(.run_id == $run_id)" in wake - assert "select(.name == $name)" in wake - assert 'select(.status == "completed" and .conclusion == "failure")' in wake - assert 'actions/jobs/${REQUIRED_JOB_ID}/rerun' in wake - assert "rerun-failed-jobs" not in wake + assert "bound failed CodeQL jobs" in wake + assert 'actions/runs/${REQUIRED_RUN_ID}/rerun-failed-jobs' in wake assert "while " not in wake assert "sleep " not in wake @@ -610,7 +608,7 @@ def _run_wake_step( "conclusion": "failure", } script = _extract_run_block( - WORKFLOW_PATH.read_text(encoding="utf-8"), "Wake exact CodeQL required job" + WORKFLOW_PATH.read_text(encoding="utf-8"), "Wake exact CodeQL required jobs" ) fake_bin = tmp_path / "bin" fake_bin.mkdir(parents=True) @@ -625,10 +623,13 @@ def _run_wake_step( ' printf \'%s\\n\' "$4" >>"$FAKE_POST_LOG"\n' " exit 0\n" "fi\n" + 'if [ "${2:-}" = "--paginate" ]; then\n' + ' printf \'{"jobs":[%s,%s]}\\n\' "$FAKE_JOB_JSON" "$FAKE_SECOND_JOB_JSON"\n' + " exit 0\n" + "fi\n" 'case "$2" in\n' ' */pulls/*) printf \'%s\\n\' "$FAKE_PULL_JSON" ;;\n' ' */actions/runs/*) printf \'%s\\n\' "$FAKE_RUN_JSON" ;;\n' - ' */actions/jobs/*) printf \'%s\\n\' "$FAKE_JOB_JSON" ;;\n' " *) exit 1 ;;\n" "esac\n", encoding="utf-8", @@ -640,6 +641,14 @@ def _run_wake_step( "FAKE_PULL_JSON": json.dumps(pull), "FAKE_RUN_JSON": json.dumps(run), "FAKE_JOB_JSON": json.dumps(job), + "FAKE_SECOND_JOB_JSON": json.dumps( + { + "id": 44, + "name": "CodeQL compatibility analysis (actions)", + "status": "completed", + "conclusion": "failure", + } + ), "FAKE_POST_LOG": str(post_log), "GH_TOKEN": "fake-token", "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", @@ -661,12 +670,12 @@ def _run_wake_step( return result, post_log -def test_dispatch_wake_reruns_only_fixture_bound_exact_job(tmp_path: Path) -> None: +def test_dispatch_wake_reruns_only_fixture_bound_exact_job_set(tmp_path: Path) -> None: result, post_log = _run_wake_step(tmp_path) assert result.returncode == 0, result.stderr assert post_log.read_text(encoding="utf-8").splitlines() == [ - "repos/ContextualWisdomLab/naruon/actions/jobs/43/rerun" + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs" ] @@ -684,13 +693,11 @@ def test_dispatch_wake_rejects_stale_head_and_closed_pr(tmp_path: Path) -> None: assert not closed_log.exists() -def test_dispatch_wake_rejects_ambiguous_or_nonfailed_job_identity(tmp_path: Path) -> None: +def test_dispatch_wake_rejects_unbound_or_nonfailed_job_set(tmp_path: Path) -> None: wrong_job_result, wrong_job_log = _run_wake_step( tmp_path / "wrong-job", job={ - "id": 43, - "run_id": 999, - "head_sha": "b" * 40, + "id": 44, "name": "CodeQL compatibility analysis (python)", "status": "completed", "conclusion": "failure", @@ -700,8 +707,6 @@ def test_dispatch_wake_rejects_ambiguous_or_nonfailed_job_identity(tmp_path: Pat tmp_path / "successful-job", job={ "id": 43, - "run_id": 42, - "head_sha": "b" * 40, "name": "CodeQL compatibility analysis (python)", "status": "completed", "conclusion": "success", @@ -710,13 +715,13 @@ def test_dispatch_wake_rejects_ambiguous_or_nonfailed_job_identity(tmp_path: Pat assert wrong_job_result.returncode == 1 assert successful_job_result.returncode == 1 - assert "missing or ambiguous exact run/job identity" in wrong_job_result.stdout + assert "bound failed CodeQL jobs" in wrong_job_result.stdout assert not wrong_job_log.exists() assert not successful_job_log.exists() -def test_dispatch_wake_allows_parallel_language_rerun_on_same_exact_run(tmp_path: Path) -> None: - """Another language may already have moved the shared run back to in_progress.""" +def test_dispatch_wake_rejects_an_already_running_exact_run(tmp_path: Path) -> None: + """The single wake runs only after the matrix has completed.""" result, post_log = _run_wake_step( tmp_path, run={ @@ -729,8 +734,8 @@ def test_dispatch_wake_allows_parallel_language_rerun_on_same_exact_run(tmp_path }, ) - assert result.returncode == 0, result.stderr - assert post_log.exists() + assert result.returncode == 1 + assert not post_log.exists() def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: From 75ca862da09729361b52183b4345987fc0112911 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 16:15:26 +0900 Subject: [PATCH 2/3] docs(ci): record CodeQL wake bootstrap boundary --- AGENTS.md | 4 ++++ CHANGELOG.md | 7 +++++++ CLAUDE.md | 4 ++++ .../codeql-dispatch-multi-language-wake-race.md | 17 +++++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 8581ce1abc..297725d3f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,10 @@ The materialization contract is also covered by [`docs/doctoring/exact-artifact- head. If a current-head dispatch is cancelled while deduplicating, enqueue exactly one replacement for that PR and workflow and verify the replacement carries the same live target head. +- A `repository_dispatch` handler executes its default-branch workflow, not an + open control-plane PR's file. Do not add `workflow_dispatch` merely to test a + privileged branch implementation. Use fixture-backed contracts before merge, + then verify the first default-branch dispatch after protected integration. - Before every review, retry, push, or merge claim, re-fetch the PR's exact head SHA, base SHA, review threads, required checks, and ruleset result. A push invalidates earlier checks and reviews. Never self-approve, dismiss reviews, diff --git a/CHANGELOG.md b/CHANGELOG.md index f2562cf487..24b25c013b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,13 @@ ### CodeQL scan dispatch matrix serialisation +- Documented the CodeQL wake bootstrap boundary: a `repository_dispatch` run + executes the default-branch handler, so an open control-plane PR cannot + exercise its proposed privileged workflow by selecting its branch. The + pre-merge evidence is fixture-backed contracts and actionlint; the first + protected default-branch dispatch is the required live proof. No + `workflow_dispatch` bypass was added. + - Serialised the dispatched CodeQL matrix with `toJSON()` in `codeql-scan-dispatch.yml`. `codeql-pr.yml` sends `client_payload.matrix` as an array and the handler assigned it straight into `env:`, where a value must be a scalar, so GitHub rejected the step with "A sequence was not expected" and the dispatched scan never ran -- 0 successes against 136 failures since the handler was added in #1776. The validate step already consumes the value through `jq`, so JSON text is the shape it was written for and no consumer changes. Added a string contract test, because neither `yaml.safe_load` nor `actionlint` 1.7.12 flags this: it is an Actions template rule, so only GitHub's own validator rejects it and no local gate catches the class. ### Contextual-orchestrator pin refresh diff --git a/CLAUDE.md b/CLAUDE.md index 5ffaf2d19d..4fd30e0fb0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,10 @@ configuring any such loop. The repo/Project — not private agent memory — is the source of truth. This file complements those documents; it does not replace them. +`repository_dispatch` executes the default-branch workflow. An open central workflow PR therefore +needs fixture-backed contracts before merge and a fresh default-branch dispatch after protected +integration; never add branch-selected `workflow_dispatch` to bypass that boundary. + ## What this repository is This is the ContextualWisdomLab **organization-wide `.github` special repository**. It has three roles: diff --git a/docs/doctoring/codeql-dispatch-multi-language-wake-race.md b/docs/doctoring/codeql-dispatch-multi-language-wake-race.md index 5e7ab83d98..822dbc47fa 100644 --- a/docs/doctoring/codeql-dispatch-multi-language-wake-race.md +++ b/docs/doctoring/codeql-dispatch-multi-language-wake-race.md @@ -32,3 +32,20 @@ already-running required workflow. Hosted exact-head evidence remains required before any PR or consumer result is treated as successful. If the binding does not match, do not retry a job manually; inspect the exact run and dispatch a new current-head scan only through the owner workflow. + +### Bootstrap boundary + +On 2026-09-09, PR `#2056` at +`aad55ed864db3466d52d24dcd88a80d15e84996d` triggered dispatch run +`34322210652` for required run `34321725703`. The run executed the protected +default-branch handler, as GitHub defines for `repository_dispatch`; its log +therefore used the old per-job wake and reproduced the same HTTP 403 when the +second shard tried to rerun an already-running required workflow. This does +not execute or disprove #2056's proposed run-level wake. + +The branch cannot use `workflow_dispatch` as a substitute: that would let a +caller select an unprotected workflow ref while minting privileged cross-repo +credentials, and the central queue contract forbids it. The pre-merge proof is +the fixture-backed contract plus actionlint. After protected integration, a +fresh default-branch dispatch must prove the one-call wake against an exact +current head before a consumer adopts the owner. From 69ae472562c93cc17674af5e2085a58947d3fab8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 18:12:42 +0900 Subject: [PATCH 3/3] fix(codeql): reflect terminal scan failures Signed-off-by: Seongho Bae --- .github/workflows/codeql-scan-dispatch.yml | 2 +- AGENTS.md | 4 +++- CHANGELOG.md | 3 +++ CLAUDE.md | 3 ++- tests/test_codeql_scan_dispatch_workflow_contract.py | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 1f95d35c4c..b698f6154e 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -520,7 +520,7 @@ jobs: if: >- always() && needs.validate-dispatch.result == 'success' - && needs.scan.result == 'success' + && needs.scan.result != 'cancelled' && needs.validate-dispatch.outputs.target_repository != '' && needs.validate-dispatch.outputs.pr_number != '' && needs.validate-dispatch.outputs.base_ref != '' diff --git a/AGENTS.md b/AGENTS.md index 4b111c74b3..eef7ed2e34 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -234,7 +234,9 @@ them alone proves succession. - A successful rerun of one matrix job does not rerun its sibling matrix jobs. Therefore an `already running` response from a second per-job rerun must remain a failure: even if the shared run is active, that sibling can still retain its old failed verdict. Coordinate the wake only - after all dispatch shards publish, then rerun the exact run's failed jobs as one operation. + after all dispatch shards terminate, including a scan failure whose exact verdict must be + reflected by the required check; exclude cancellation because it has no complete evidence. + Then rerun the exact run's failed jobs as one operation. - A `codeql-dispatch/` commit status is head-scoped and carries neither the PR base nor the required-run identity. Keep it as diagnostic output only. Shards and the coordinator may accept a terminal verdict only from a completed central dispatch run named with diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fdd58be2..6f3ce3c2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ while a sibling language scan is still running. The coordinator now keeps an active run with the same immutable repository, PR, head, base, and required run identity, avoiding same-PR cancellation of valid evidence. +- The run-level wake starts after a terminal scan matrix whether its verdict is + success or failure, so exact failure evidence reaches the required check; a + cancelled matrix remains excluded because it has no complete verdict. ### Failed-check finding names the Strix sandbox instead of the gateway diff --git a/CLAUDE.md b/CLAUDE.md index 570b994fb5..2e39d3fd13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -236,7 +236,8 @@ repeatable compile command. branch or from the autofix flow's conflict-marker resolution. - **Per-job reruns do not cover matrix siblings.** Do not accept a second shard's `already running` response merely because the shared run is active. Coordinate after every dispatch shard has - published its verdict and wake the exact run's failed jobs once, so no sibling retains a stale + terminated and wake the exact run's failed jobs once, so a scan failure is reflected rather than + suppressed and no sibling retains a stale failed required check. The wake is valid only while PR number, head SHA, and base SHA still match both live PR metadata and the exact required run's `pull_requests[]` association. - **Head-only CodeQL statuses are diagnostic, not terminal authority.** They cannot distinguish diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 9f09710ffe..e90421b535 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -554,7 +554,7 @@ def test_dispatch_wakes_failed_jobs_once_after_all_language_shards() -> None: assert "needs: [validate-dispatch, scan]" in wake_job assert "always()" in wake_job - assert "needs.scan.result == 'success'" in wake_job + assert "needs.scan.result != 'cancelled'" in wake_job assert "needs.validate-dispatch.outputs.base_ref != ''" in wake_job assert "needs.validate-dispatch.outputs.base_sha != ''" in wake_job assert "BASE_REF: ${{ needs.validate-dispatch.outputs.base_ref }}" in wake_job