From 1d9c70bc9b6817ab1003c336b83b4044d93d0e93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 10:09:57 +0900 Subject: [PATCH 1/9] fix(codeql): keep a clean dispatch scan when status publish 403s opencode-agent is installed with statuses:read, so POST /statuses to a target repo returns HTTP 403 after the SARIF gate already passed. Treat the completed dispatch scan job as terminal evidence and let the required shard consume that public run on rerun instead of fail-closing a clean scan. --- .github/workflows/codeql-pr.yml | 30 +++++++++ .github/workflows/codeql-scan-dispatch.yml | 5 ++ tests/test_codeql_pr_workflow_contract.py | 66 ++++++++++++++++++- ..._codeql_scan_dispatch_workflow_contract.py | 18 +++++ 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index db06b7c4ba..9c61471ef4 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -208,6 +208,36 @@ jobs: exit 0 ;; esac + + expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}" + expected_job="CodeQL dispatch scan (${LANGUAGE})" + runs_json="$(gh api "repos/ContextualWisdomLab/.github/actions/workflows/codeql-scan-dispatch.yml/runs?per_page=30")" + run_id="$(printf '%s' "$runs_json" | jq -r --arg title "$expected_title" --arg path ".github/workflows/codeql-scan-dispatch.yml" ' + [ + .workflow_runs[] + | select(.path == $path) + | select(.event == "repository_dispatch") + | select(.status == "completed") + | select(.display_title == $title or .name == $title) + ] + | first + | .id // empty + ')" + if [[ "$run_id" =~ ^[1-9][0-9]*$ ]]; then + jobs_json="$(gh api "repos/ContextualWisdomLab/.github/actions/runs/${run_id}/jobs?per_page=20")" + job_conclusion="$(printf '%s' "$jobs_json" | jq -r --arg name "$expected_job" ' + [.jobs[] | select(.name == $name)] + | if length == 1 then .[0].conclusion else empty end + ')" + case "$job_conclusion" in + success|failure) + echo "verdict=${job_conclusion}" >>"$GITHUB_OUTPUT" + echo "Found completed CodeQL dispatch scan job for ${LANGUAGE}: ${job_conclusion}." + exit 0 + ;; + esac + fi + if [ "$RUN_ATTEMPT" != "1" ]; then echo "::error::Exact CodeQL job was rerun without an authenticated terminal verdict." exit 1 diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 521ceeb167..d31c9c6c58 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -503,6 +503,11 @@ jobs: exit 0 fi + if [ "$GATE_OUTCOME" = "success" ]; then + echo "::notice::Could not publish the CodeQL dispatch status after all configured credentials failed. The completed dispatch scan job remains the evidence for this head." + exit 0 + fi + 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 diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 2d11ca0141..6dd49eaf70 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -133,7 +133,12 @@ def test_codeql_pr_dispatch_and_release_run_blocks_are_valid_bash() -> None: def _run_verdict_read( - tmp_path: Path, statuses: list[dict] + tmp_path: Path, + statuses: list[dict], + *, + dispatch_runs: dict | None = None, + dispatch_jobs: dict | None = None, + run_attempt: str = "2", ) -> tuple[subprocess.CompletedProcess[str], subprocess.CompletedProcess[str]]: """Execute the real one-shot status read and verdict enforcement blocks.""" bash = shutil.which("bash") @@ -157,6 +162,8 @@ def _run_verdict_read( 'case "$2" in\n' " */pulls/*) printf '%s\\n' \"$FAKE_PULL_JSON\" ;;\n" " */statuses) printf '%s\\n' \"$FAKE_STATUSES_JSON\" ;;\n" + " */codeql-scan-dispatch.yml/runs*) printf '%s\\n' \"$FAKE_DISPATCH_RUNS_JSON\" ;;\n" + " */actions/runs/*/jobs*) printf '%s\\n' \"$FAKE_DISPATCH_JOBS_JSON\" ;;\n" " *) exit 1 ;;\n" "esac\n", encoding="utf-8", @@ -169,6 +176,12 @@ def _run_verdict_read( "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps(live_pr), "FAKE_STATUSES_JSON": json.dumps(statuses), + "FAKE_DISPATCH_RUNS_JSON": json.dumps( + dispatch_runs if dispatch_runs is not None else {"workflow_runs": []} + ), + "FAKE_DISPATCH_JOBS_JSON": json.dumps( + dispatch_jobs if dispatch_jobs is not None else {"jobs": []} + ), "GH_TOKEN": "fake-token", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", "PR_NUMBER": "42", @@ -178,7 +191,7 @@ def _run_verdict_read( "BASE_REF": "main", "BASE_SHA": "a" * 40, "HEAD_REF": "feature", - "RUN_ATTEMPT": "2", + "RUN_ATTEMPT": run_attempt, "REQUIRED_RUN_ID": "42", "REQUIRED_JOB_ID": "43", "GITHUB_OUTPUT": str(output), @@ -248,6 +261,51 @@ def test_codeql_pr_one_shot_read_accepts_the_opencode_agent_creator(tmp_path: Pa assert "Current-head CodeQL dispatch verdict for python: success." in verdict_result.stdout +def test_codeql_pr_one_shot_read_accepts_completed_dispatch_scan_job_when_status_unpublishable( + tmp_path: Path, +) -> None: + """A completed dispatch scan job is terminal evidence when statuses:write 403s. + + Live 2026-09-08 naruon#1596 dispatch run 34173910106 scanned clean, then + POST /statuses returned HTTP 403 for opencode-agent (statuses:read only) + and github.token (cross-repo). The required shard must consume that + completed scan job instead of staying fail-closed on a missing status. + """ + head_sha = "b" * 40 + dispatch_result, verdict_result = _run_verdict_read( + tmp_path, + statuses=[], + dispatch_runs={ + "workflow_runs": [ + { + "id": 34173910106, + "event": "repository_dispatch", + "path": ".github/workflows/codeql-scan-dispatch.yml", + "status": "completed", + "display_title": ( + "CodeQL Scan Dispatch ContextualWisdomLab/naruon#42@" + head_sha + ), + "name": ( + "CodeQL Scan Dispatch ContextualWisdomLab/naruon#42@" + head_sha + ), + } + ] + }, + dispatch_jobs={ + "jobs": [ + { + "name": "CodeQL dispatch scan (python)", + "conclusion": "success", + } + ] + }, + ) + assert dispatch_result.returncode == 0, dispatch_result.stderr + dispatch_result.stdout + assert verdict_result.returncode == 0, verdict_result.stderr + verdict_result.stdout + assert "completed CodeQL dispatch scan job for python: success" in dispatch_result.stdout + assert "Current-head CodeQL dispatch verdict for python: success." in verdict_result.stdout + + def test_codeql_action_steps_use_one_version_per_workflow() -> None: """Prevent CodeQL init/analyze version splits from failing the scheduled scan.""" workflow = (REPO_ROOT / ".github/workflows/scheduled-security-scan.yml").read_text( @@ -322,6 +380,8 @@ def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch( 'case "$2" in\n' " */pulls/*) printf '%s\\n' \"$FAKE_PULL_JSON\" ;;\n" " */statuses) printf '%s\\n' \"$FAKE_STATUSES_JSON\" ;;\n" + " */codeql-scan-dispatch.yml/runs*) printf '%s\\n' \"$FAKE_DISPATCH_RUNS_JSON\" ;;\n" + " */actions/runs/*/jobs*) printf '%s\\n' \"$FAKE_DISPATCH_JOBS_JSON\" ;;\n" " *) exit 1 ;;\n" "esac\n", encoding="utf-8", @@ -333,6 +393,8 @@ def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch( "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps({"head": {"sha": head_sha}, "state": "open"}), "FAKE_STATUSES_JSON": json.dumps([]), + "FAKE_DISPATCH_RUNS_JSON": json.dumps({"workflow_runs": []}), + "FAKE_DISPATCH_JOBS_JSON": json.dumps({"jobs": []}), "FAKE_POST_LOG": str(post_log), "GH_TOKEN": "fake-token", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index dea1326494..9562387473 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -505,6 +505,24 @@ def test_codeql_scan_dispatch_is_not_in_the_required_workflow_ruleset_scope(): assert ".github/workflows/codeql-scan-dispatch.yml" not in required_paths +def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> None: + """A clean SARIF gate must not fail the handler solely because POST /statuses 403s. + + opencode-agent is installed with statuses:read. Cross-repo github.token cannot + write naruon commit statuses. The completed scan job is the remaining evidence. + """ + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + publish = workflow.split(" - name: Publish CodeQL dispatch status\n", 1)[1].split( + "\n - name: Wake exact CodeQL required job\n", 1 + )[0] + + assert "GATE_OUTCOME" in publish + assert 'if [ "$GATE_OUTCOME" = "success" ]; then' in publish + assert "completed dispatch scan job remains the evidence" in publish + assert "continue-on-error:" not in publish + assert "cancel-in-progress: true" not in publish + + def test_dispatch_wakes_only_the_exact_failed_codeql_job() -> None: workflow = WORKFLOW_PATH.read_text(encoding="utf-8") wake = workflow.split(" - name: Wake exact CodeQL required job\n", 1)[1].split( From 6c4ed678a2fbbd9d624b320c73b81e06ba1599fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:04:34 +0900 Subject: [PATCH 2/9] fix(codeql): dispatch remaining languages on workflow reruns Attempt 2 of .github#2028 skipped Dispatch current-head because the coordinator required github.run_attempt == 1, so no codeql-scan was posted. Later attempts still skip when every language already has a terminal opencode-agent verdict. --- .github/workflows/codeql-pr.yml | 1 - tests/test_codeql_pr_workflow_contract.py | 22 +++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 9c61471ef4..74cc8be0c7 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -281,7 +281,6 @@ jobs: always() && github.event.action != 'closed' && github.event.pull_request.state != 'closed' - && github.run_attempt == 1 && needs.detect-languages.result == 'success' && needs.detect-languages.outputs.code == 'true' runs-on: ubuntu-24.04 diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 6dd49eaf70..af5072f18e 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -87,8 +87,28 @@ def test_codeql_pr_shards_do_not_dispatch_and_coordinator_sends_the_full_matrix_ assert "needs: [detect-languages, analyze-head]" in coordinator assert "always()" in coordinator.split("\n runs-on:", 1)[0] assert "github.event.action != 'closed'" in coordinator.split("\n runs-on:", 1)[0] - assert "github.run_attempt == 1" in coordinator.split("\n runs-on:", 1)[0] + coordinator_if = coordinator.split("\n runs-on:", 1)[0] + assert "github.run_attempt == 1" not in coordinator_if assert coordinator.count("repos/ContextualWisdomLab/.github/dispatches") == 1 + + +def test_codeql_coordinator_dispatches_later_attempts_when_no_terminal_verdict() -> None: + """A rerun must still POST codeql-scan if attempt 1 never dispatched. + + Live ContextualWisdomLab/.github#2028 run 34175742278 was attempt 2. + ``github.run_attempt == 1`` skipped Dispatch current-head, so no + codeql-scan-dispatch.yml run existed and compatibility stayed pending. + The coordinator script already skips when every language has a terminal + opencode-agent verdict, so later attempts are safe. + """ + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + coordinator_if = workflow.split(" dispatch-current-head:\n", 1)[1].split( + "\n runs-on:", 1 + )[0] + coordinator = workflow.split(" dispatch-current-head:\n", 1)[1] + + assert "github.run_attempt == 1" not in coordinator_if + assert "All detected CodeQL languages already have authenticated terminal verdicts" in coordinator assert 'event_type:"codeql-scan"' in coordinator assert "required_jobs:$required_jobs" in coordinator assert "required_run_id:$required_run_id" in coordinator From c92e3367dd259b4d0a8a7256380414f60a0475d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:09:32 +0900 Subject: [PATCH 3/9] test(codeql): require paginated dispatch evidence lookup --- tests/test_codeql_pr_workflow_contract.py | 46 ++++++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index af5072f18e..303c41d1d9 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -156,7 +156,7 @@ def _run_verdict_read( tmp_path: Path, statuses: list[dict], *, - dispatch_runs: dict | None = None, + dispatch_runs: dict | list[dict] | None = None, dispatch_jobs: dict | None = None, run_attempt: str = "2", ) -> tuple[subprocess.CompletedProcess[str], subprocess.CompletedProcess[str]]: @@ -197,7 +197,9 @@ def _run_verdict_read( "FAKE_PULL_JSON": json.dumps(live_pr), "FAKE_STATUSES_JSON": json.dumps(statuses), "FAKE_DISPATCH_RUNS_JSON": json.dumps( - dispatch_runs if dispatch_runs is not None else {"workflow_runs": []} + dispatch_runs + if isinstance(dispatch_runs, list) + else [dispatch_runs if dispatch_runs is not None else {"workflow_runs": []}] ), "FAKE_DISPATCH_JOBS_JSON": json.dumps( dispatch_jobs if dispatch_jobs is not None else {"jobs": []} @@ -326,6 +328,46 @@ def test_codeql_pr_one_shot_read_accepts_completed_dispatch_scan_job_when_status assert "Current-head CodeQL dispatch verdict for python: success." in verdict_result.stdout +def test_codeql_pr_finds_completed_dispatch_scan_beyond_first_results_page( + tmp_path: Path, +) -> None: + """The exact completed dispatch remains discoverable on later API pages.""" + head_sha = "b" * 40 + expected_title = "CodeQL Scan Dispatch ContextualWisdomLab/naruon#42@" + head_sha + dispatch_result, verdict_result = _run_verdict_read( + tmp_path, + statuses=[], + dispatch_runs=[ + {"workflow_runs": []}, + { + "workflow_runs": [ + { + "id": 34173910106, + "event": "repository_dispatch", + "path": ".github/workflows/codeql-scan-dispatch.yml", + "status": "completed", + "display_title": expected_title, + "name": expected_title, + } + ] + }, + ], + dispatch_jobs={ + "jobs": [ + { + "name": "CodeQL dispatch scan (python)", + "conclusion": "success", + } + ] + }, + ) + + assert dispatch_result.returncode == 0, dispatch_result.stderr + dispatch_result.stdout + assert verdict_result.returncode == 0, verdict_result.stderr + verdict_result.stdout + assert "completed CodeQL dispatch scan job for python: success" in dispatch_result.stdout + + + def test_codeql_action_steps_use_one_version_per_workflow() -> None: """Prevent CodeQL init/analyze version splits from failing the scheduled scan.""" workflow = (REPO_ROOT / ".github/workflows/scheduled-security-scan.yml").read_text( From 3cefd2c844e394ee2be2ddd3ea0cd3520a8032c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:10:16 +0900 Subject: [PATCH 4/9] test(codeql): require paginated dispatch job lookup --- tests/test_codeql_pr_workflow_contract.py | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 303c41d1d9..bcb57e78a9 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -157,7 +157,7 @@ def _run_verdict_read( statuses: list[dict], *, dispatch_runs: dict | list[dict] | None = None, - dispatch_jobs: dict | None = None, + dispatch_jobs: dict | list[dict] | None = None, run_attempt: str = "2", ) -> tuple[subprocess.CompletedProcess[str], subprocess.CompletedProcess[str]]: """Execute the real one-shot status read and verdict enforcement blocks.""" @@ -202,7 +202,9 @@ def _run_verdict_read( else [dispatch_runs if dispatch_runs is not None else {"workflow_runs": []}] ), "FAKE_DISPATCH_JOBS_JSON": json.dumps( - dispatch_jobs if dispatch_jobs is not None else {"jobs": []} + dispatch_jobs + if isinstance(dispatch_jobs, list) + else [dispatch_jobs if dispatch_jobs is not None else {"jobs": []}] ), "GH_TOKEN": "fake-token", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", @@ -352,14 +354,17 @@ def test_codeql_pr_finds_completed_dispatch_scan_beyond_first_results_page( ] }, ], - dispatch_jobs={ - "jobs": [ - { - "name": "CodeQL dispatch scan (python)", - "conclusion": "success", - } - ] - }, + dispatch_jobs=[ + {"jobs": []}, + { + "jobs": [ + { + "name": "CodeQL dispatch scan (python)", + "conclusion": "success", + } + ] + }, + ], ) assert dispatch_result.returncode == 0, dispatch_result.stderr + dispatch_result.stdout From d51da47d1c4ed0aefccff18bb52123315a3c2f39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:10:43 +0900 Subject: [PATCH 5/9] fix(codeql): paginate exact dispatch evidence --- .github/workflows/codeql-pr.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 74cc8be0c7..9e0e04a9d1 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -211,10 +211,10 @@ jobs: expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}" expected_job="CodeQL dispatch scan (${LANGUAGE})" - runs_json="$(gh api "repos/ContextualWisdomLab/.github/actions/workflows/codeql-scan-dispatch.yml/runs?per_page=30")" + runs_json="$(gh api --paginate --slurp "repos/ContextualWisdomLab/.github/actions/workflows/codeql-scan-dispatch.yml/runs")" run_id="$(printf '%s' "$runs_json" | jq -r --arg title "$expected_title" --arg path ".github/workflows/codeql-scan-dispatch.yml" ' [ - .workflow_runs[] + .[] | .workflow_runs[] | select(.path == $path) | select(.event == "repository_dispatch") | select(.status == "completed") @@ -224,9 +224,9 @@ jobs: | .id // empty ')" if [[ "$run_id" =~ ^[1-9][0-9]*$ ]]; then - jobs_json="$(gh api "repos/ContextualWisdomLab/.github/actions/runs/${run_id}/jobs?per_page=20")" + jobs_json="$(gh api --paginate --slurp "repos/ContextualWisdomLab/.github/actions/runs/${run_id}/jobs")" job_conclusion="$(printf '%s' "$jobs_json" | jq -r --arg name "$expected_job" ' - [.jobs[] | select(.name == $name)] + [.[] | .jobs[] | select(.name == $name)] | if length == 1 then .[0].conclusion else empty end ')" case "$job_conclusion" in From 26e4e80409ea770d4d519028d2d26130e4b5aa9f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:14:52 +0900 Subject: [PATCH 6/9] test(codeql): parse paginated gh endpoint options --- tests/test_codeql_pr_workflow_contract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index bcb57e78a9..b5858ac06a 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -179,7 +179,8 @@ def _run_verdict_read( "#!/usr/bin/env bash\n" "set -euo pipefail\n" 'test "$1" = api\n' - 'case "$2" in\n' + 'endpoint="${@: -1}"\n' + 'case "$endpoint" in\n' " */pulls/*) printf '%s\\n' \"$FAKE_PULL_JSON\" ;;\n" " */statuses) printf '%s\\n' \"$FAKE_STATUSES_JSON\" ;;\n" " */codeql-scan-dispatch.yml/runs*) printf '%s\\n' \"$FAKE_DISPATCH_RUNS_JSON\" ;;\n" @@ -444,7 +445,8 @@ def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch( ' printf \'%s\\n\' "$4" >>"$FAKE_POST_LOG"\n' " exit 0\n" "fi\n" - 'case "$2" in\n' + 'endpoint="${@: -1}"\n' + 'case "$endpoint" in\n' " */pulls/*) printf '%s\\n' \"$FAKE_PULL_JSON\" ;;\n" " */statuses) printf '%s\\n' \"$FAKE_STATUSES_JSON\" ;;\n" " */codeql-scan-dispatch.yml/runs*) printf '%s\\n' \"$FAKE_DISPATCH_RUNS_JSON\" ;;\n" From c99d49a86a2728d136d9e3a34a6b82a2c0e84a90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:16:53 +0900 Subject: [PATCH 7/9] style(codeql): normalize pagination fixture spacing --- tests/test_codeql_pr_workflow_contract.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index b5858ac06a..d511ec8385 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -373,7 +373,6 @@ def test_codeql_pr_finds_completed_dispatch_scan_beyond_first_results_page( assert "completed CodeQL dispatch scan job for python: success" in dispatch_result.stdout - def test_codeql_action_steps_use_one_version_per_workflow() -> None: """Prevent CodeQL init/analyze version splits from failing the scheduled scan.""" workflow = (REPO_ROOT / ".github/workflows/scheduled-security-scan.yml").read_text( From e715a5e5e5235130cd8d6054f14088cd2c6cad6f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:39:28 +0900 Subject: [PATCH 8/9] test(codeql): match paginated empty dispatch fixtures Signed-off-by: Seongho Bae --- tests/test_codeql_pr_workflow_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index d511ec8385..9a31e4de14 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -461,8 +461,8 @@ def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch( "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps({"head": {"sha": head_sha}, "state": "open"}), "FAKE_STATUSES_JSON": json.dumps([]), - "FAKE_DISPATCH_RUNS_JSON": json.dumps({"workflow_runs": []}), - "FAKE_DISPATCH_JOBS_JSON": json.dumps({"jobs": []}), + "FAKE_DISPATCH_RUNS_JSON": json.dumps([{"workflow_runs": []}]), + "FAKE_DISPATCH_JOBS_JSON": json.dumps([{"jobs": []}]), "FAKE_POST_LOG": str(post_log), "GH_TOKEN": "fake-token", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", From af7c6e3a22dcab57302c79c5da281870d2be8e9c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 12:04:52 +0900 Subject: [PATCH 9/9] fix(codeql): bind dispatch fallback to live base and required run The completed-scan fallback matched only repo#PR@head plus language, so a same-head retarget could consume a predecessor-base scan and a different waiting required run could satisfy this shard. Encode live base SHA and required_run_id in the public dispatch run-name, look up that identity from the required shard, and POST the live base on later attempts. Concurrency stays repository+PR per #2008/#2009. --- .github/workflows/codeql-pr.yml | 27 ++- .github/workflows/codeql-scan-dispatch.yml | 4 +- tests/test_codeql_pr_workflow_contract.py | 205 ++++++++++++++---- ..._codeql_scan_dispatch_workflow_contract.py | 21 ++ 4 files changed, 212 insertions(+), 45 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 9e0e04a9d1..c21c8446df 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -171,10 +171,12 @@ jobs: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} LANGUAGE: ${{ matrix.language }} RUN_ATTEMPT: ${{ github.run_attempt }} + REQUIRED_RUN_ID: ${{ github.run_id }} run: | set -euo pipefail live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')" + live_base="$(printf '%s' "$live_pr" | jq -r '.base.sha // empty')" live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')" if [ -z "$live_head" ] || [ -z "$live_state" ]; then echo "::error::Could not validate live pull request state before CodeQL dispatch." @@ -188,6 +190,14 @@ jobs: echo "Pull request head moved on the live open PR; a fresh dispatch will fire for the current head." exit 0 fi + if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::Could not validate live pull request base SHA before CodeQL verdict read." + exit 1 + fi + if ! [[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::CodeQL shard requires a canonical current run id." + exit 1 + fi statuses="$(gh api "repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses")" verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "codeql-dispatch/${LANGUAGE}" ' @@ -209,7 +219,7 @@ jobs: ;; esac - expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}" + expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}" expected_job="CodeQL dispatch scan (${LANGUAGE})" runs_json="$(gh api --paginate --slurp "repos/ContextualWisdomLab/.github/actions/workflows/codeql-scan-dispatch.yml/runs")" run_id="$(printf '%s' "$runs_json" | jq -r --arg title "$expected_title" --arg path ".github/workflows/codeql-scan-dispatch.yml" ' @@ -306,6 +316,9 @@ jobs: set -euo pipefail live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')" + live_base="$(printf '%s' "$live_pr" | jq -r '.base.sha // empty')" + live_base_ref="$(printf '%s' "$live_pr" | jq -r '.base.ref // empty')" + live_head_ref="$(printf '%s' "$live_pr" | jq -r '.head.ref // empty')" live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')" if [ -z "$live_head" ] || [ -z "$live_state" ]; then echo "::error::Could not validate live pull request state before CodeQL dispatch." @@ -323,6 +336,10 @@ jobs: echo "::error::CodeQL dispatch requires a canonical current run id." exit 1 fi + if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]] || [ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then + echo "::error::Could not validate live pull request base identity before CodeQL dispatch." + exit 1 + fi include_json="$(printf '%s' "$MATRIX" | jq -c '.include // empty' 2>/dev/null || true)" if [ -z "$include_json" ] || @@ -414,10 +431,10 @@ jobs: jq -cn \ --arg target_repository "$TARGET_REPOSITORY" \ --arg pr_number "$PR_NUMBER" \ - --arg pr_base_ref "$PR_BASE_REF" \ - --arg pr_base_sha "$PR_BASE_SHA" \ - --arg pr_head_ref "$PR_HEAD_REF" \ - --arg pr_head_sha "$PR_HEAD_SHA" \ + --arg pr_base_ref "$live_base_ref" \ + --arg pr_base_sha "$live_base" \ + --arg pr_head_ref "$live_head_ref" \ + --arg pr_head_sha "$live_head" \ --argjson matrix "$pending_matrix" \ --arg required_run_id "$REQUIRED_RUN_ID" \ --argjson required_jobs "$required_jobs" \ diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index d31c9c6c58..c94fdf55c2 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -16,7 +16,9 @@ run-name: >- CodeQL Scan Dispatch ${{ github.event.client_payload.target_repository || github.repository }}#${{ github.event.client_payload.pr_number || 'event' }}@${{ - github.event.client_payload.pr_head_sha || github.sha }} + github.event.client_payload.pr_head_sha || github.sha }}/${{ + github.event.client_payload.pr_base_sha || 'none' }}/${{ + github.event.client_payload.required_run_id || github.run_id }} on: repository_dispatch: diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 9a31e4de14..dc67eef258 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -150,6 +150,38 @@ def test_codeql_pr_dispatch_and_release_run_blocks_are_valid_bash() -> None: DISPATCH_STEP_NAME = "Read current-head CodeQL dispatch verdict" VERDICT_STEP_NAME = "Release runner or enforce current-head CodeQL verdict" COORDINATOR_STEP_NAME = "Dispatch current-head CodeQL scan" +_TEST_HEAD_SHA = "b" * 40 +_TEST_BASE_SHA = "a" * 40 +_TEST_REQUIRED_RUN_ID = "42" + + +def _dispatch_scan_title( + *, + head_sha: str = _TEST_HEAD_SHA, + base_sha: str = _TEST_BASE_SHA, + required_run_id: str = _TEST_REQUIRED_RUN_ID, +) -> str: + """Return the immutable CodeQL dispatch run-name for one required shard.""" + return ( + "CodeQL Scan Dispatch ContextualWisdomLab/naruon#42@" + f"{head_sha}/{base_sha}/{required_run_id}" + ) + + +def _completed_dispatch_run( + *, + title: str, + run_id: int = 34173910106, +) -> dict: + """Return one completed central CodeQL dispatch workflow-run fixture.""" + return { + "id": run_id, + "event": "repository_dispatch", + "path": ".github/workflows/codeql-scan-dispatch.yml", + "status": "completed", + "display_title": title, + "name": title, + } def _run_verdict_read( @@ -169,8 +201,12 @@ def _run_verdict_read( dispatch_script = _extract_run_block(workflow_text, DISPATCH_STEP_NAME) verdict_script = _extract_run_block(workflow_text, VERDICT_STEP_NAME) - head_sha = "b" * 40 - live_pr = {"head": {"sha": head_sha}, "state": "open"} + head_sha = _TEST_HEAD_SHA + live_pr = { + "head": {"sha": head_sha}, + "base": {"sha": _TEST_BASE_SHA}, + "state": "open", + } fake_bin = tmp_path / "bin" fake_bin.mkdir() @@ -214,10 +250,10 @@ def _run_verdict_read( "LANGUAGE": "python", "BUILD_MODE": "none", "BASE_REF": "main", - "BASE_SHA": "a" * 40, + "BASE_SHA": _TEST_BASE_SHA, "HEAD_REF": "feature", "RUN_ATTEMPT": run_attempt, - "REQUIRED_RUN_ID": "42", + "REQUIRED_RUN_ID": _TEST_REQUIRED_RUN_ID, "REQUIRED_JOB_ID": "43", "GITHUB_OUTPUT": str(output), } @@ -225,9 +261,16 @@ def _run_verdict_read( [bash], input=dispatch_script, text=True, capture_output=True, check=False, env=dispatch_env, timeout=60, ) - output_values = dict( - line.split("=", 1) for line in output.read_text(encoding="utf-8").splitlines() - ) + output_values = {} + if output.exists(): + output_values = dict( + line.split("=", 1) for line in output.read_text(encoding="utf-8").splitlines() + if "=" in line + ) + if "verdict" not in output_values: + return dispatch_result, subprocess.CompletedProcess( + args=[bash], returncode=1, stdout="", stderr="" + ) verdict_env = { **os.environ, "LANGUAGE": "python", @@ -296,26 +339,12 @@ def test_codeql_pr_one_shot_read_accepts_completed_dispatch_scan_job_when_status and github.token (cross-repo). The required shard must consume that completed scan job instead of staying fail-closed on a missing status. """ - head_sha = "b" * 40 + head_sha = _TEST_HEAD_SHA + title = _dispatch_scan_title(head_sha=head_sha) dispatch_result, verdict_result = _run_verdict_read( tmp_path, statuses=[], - dispatch_runs={ - "workflow_runs": [ - { - "id": 34173910106, - "event": "repository_dispatch", - "path": ".github/workflows/codeql-scan-dispatch.yml", - "status": "completed", - "display_title": ( - "CodeQL Scan Dispatch ContextualWisdomLab/naruon#42@" + head_sha - ), - "name": ( - "CodeQL Scan Dispatch ContextualWisdomLab/naruon#42@" + head_sha - ), - } - ] - }, + dispatch_runs={"workflow_runs": [_completed_dispatch_run(title=title)]}, dispatch_jobs={ "jobs": [ { @@ -335,25 +364,14 @@ def test_codeql_pr_finds_completed_dispatch_scan_beyond_first_results_page( tmp_path: Path, ) -> None: """The exact completed dispatch remains discoverable on later API pages.""" - head_sha = "b" * 40 - expected_title = "CodeQL Scan Dispatch ContextualWisdomLab/naruon#42@" + head_sha + head_sha = _TEST_HEAD_SHA + expected_title = _dispatch_scan_title(head_sha=head_sha) dispatch_result, verdict_result = _run_verdict_read( tmp_path, statuses=[], dispatch_runs=[ {"workflow_runs": []}, - { - "workflow_runs": [ - { - "id": 34173910106, - "event": "repository_dispatch", - "path": ".github/workflows/codeql-scan-dispatch.yml", - "status": "completed", - "display_title": expected_title, - "name": expected_title, - } - ] - }, + {"workflow_runs": [_completed_dispatch_run(title=expected_title)]}, ], dispatch_jobs=[ {"jobs": []}, @@ -373,6 +391,83 @@ def test_codeql_pr_finds_completed_dispatch_scan_beyond_first_results_page( assert "completed CodeQL dispatch scan job for python: success" in dispatch_result.stdout +def test_codeql_pr_rejects_completed_dispatch_scan_from_a_stale_base( + tmp_path: Path, +) -> None: + """Same head and language after a base retarget must not reuse the prior scan. + + A PR can keep its head SHA while the base moves. The native handler already + binds receipts to the live base SHA; the required shard must not accept a + completed dispatch whose run-name still names the predecessor base. + """ + stale_title = _dispatch_scan_title(base_sha="c" * 40) + dispatch_result, _verdict_result = _run_verdict_read( + tmp_path, + statuses=[], + dispatch_runs={"workflow_runs": [_completed_dispatch_run(title=stale_title)]}, + dispatch_jobs={ + "jobs": [ + { + "name": "CodeQL dispatch scan (python)", + "conclusion": "success", + } + ] + }, + ) + + assert dispatch_result.returncode == 1, dispatch_result.stderr + dispatch_result.stdout + assert "without an authenticated terminal verdict" in dispatch_result.stdout + assert "completed CodeQL dispatch scan job for python: success" not in dispatch_result.stdout + + +def test_codeql_pr_rejects_completed_dispatch_scan_from_a_different_required_run( + tmp_path: Path, +) -> None: + """A same-PR/head/language scan for another required run cannot wake this shard. + + Language plus repository/PR/head is not enough: each waiting required job + lives in one required-workflow run. Binding required_run_id in the + dispatch run-name, together with the language job name, is the job + identity the shard can observe without reading client_payload. + """ + other_run_title = _dispatch_scan_title(required_run_id="99") + dispatch_result, _verdict_result = _run_verdict_read( + tmp_path, + statuses=[], + dispatch_runs={ + "workflow_runs": [_completed_dispatch_run(title=other_run_title)] + }, + dispatch_jobs={ + "jobs": [ + { + "name": "CodeQL dispatch scan (python)", + "conclusion": "success", + } + ] + }, + ) + + assert dispatch_result.returncode == 1, dispatch_result.stderr + dispatch_result.stdout + assert "without an authenticated terminal verdict" in dispatch_result.stdout + assert "completed CodeQL dispatch scan job for python: success" not in dispatch_result.stdout + + +def test_codeql_pr_fallback_binds_live_base_and_required_run_identity() -> None: + """The required shard looks up the public dispatch run by immutable identity.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + shard = workflow.split(" analyze-head:\n", 1)[1].split( + " dispatch-current-head:\n", 1 + )[0] + + assert "REQUIRED_RUN_ID: ${{ github.run_id }}" in shard + assert 'live_base="$(printf' in shard + assert ( + 'expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}' + '@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}"' + ) in shard + assert "Could not validate live pull request base SHA before CodeQL verdict read." in shard + + def test_codeql_action_steps_use_one_version_per_workflow() -> None: """Prevent CodeQL init/analyze version splits from failing the scheduled scan.""" workflow = (REPO_ROOT / ".github/workflows/scheduled-security-scan.yml").read_text( @@ -459,7 +554,13 @@ def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch( env = { **os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", - "FAKE_PULL_JSON": json.dumps({"head": {"sha": head_sha}, "state": "open"}), + "FAKE_PULL_JSON": json.dumps( + { + "head": {"sha": head_sha}, + "base": {"sha": _TEST_BASE_SHA}, + "state": "open", + } + ), "FAKE_STATUSES_JSON": json.dumps([]), "FAKE_DISPATCH_RUNS_JSON": json.dumps([{"workflow_runs": []}]), "FAKE_DISPATCH_JOBS_JSON": json.dumps([{"jobs": []}]), @@ -723,6 +824,32 @@ def test_codeql_coordinator_fails_closed_when_a_shard_job_id_is_missing( assert not post_log.exists() +def test_codeql_coordinator_dispatches_the_live_base_after_a_same_head_retarget( + tmp_path: Path, +) -> None: + """A retargeted PR must dispatch against the live base, not the event snapshot.""" + live_base = "c" * 40 + result, post_log, post_body = _run_coordinator( + tmp_path, + pull={ + "state": "open", + "head": {"sha": "b" * 40, "ref": "feature"}, + "base": {"sha": live_base, "ref": "release"}, + }, + env_overrides={"PR_BASE_SHA": "a" * 40, "PR_BASE_REF": "main"}, + ) + + assert result.returncode == 0, result.stderr + result.stdout + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/.github/dispatches" + ] + client = json.loads(post_body.read_text(encoding="utf-8"))["client_payload"] + assert client["pr_base_sha"] == live_base + assert client["pr_base_ref"] == "release" + assert client["pr_head_sha"] == "b" * 40 + assert client["required_run_id"] == "99" + + def test_codeql_coordinator_does_not_dispatch_a_closed_or_stale_pull_request( tmp_path: Path, ) -> None: diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 9562387473..dd30c8506d 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -505,6 +505,27 @@ def test_codeql_scan_dispatch_is_not_in_the_required_workflow_ruleset_scope(): assert ".github/workflows/codeql-scan-dispatch.yml" not in required_paths +def test_codeql_scan_dispatch_run_name_binds_base_and_required_run() -> None: + """Public run identity includes base SHA and required run id without changing concurrency. + + The required shard cannot read client_payload. Encoding those fields in + run-name lets it reject a same-head retarget or a different waiting + required run. The #2008/#2009 group stays repository+PR so a newer HEAD + of the same pull request still cancels its predecessor. + """ + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + header = workflow.split("\non:", 1)[0] + group_value = workflow_level_concurrency_group(workflow) + + assert "github.event.client_payload.pr_head_sha" in header + assert "github.event.client_payload.pr_base_sha" in header + assert "github.event.client_payload.required_run_id" in header + assert "github.event.client_payload.pr_base_sha" not in group_value + assert "github.event.client_payload.required_run_id" not in group_value + assert "github.event.client_payload.target_repository" in group_value + assert "github.event.client_payload.pr_number" in group_value + + def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> None: """A clean SARIF gate must not fail the handler solely because POST /statuses 403s.