From 952f95fe2a5797ce265a39c6706a826c516b7d7c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:17:03 +0900 Subject: [PATCH 01/28] test(codeql): require versioned handler rollout bootstrap --- ..._codeql_scan_dispatch_workflow_contract.py | 947 ++++++++++++++++-- ...d_codeql_dispatch_runner_image_contract.py | 4 +- 2 files changed, 870 insertions(+), 81 deletions(-) diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index dd30c8506d..807ae6c4b1 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -17,6 +17,8 @@ import sys from pathlib import Path +import pytest + from scripts.ci import audit_central_required_workflows as ruleset_audit from tests.test_opencode_workflow_shell_syntax import _extract_run_block from tests.test_required_workflow_queue_contract import ( @@ -36,7 +38,8 @@ "Fetch the pinned CodeQL SARIF gate script", "Materialize pull request head for CodeQL scan", "Publish CodeQL dispatch status", - "Wake exact CodeQL required job", + "Exchange OpenCode app token for run settlement", + "Settle exact CodeQL required run", ) @@ -78,7 +81,9 @@ def test_codeql_scan_dispatch_workflow_structure(): assert workflow.count("github/codeql-action/init@") == 1 assert workflow.count("github/codeql-action/analyze@") == 1 assert "scripts/ci/codeql_sarif_gate.py" in workflow - assert 'context="codeql-dispatch/${LANGUAGE}"' in workflow + assert '-f context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}"' in workflow + assert "github.event.client_payload.producer_source_sha" in workflow + assert 'receipt_description="cwl1;h=${HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${PRODUCER_SOURCE_SHA}"' in workflow assert "OPENCODE_REPOSITORY_DISPATCH_ACTOR" in workflow # Deliberately NOT vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS: that allowlist # scopes a gradual ~12-repo OpenCode review rollout, while ruleset @@ -137,7 +142,12 @@ def _run_validate_step(tmp_path: Path, env_overrides: dict[str, str], pull_reque "#!/usr/bin/env bash\n" "set -euo pipefail\n" 'test "$1" = api\n' - 'printf \'%s\\n\' "$FAKE_PULL_JSON"\n', + 'endpoint="${!#}"\n' + 'case "$endpoint" in\n' + ' repos/ContextualWisdomLab/.github/compare/*) printf \'%s\\n\' "$FAKE_SOURCE_COMPARE_JSON" ;;\n' + ' repos/ContextualWisdomLab/*/git/commits/*) printf \'%s\\n\' "$FAKE_PRODUCER_COMMIT_JSON" ;;\n' + ' *) printf \'%s\\n\' "$FAKE_PULL_JSON" ;;\n' + 'esac\n', encoding="utf-8", ) fake_gh.chmod(0o755) @@ -147,6 +157,13 @@ def _run_validate_step(tmp_path: Path, env_overrides: dict[str, str], pull_reque **os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps(pull_request), + "FAKE_SOURCE_COMPARE_JSON": "{}", + "FAKE_PRODUCER_COMMIT_JSON": json.dumps( + { + "sha": "c" * 40, + "parents": [{"sha": "a" * 40}, {"sha": "b" * 40}], + } + ), "GITHUB_OUTPUT": str(output), "DISPATCH_ACTOR": "seonghobae", "DISPATCH_SENDER": "seonghobae", @@ -155,11 +172,18 @@ def _run_validate_step(tmp_path: Path, env_overrides: dict[str, str], pull_reque "PR_NUMBER": "42", "SUPPLIED_BASE_REF": "main", "SUPPLIED_BASE_SHA": "a" * 40, + "SUPPLIED_HEAD_ENVELOPE": "null", + "SUPPLIED_HEAD_SCHEMA": "", "SUPPLIED_HEAD_REF": "feature", "SUPPLIED_HEAD_SHA": "b" * 40, + "SUPPLIED_LEGACY_HEAD_REF": "feature", + "SUPPLIED_LEGACY_HEAD_SHA": "b" * 40, + "SUPPLIED_PRODUCER_SOURCE_SHA": "c" * 40, "SUPPLIED_MATRIX": json.dumps([{"language": "python", "build-mode": "none"}]), "SUPPLIED_REQUIRED_RUN_ID": "42", "SUPPLIED_REQUIRED_JOBS": json.dumps([{"language": "python", "job_id": 43}]), + "SUPPLIED_RERUN_MODE": "", + "SUPPLIED_RERUN_REQUEST": "null", "SUPPLIED_REQUIRED_JOB_ID": "", "SUPPLIED_REQUIRED_LANGUAGE": "", **env_overrides, @@ -173,6 +197,7 @@ def _matching_pull_request() -> dict: """A live PR payload that matches the default supplied metadata in _run_validate_step.""" return { "state": "open", + "merge_commit_sha": "c" * 40, "base": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "main", "sha": "a" * 40}, "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "feature", "sha": "b" * 40}, } @@ -189,11 +214,362 @@ def test_codeql_scan_dispatch_validate_step_accepts_matching_live_metadata(tmp_p assert "head_sha=" + "b" * 40 in output_text assert '[{"language":"python","build-mode":"none"}]' in output_text assert "required_run_id=42" in output_text + assert "producer_source_sha=" + "c" * 40 in output_text assert '"job_id":43' in output_text.replace(" ", "") assert "required_job_id=" not in output_text assert "required_language=" not in output_text +def test_codeql_scan_dispatch_validate_step_rejects_unknown_head_schema(tmp_path): + """Unknown nested-head schema versions fail before metadata can be trusted.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": "2", "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "2", + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "unsupported pr_head schema=2" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_accepts_versioned_head_envelope(tmp_path): + """Schema-one nested head metadata reaches the live validation success path.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": "1", "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "1", + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + }, + _matching_pull_request(), + ) + + assert result.returncode == 0 + assert ( + "Validated current live metadata for ContextualWisdomLab/naruon#42: base=main/" + in result.stdout + ) + assert "head=feature/" in result.stdout + + +@pytest.mark.parametrize( + ("legacy_ref", "legacy_sha"), + [ + ("feature-wrong", "b" * 40), + ("feature", "c" * 40), + ("feature", ""), + ("", "b" * 40), + ], +) +def test_codeql_scan_dispatch_validate_step_rejects_conflicting_dual_head_identity( + tmp_path, legacy_ref, legacy_sha +): + """Nested identity cannot shadow an unequal or partial legacy representation.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": "1", "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "1", + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + "SUPPLIED_LEGACY_HEAD_REF": legacy_ref, + "SUPPLIED_LEGACY_HEAD_SHA": legacy_sha, + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "conflicting nested and legacy pr_head identity" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_numeric_head_schema(tmp_path): + """The JSON envelope schema stays a version string, not a numeric alias.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": 1, "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "1", + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "invalid pr_head envelope" in result.stdout + + +@pytest.mark.parametrize("missing_field", ["ref", "sha"]) +def test_codeql_scan_dispatch_validate_step_rejects_incomplete_head_envelope( + tmp_path, missing_field +): + """A present envelope cannot borrow a required value from legacy fields.""" + envelope = {"schema": "1", "ref": "feature", "sha": "b" * 40} + del envelope[missing_field] + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps(envelope), + "SUPPLIED_HEAD_SCHEMA": "1", + "SUPPLIED_LEGACY_HEAD_REF": "feature", + "SUPPLIED_LEGACY_HEAD_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "invalid pr_head envelope" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_unversioned_head_envelope(tmp_path): + """A nested head tuple without its schema version fails closed.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps({"ref": "feature", "sha": "b" * 40}), + "SUPPLIED_HEAD_SCHEMA": "", + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "unsupported pr_head schema=" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_accepts_nested_rerun_request(tmp_path): + """The bounded ten-key producer envelope normalizes mode and job identities.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_REQUIRED_JOBS": "null", + "SUPPLIED_RERUN_REQUEST": json.dumps( + { + "schema": "1", + "mode": "failed", + "required_jobs": [{"language": "python", "job_id": 43}], + } + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 0, result.stderr + output_text = result.output_path.read_text(encoding="utf-8") + assert "rerun_mode=failed" in output_text + assert "rerun_schema=1" in output_text + assert '"job_id":43' in output_text.replace(" ", "") + + +@pytest.mark.parametrize( + "rerun_request, expected_message", + [ + ( + {"mode": "failed", "required_jobs": [{"language": "python", "job_id": 43}]}, + "unsupported CodeQL rerun schema=", + ), + ( + { + "schema": "2", + "mode": "failed", + "required_jobs": [{"language": "python", "job_id": 43}], + }, + "unsupported CodeQL rerun schema=2", + ), + ( + { + "schema": 1, + "mode": "failed", + "required_jobs": [{"language": "python", "job_id": 43}], + }, + "CodeQL rerun schema must be a string", + ), + ], +) +def test_codeql_scan_dispatch_rejects_unversioned_or_unknown_nested_rerun_schema( + tmp_path, rerun_request, expected_message +): + """Nested authority is accepted only under the exact version-one schema.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_REQUIRED_JOBS": "null", + "SUPPLIED_RERUN_REQUEST": json.dumps(rerun_request), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert expected_message in result.stdout + + +def test_codeql_scan_dispatch_validate_step_binds_producer_revision(tmp_path): + """Only the exact live base/head merge revision can invoke the handler.""" + missing = _run_validate_step( + tmp_path / "missing", + {"SUPPLIED_PRODUCER_SOURCE_SHA": ""}, + _matching_pull_request(), + ) + wrong_revision = _run_validate_step( + tmp_path / "wrong-revision", + { + "SUPPLIED_PRODUCER_SOURCE_SHA": "d" * 40, + "FAKE_PRODUCER_COMMIT_JSON": json.dumps( + { + "sha": "d" * 40, + "parents": [{"sha": "a" * 40}, {"sha": "b" * 40}], + } + ), + }, + _matching_pull_request(), + ) + wrong_parents = _run_validate_step( + tmp_path / "wrong-parents", + { + "FAKE_PRODUCER_COMMIT_JSON": json.dumps( + { + "sha": "c" * 40, + "parents": [{"sha": "f" * 40}, {"sha": "b" * 40}], + } + ), + }, + _matching_pull_request(), + ) + + assert missing.returncode == 1 + assert wrong_revision.returncode == 1 + assert wrong_parents.returncode == 1 + assert "producer source" in missing.stdout.lower() + assert "producer revision" in wrong_revision.stdout.lower() + assert "producer revision" in wrong_parents.stdout.lower() + + +def test_codeql_scan_dispatch_accepts_exact_pull_request_merge_revision(tmp_path): + """Bind the producer revision to the live PR base/head merge, not handler ancestry.""" + merge_sha = "e" * 40 + pull_request = _matching_pull_request() + pull_request["merge_commit_sha"] = merge_sha + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_PRODUCER_SOURCE_SHA": merge_sha, + "FAKE_SOURCE_COMPARE_JSON": json.dumps( + { + "status": "diverged", + "behind_by": 1, + "base_commit": {"sha": "f" * 40}, + "merge_base_commit": {"sha": "f" * 40}, + } + ), + "FAKE_PRODUCER_COMMIT_JSON": json.dumps( + { + "sha": merge_sha, + "parents": [ + {"sha": "a" * 40}, + {"sha": "b" * 40}, + ], + } + ), + }, + pull_request, + ) + + assert result.returncode == 0, result.stdout + result.stderr + + +def test_codeql_scan_dispatch_validate_step_accepts_legacy_rerun_mode(tmp_path): + """An already queued top-level mode retains whole-attempt semantics.""" + result = _run_validate_step( + tmp_path, + {"SUPPLIED_RERUN_MODE": "all"}, + _matching_pull_request(), + ) + + assert result.returncode == 0, result.stderr + assert "rerun_mode=all" in result.output_path.read_text(encoding="utf-8") + + +def test_codeql_scan_dispatch_validate_step_rejects_conflicting_rerun_envelopes( + tmp_path, +): + """A caller cannot supply both legacy and nested rerun authority.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_RERUN_REQUEST": json.dumps( + { + "schema": "1", + "mode": "failed", + "required_jobs": [{"language": "python", "job_id": 43}], + } + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "conflicting legacy and nested rerun envelopes" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_unknown_rerun_mode(tmp_path): + """Only the two run-wide GitHub rerun operations are accepted.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_REQUIRED_JOBS": "null", + "SUPPLIED_RERUN_REQUEST": json.dumps( + { + "schema": "1", + "mode": "one-job", + "required_jobs": [{"language": "python", "job_id": 43}], + } + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "rerun mode" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_duplicate_job_id(tmp_path): + """Two language labels cannot authorize mutation of the same required job.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_MATRIX": json.dumps( + [ + {"language": "python", "build-mode": "none"}, + {"language": "actions", "build-mode": "none"}, + ] + ), + "SUPPLIED_REQUIRED_JOBS": json.dumps( + [ + {"language": "python", "job_id": 43}, + {"language": "actions", "job_id": 43}, + ] + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "wake identity is missing" in result.stdout + + def test_codeql_scan_dispatch_validate_step_rejects_actor_mismatch(tmp_path): """A dispatch from an unauthorized actor is rejected before any live PR read.""" result = _run_validate_step(tmp_path, {"DISPATCH_ACTOR": "someone-else"}, _matching_pull_request()) @@ -357,6 +733,28 @@ def test_codeql_scan_dispatch_validate_step_accepts_multi_language_payload(tmp_p assert '"job_id":43' in output_text.replace(" ", "") +def test_codeql_scan_dispatch_validate_step_rejects_unproven_matrix_subset(tmp_path): + """A partial scan cannot authorize waking an unscanned required language.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_MATRIX": json.dumps( + [{"language": "actions", "build-mode": "none"}] + ), + "SUPPLIED_REQUIRED_JOBS": json.dumps( + [ + {"language": "python", "job_id": 43}, + {"language": "actions", "job_id": 44}, + ] + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "does not match the dispatched languages one-to-one" in result.stdout + + def test_codeql_scan_dispatch_validate_step_accepts_legacy_single_language_payload(tmp_path): """A queued pre-cutover payload still validates after required_jobs became mandatory. @@ -534,65 +932,105 @@ def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> """ 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 + "\n\n settle-required-run:\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 "exact completed scan and preserved SARIF artifact remain" 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: +def test_dispatch_publish_rejects_superseded_metadata_and_legacy_context() -> None: + """A stale handler cannot poison HEAD or publish an unbound legacy status. + + Run 34235814716 proved that a scan can become superseded after initial + validation but before publication. #1902's evidence-complete producer is + integrated into the same successor, so publication requires successful + live-metadata revalidation and emits only the base-bound receipt. + """ workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - wake = workflow.split(" - name: Wake exact CodeQL required job\n", 1)[1].split( - "\n\n - name:", 1 + revalidate = workflow.split( + " - name: Re-validate live pull request metadata before privileged scan\n", + 1, + )[1].split(" - name: Fetch the pinned CodeQL SARIF gate script\n", 1)[0] + publish = workflow.split(" - name: Publish CodeQL dispatch status\n", 1)[1].split( + "\n\n settle-required-run:\n", 1 )[0] - assert "steps.publish_status.outcome == '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 '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 "while " not in wake - assert "sleep " not in wake - - -def test_dispatch_wake_has_only_trusted_actions_write_boundary() -> None: + assert " id: live_metadata\n" in revalidate + assert "if: always() && steps.live_metadata.outcome == 'success'" in publish + assert '-f context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}"' in publish + assert '-f context="codeql-dispatch/${LANGUAGE}"' not in publish + assert "SARIF_UPLOAD_OUTCOME: ${{ steps.sarif_upload.outcome }}" in publish + assert 'if [ "${SARIF_UPLOAD_OUTCOME:-}" != "success" ]; then' in publish + assert 'actual_creator="$(jq -r' in publish + assert "unexpected creator" in publish + + +def test_dispatch_settles_all_languages_with_one_run_wide_mutation() -> None: + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + settlement = workflow.split(" settle-required-run:\n", 1)[1] + + assert "needs: [validate-dispatch, scan]" in settlement + assert "always()" in settlement.split(" runs-on:", 1)[0] + assert "actions: write" in settlement.split(" steps:\n", 1)[0] + assert 'github_api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"' in settlement + assert 'github_api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}"' in settlement + assert 'github_api --paginate --slurp "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/jobs?per_page=100"' in settlement + assert "rerun-failed-jobs" in settlement + assert '"rerun"' in settlement + assert "actions/jobs/${REQUIRED_JOB_ID}/rerun" not in workflow + assert "sleep " not in settlement + + +def test_dispatch_settlement_has_only_trusted_actions_write_boundary() -> None: workflow = WORKFLOW_PATH.read_text(encoding="utf-8") scan = workflow.split(" scan:\n", 1)[1] scan_permissions = scan.split(" strategy:\n", 1)[0] + settlement = workflow.split(" settle-required-run:\n", 1)[1] + settlement_permissions = settlement.split(" steps:\n", 1)[0] - assert "actions: write" in scan_permissions + assert "actions: write" not in scan_permissions + assert "actions: read" in scan_permissions + assert "actions: write" in settlement_permissions assert "pull_request:" not in workflow assert "pull_request_target:" not in workflow - assert "needs.validate-dispatch.outputs.required_run_id != ''" in scan - assert "needs.validate-dispatch.outputs.required_jobs != ''" in scan + assert "needs.validate-dispatch.outputs.required_run_id" in settlement + assert "needs.validate-dispatch.outputs.required_jobs" in settlement assert "github.event.client_payload.required_job_id" not in scan -def _run_wake_step( +def _run_settlement_step( tmp_path: Path, *, pull: dict | None = None, run: dict | None = None, - job: dict | None = None, + required_jobs: list[dict] | None = None, + handler_jobs: list[dict] | None = None, + handler_artifacts: list[dict] | None = None, + extra_env: dict[str, str] | None = None, ) -> tuple[subprocess.CompletedProcess[str], Path]: - """Execute the exact wake block against fixture-backed GitHub API responses.""" + """Execute the run-wide settlement block against fixture-backed API responses.""" bash = shutil.which("bash") jq = shutil.which("jq") assert bash is not None and jq is not None, "bash and jq are required to run this test" head_sha = "b" * 40 - pull = pull or {"state": "open", "head": {"sha": head_sha}} + pull = pull or { + "state": "open", + "base": { + "repo": {"full_name": "ContextualWisdomLab/naruon"}, + "ref": "main", + "sha": "a" * 40, + }, + "head": { + "repo": {"full_name": "ContextualWisdomLab/naruon"}, + "ref": "feature", + "sha": head_sha, + }, + } run = run or { "id": 42, "event": "pull_request", @@ -601,16 +1039,60 @@ def _run_wake_step( "status": "completed", "conclusion": "failure", } - job = job or { - "id": 43, - "run_id": 42, - "head_sha": head_sha, - "name": "CodeQL compatibility analysis (python)", - "status": "completed", - "conclusion": "failure", - } + required_jobs = required_jobs or [ + { + "id": 43, + "run_id": 42, + "head_sha": head_sha, + "name": "CodeQL compatibility analysis (python)", + "status": "completed", + "conclusion": "failure", + }, + { + "id": 44, + "run_id": 42, + "head_sha": head_sha, + "name": "CodeQL compatibility analysis (actions)", + "status": "completed", + "conclusion": "failure", + }, + ] + handler_jobs = handler_jobs or [ + { + "name": "CodeQL dispatch scan (python)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [ + {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, + ], + }, + { + "name": "CodeQL dispatch scan (actions)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [ + {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, + ], + }, + ] + handler_artifacts = handler_artifacts or [ + { + "name": "codeql-dispatch-python-100-1", + "expired": False, + "size_in_bytes": 10, + }, + { + "name": "codeql-dispatch-actions-100-1", + "expired": False, + "size_in_bytes": 10, + }, + ] script = _extract_run_block( - WORKFLOW_PATH.read_text(encoding="utf-8"), "Wake exact CodeQL required job" + WORKFLOW_PATH.read_text(encoding="utf-8"), "Settle exact CodeQL required run" ) fake_bin = tmp_path / "bin" fake_bin.mkdir(parents=True) @@ -620,15 +1102,30 @@ def _run_wake_step( "#!/usr/bin/env bash\n" "set -euo pipefail\n" 'test "$1" = api\n' - 'if [ "${2:-}" = "-X" ]; then\n' - ' test "$3" = POST\n' - ' printf \'%s\\n\' "$4" >>"$FAKE_POST_LOG"\n' + 'endpoint="${!#}"\n' + 'if printf \'%s\\n\' "$@" | grep -qx POST; then\n' + ' printf \'%s\\n\' "$endpoint" >>"$FAKE_POST_LOG"\n' + ' if [ -n "${FAKE_WAKE_POST_FAIL_TOKEN:-}" ] && ' + '[ "${GH_TOKEN:-}" = "$FAKE_WAKE_POST_FAIL_TOKEN" ]; then\n' + " exit 1\n" + " fi\n" + ' if [ -n "${FAKE_DENIED_TOKEN:-}" ] && ' + '[ "${GH_TOKEN:-}" = "$FAKE_DENIED_TOKEN" ]; then\n' + " exit 1\n" + " fi\n" + ' if [ "${FAKE_WAKE_POST_FAIL_ALL:-}" = "1" ]; then\n' + " exit 1\n" + " fi\n" + ' test "${FAKE_POST_EXIT:-0}" = 0 || exit "$FAKE_POST_EXIT"\n' " exit 0\n" "fi\n" - 'case "$2" in\n' + 'test "${GH_TOKEN:-}" != "${FAKE_DENIED_TOKEN:-}" || exit 1\n' + 'case "$endpoint" 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' + ' repos/ContextualWisdomLab/naruon/actions/runs/42/jobs*) printf \'%s\\n\' "$FAKE_REQUIRED_JOB_PAGES" ;;\n' + ' repos/ContextualWisdomLab/naruon/actions/runs/42) printf \'%s\\n\' "$FAKE_RUN_JSON" ;;\n' + ' repos/ContextualWisdomLab/.github/actions/runs/100/jobs*) printf \'%s\\n\' "$FAKE_HANDLER_JOB_PAGES" ;;\n' + ' repos/ContextualWisdomLab/.github/actions/runs/100/artifacts*) printf \'%s\\n\' "$FAKE_HANDLER_ARTIFACT_PAGES" ;;\n' " *) exit 1 ;;\n" "esac\n", encoding="utf-8", @@ -639,12 +1136,28 @@ def _run_wake_step( "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps(pull), "FAKE_RUN_JSON": json.dumps(run), - "FAKE_JOB_JSON": json.dumps(job), + "FAKE_REQUIRED_JOB_PAGES": json.dumps([{"jobs": required_jobs}]), + "FAKE_HANDLER_JOB_PAGES": json.dumps([{"jobs": handler_jobs}]), + "FAKE_HANDLER_ARTIFACT_PAGES": json.dumps( + [{"artifacts": handler_artifacts}] + ), "FAKE_POST_LOG": str(post_log), + "FAKE_POST_EXIT": "0", + "FAKE_DENIED_TOKEN": "", "GH_TOKEN": "fake-token", - "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", + "TARGET_APP_WAKE_TOKEN": "", + "PR_REVIEW_MERGE_WAKE_TOKEN": "", + "OPENCODE_APPROVE_WAKE_TOKEN": "", + "GITHUB_WAKE_TOKEN": "fake-token", + "HANDLER_READ_TOKEN": "handler-token", + "GITHUB_REPOSITORY": "ContextualWisdomLab/.github", + "GITHUB_RUN_ID": "100", + "GITHUB_RUN_ATTEMPT": "1", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", "PR_NUMBER": "42", + "BASE_REF": "main", + "BASE_SHA": "a" * 40, + "HEAD_REF": "feature", "HEAD_SHA": head_sha, "REQUIRED_RUN_ID": "42", "REQUIRED_JOBS": json.dumps( @@ -653,28 +1166,126 @@ def _run_wake_step( {"language": "actions", "job_id": 44}, ] ), - "REQUIRED_LANGUAGE": "python", + "RERUN_MODE": "failed", } + if extra_env: + env.update(extra_env) result = subprocess.run( [bash], input=script, text=True, capture_output=True, check=False, env=env ) return result, post_log -def test_dispatch_wake_reruns_only_fixture_bound_exact_job(tmp_path: Path) -> None: - result, post_log = _run_wake_step(tmp_path) +def test_dispatch_settlement_reruns_two_languages_once(tmp_path: Path) -> None: + result, post_log = _run_settlement_step(tmp_path) + + assert result.returncode == 0, result.stderr + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs" + ] + + +def test_dispatch_settlement_fails_closed_when_no_credential( + tmp_path: Path, +) -> None: + result, post_log = _run_settlement_step( + tmp_path, + extra_env={ + "GH_TOKEN": "", + "TARGET_APP_WAKE_TOKEN": "", + "PR_REVIEW_MERGE_WAKE_TOKEN": "", + "OPENCODE_APPROVE_WAKE_TOKEN": "", + "GITHUB_WAKE_TOKEN": "", + }, + ) + + assert result.returncode == 1 + assert "could not read the current pull request" in result.stdout + assert not post_log.exists() + + +def test_dispatch_settlement_falls_back_when_target_app_token_cannot_rerun( + tmp_path: Path, +) -> None: + """A nonempty App token without Actions write must not shadow fallbacks.""" + result, post_log = _run_settlement_step( + tmp_path, + extra_env={ + "TARGET_APP_WAKE_TOKEN": "forbidden-app-token", + "PR_REVIEW_MERGE_WAKE_TOKEN": "actions-write-token", + "OPENCODE_APPROVE_WAKE_TOKEN": "", + "GITHUB_WAKE_TOKEN": "", + "GH_TOKEN": "", + "FAKE_WAKE_POST_FAIL_TOKEN": "forbidden-app-token", + }, + ) + + assert result.returncode == 0, result.stderr + assert ( + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs" + in post_log.read_text(encoding="utf-8") + ) + assert "pr-review-merge-token" in result.stdout + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + ] + + +def test_dispatch_settlement_fails_closed_after_every_wake_is_denied( + tmp_path: Path, +) -> None: + """A clean scan is not authoritative until one exact-job wake is accepted.""" + result, post_log = _run_settlement_step( + tmp_path, + extra_env={ + "TARGET_APP_WAKE_TOKEN": "app-token", + "PR_REVIEW_MERGE_WAKE_TOKEN": "merge-token", + "OPENCODE_APPROVE_WAKE_TOKEN": "approve-token", + "GITHUB_WAKE_TOKEN": "github-token", + "GH_TOKEN": "", + "FAKE_WAKE_POST_FAIL_ALL": "1", + }, + ) + + assert result.returncode == 1 + assert "could not enqueue verified run-wide recovery" in result.stdout + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + ] + + +def test_dispatch_settlement_retries_reads_with_next_configured_credential( + tmp_path: Path, +) -> None: + result, post_log = _run_settlement_step( + tmp_path, + extra_env={ + "GH_TOKEN": "target-token", + "TARGET_APP_WAKE_TOKEN": "target-token", + "PR_REVIEW_MERGE_WAKE_TOKEN": "fallback-token", + "OPENCODE_APPROVE_WAKE_TOKEN": "", + "GITHUB_WAKE_TOKEN": "", + "FAKE_DENIED_TOKEN": "target-token", + }, + ) assert result.returncode == 0, result.stderr + assert "pr-review-merge-token" in result.stdout 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", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", ] -def test_dispatch_wake_rejects_stale_head_and_closed_pr(tmp_path: Path) -> None: - stale_result, stale_log = _run_wake_step( +def test_dispatch_settlement_rejects_stale_head_and_closed_pr(tmp_path: Path) -> None: + stale_result, stale_log = _run_settlement_step( tmp_path / "stale", pull={"state": "open", "head": {"sha": "c" * 40}} ) - closed_result, closed_log = _run_wake_step( + closed_result, closed_log = _run_settlement_step( tmp_path / "closed", pull={"state": "closed", "head": {"sha": "b" * 40}} ) @@ -684,10 +1295,52 @@ 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: - wrong_job_result, wrong_job_log = _run_wake_step( - tmp_path / "wrong-job", - job={ +def test_dispatch_settlement_rejects_changed_repository_or_head_ref(tmp_path: Path) -> None: + """Settlement revalidates the complete live PR repository/ref identity.""" + wrong_repository, wrong_repository_log = _run_settlement_step( + tmp_path / "wrong-repository", + pull={ + "state": "open", + "base": {"repo": {"full_name": "ContextualWisdomLab/other"}, "ref": "main", "sha": "a" * 40}, + "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "feature", "sha": "b" * 40}, + }, + ) + changed_ref, changed_ref_log = _run_settlement_step( + tmp_path / "changed-ref", + pull={ + "state": "open", + "base": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "main", "sha": "a" * 40}, + "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "other", "sha": "b" * 40}, + }, + ) + + assert wrong_repository.returncode == 1 + assert changed_ref.returncode == 1 + assert not wrong_repository_log.exists() + assert not changed_ref_log.exists() + + +def test_dispatch_settlement_rejects_successful_required_run(tmp_path: Path) -> None: + """A completed success cannot be mutated as though it were a failed attempt.""" + result, post_log = _run_settlement_step( + tmp_path, + run={ + "id": 42, + "event": "pull_request", + "path": ".github/workflows/codeql-pr.yml", + "head_sha": "b" * 40, + "status": "completed", + "conclusion": "success", + }, + ) + + assert result.returncode == 1 + assert not post_log.exists() + + +def test_dispatch_settlement_rejects_wrong_or_nonfailed_job_identity(tmp_path: Path) -> None: + wrong_jobs = [ + { "id": 43, "run_id": 999, "head_sha": "b" * 40, @@ -695,42 +1348,171 @@ def test_dispatch_wake_rejects_ambiguous_or_nonfailed_job_identity(tmp_path: Pat "status": "completed", "conclusion": "failure", }, - ) - successful_job_result, successful_job_log = _run_wake_step( - tmp_path / "successful-job", - job={ - "id": 43, + { + "id": 44, "run_id": 42, "head_sha": "b" * 40, - "name": "CodeQL compatibility analysis (python)", + "name": "CodeQL compatibility analysis (actions)", "status": "completed", - "conclusion": "success", + "conclusion": "failure", }, + ] + wrong_job_result, wrong_job_log = _run_settlement_step( + tmp_path / "wrong-job", + required_jobs=wrong_jobs, + ) + successful_jobs = [dict(job) for job in wrong_jobs] + successful_jobs[0].update(run_id=42, conclusion="success") + successful_job_result, successful_job_log = _run_settlement_step( + tmp_path / "successful-job", + required_jobs=successful_jobs, ) 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 "missing or ambiguous exact job identity" 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.""" - result, post_log = _run_wake_step( - tmp_path, - run={ - "id": 42, - "event": "pull_request", - "path": ".github/workflows/codeql-pr.yml", +def test_dispatch_settlement_all_mode_reruns_success_and_failure_jobs(tmp_path: Path) -> None: + all_jobs = [ + { + "id": 43, + "run_id": 42, + "head_sha": "b" * 40, + "name": "CodeQL compatibility analysis (python)", + "status": "completed", + "conclusion": "success", + }, + { + "id": 44, + "run_id": 42, "head_sha": "b" * 40, - "status": "in_progress", - "conclusion": None, + "name": "CodeQL compatibility analysis (actions)", + "status": "completed", + "conclusion": "failure", }, + ] + result, post_log = _run_settlement_step( + tmp_path, + required_jobs=all_jobs, + extra_env={"RERUN_MODE": "all"}, ) assert result.returncode == 0, result.stderr - assert post_log.exists() + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun" + ] + + +def test_dispatch_settlement_rejects_missing_handler_artifact(tmp_path: Path) -> None: + result, post_log = _run_settlement_step( + tmp_path, + handler_artifacts=[ + { + "name": "codeql-dispatch-python-100-1", + "expired": False, + "size_in_bytes": 10, + } + ], + ) + + assert result.returncode == 1 + assert "incomplete handler gate or SARIF evidence for actions" in result.stdout + assert not post_log.exists() + + +def test_dispatch_settlement_rejects_missing_handler_gate_steps(tmp_path: Path) -> None: + """A terminal scan name alone is not authenticated gate evidence.""" + result, post_log = _run_settlement_step( + tmp_path, + handler_jobs=[ + { + "name": "CodeQL dispatch scan (python)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [], + }, + { + "name": "CodeQL dispatch scan (actions)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [ + {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, + ], + }, + ], + ) + + assert result.returncode == 1 + assert "incomplete handler gate or SARIF evidence for python" in result.stdout + assert not post_log.exists() + + +def test_dispatch_settlement_rejects_unproven_matrix_subset(tmp_path: Path) -> None: + """Every required shard needs current handler gate and artifact evidence.""" + result, post_log = _run_settlement_step( + tmp_path, + handler_jobs=[ + { + "name": "CodeQL dispatch scan (actions)", + "status": "completed", + "conclusion": "success", + } + ], + handler_artifacts=[ + { + "name": "codeql-dispatch-actions-100-1", + "expired": False, + "size_in_bytes": 10, + } + ], + ) + + assert result.returncode == 1 + assert "incomplete handler gate or SARIF evidence for python" in result.stdout + assert not post_log.exists() + + +def test_dispatch_settlement_rejects_unrelated_failed_job(tmp_path: Path) -> None: + unrelated = { + "id": 45, + "run_id": 42, + "head_sha": "b" * 40, + "name": "unrelated required job", + "status": "completed", + "conclusion": "failure", + } + result, post_log = _run_settlement_step( + tmp_path, + required_jobs=[ + { + "id": 43, + "run_id": 42, + "head_sha": "b" * 40, + "name": "CodeQL compatibility analysis (python)", + "status": "completed", + "conclusion": "failure", + }, + { + "id": 44, + "run_id": 42, + "head_sha": "b" * 40, + "name": "CodeQL compatibility analysis (actions)", + "status": "completed", + "conclusion": "failure", + }, + unrelated, + ], + ) + + assert result.returncode == 1 + assert "unrelated failed jobs" in result.stdout + assert not post_log.exists() def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: @@ -759,6 +1541,10 @@ def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: "SUPPLIED_REQUIRED_JOBS: ${{ toJSON(github.event.client_payload.required_jobs) }}" in workflow ), "SUPPLIED_REQUIRED_JOBS must be serialised with toJSON(); a bare array breaks template validation" + assert ( + "SUPPLIED_RERUN_REQUEST: ${{ toJSON(github.event.client_payload.rerun_request) }}" + in workflow + ), "The bounded nested rerun envelope must be serialized before shell validation" assert ( "SUPPLIED_REQUIRED_JOB_ID: ${{ github.event.client_payload.required_job_id || '' }}" in workflow @@ -767,3 +1553,6 @@ def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: "SUPPLIED_REQUIRED_LANGUAGE: ${{ github.event.client_payload.required_language || '' }}" in workflow ), "Queued pre-cutover payloads still supply required_language as a scalar" + assert "SUPPLIED_LEGACY_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }}" in workflow + assert "SUPPLIED_LEGACY_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }}" in workflow + assert "conflicting nested and legacy pr_head identity" in workflow diff --git a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py index ba0b2598a9..3070e7ff21 100644 --- a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py +++ b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py @@ -51,10 +51,10 @@ def test_codeql_pr_uses_explicit_supported_image(self) -> None: self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: - """Require both CodeQL Scan Dispatch jobs to pin Ubuntu 24.04.""" + """Require validation, scan, and attempt wake jobs to pin Ubuntu 24.04.""" workflow = CODEQL_SCAN_DISPATCH.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 2) + self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) def test_python_security_uses_explicit_supported_image(self) -> None: """Require all three Python Security jobs to pin Ubuntu 24.04.""" From 2351dc1388f9045dd5ff3db3588e8a2a114a2605 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:17:07 +0900 Subject: [PATCH 02/28] fix(codeql): stage versioned handler rollout bootstrap --- .github/workflows/codeql-scan-dispatch.yml | 450 ++++++++++++++++++--- 1 file changed, 394 insertions(+), 56 deletions(-) diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index c94fdf55c2..9146a7d5cc 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -16,9 +16,10 @@ 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.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 }} + github.event.client_payload.required_run_id || github.run_id }}/${{ + github.event.client_payload.producer_source_sha || 'missing-source' }} on: repository_dispatch: @@ -52,6 +53,9 @@ jobs: matrix: ${{ steps.validate.outputs.matrix }} required_run_id: ${{ steps.validate.outputs.required_run_id }} required_jobs: ${{ steps.validate.outputs.required_jobs }} + rerun_mode: ${{ steps.validate.outputs.rerun_mode }} + rerun_schema: ${{ steps.validate.outputs.rerun_schema }} + producer_source_sha: ${{ steps.validate.outputs.producer_source_sha }} steps: - name: Exchange OpenCode app token for target repository metadata reads id: metadata_read_app_token @@ -144,11 +148,18 @@ jobs: PR_NUMBER: ${{ github.event.client_payload.pr_number }} SUPPLIED_BASE_REF: ${{ github.event.client_payload.pr_base_ref || '' }} SUPPLIED_BASE_SHA: ${{ github.event.client_payload.pr_base_sha || '' }} - SUPPLIED_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }} - SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }} + SUPPLIED_HEAD_ENVELOPE: ${{ toJSON(github.event.client_payload.pr_head) }} + SUPPLIED_HEAD_SCHEMA: ${{ github.event.client_payload.pr_head.schema || '' }} + SUPPLIED_HEAD_REF: ${{ github.event.client_payload.pr_head.ref || github.event.client_payload.pr_head_ref || '' }} + SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head.sha || github.event.client_payload.pr_head_sha || '' }} + SUPPLIED_LEGACY_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }} + SUPPLIED_LEGACY_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }} + SUPPLIED_PRODUCER_SOURCE_SHA: ${{ github.event.client_payload.producer_source_sha || '' }} SUPPLIED_MATRIX: ${{ toJSON(github.event.client_payload.matrix) }} SUPPLIED_REQUIRED_RUN_ID: ${{ github.event.client_payload.required_run_id || '' }} SUPPLIED_REQUIRED_JOBS: ${{ toJSON(github.event.client_payload.required_jobs) }} + SUPPLIED_RERUN_MODE: ${{ github.event.client_payload.rerun_mode || '' }} + SUPPLIED_RERUN_REQUEST: ${{ toJSON(github.event.client_payload.rerun_request) }} # Pre-#2008 payloads still send scalar required_job_id + # required_language with a one-shard matrix. Synthesize # required_jobs from those only when the array is empty. @@ -177,14 +188,61 @@ jobs: fi printf 'Authorized repository_dispatch actor=%s sender=%s target=%s.\n' "$DISPATCH_ACTOR" "$DISPATCH_SENDER" "$TARGET_REPOSITORY" + if [ "$SUPPLIED_HEAD_ENVELOPE" != "null" ]; then + if [ "$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r ' + type == "object" + and ((.ref | type) == "string") + and ((.sha | type) == "string") + ' 2>/dev/null || true)" != "true" ]; then + printf '::error::repository_dispatch supplied invalid pr_head envelope; ref and sha must be strings.\n' + exit 1 + fi + envelope_schema_type="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.schema | type')" + if [ "$envelope_schema_type" = "null" ]; then + printf '::error::repository_dispatch supplied unsupported pr_head schema=.\n' + exit 1 + fi + if [ "$envelope_schema_type" != "string" ]; then + printf '::error::repository_dispatch supplied invalid pr_head envelope; schema must be a string.\n' + exit 1 + fi + envelope_schema="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.schema')" + envelope_ref="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.ref')" + envelope_sha="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.sha')" + if [ "$envelope_schema" != "1" ]; then + printf '::error::repository_dispatch supplied unsupported pr_head schema=%s.\n' "$envelope_schema" + exit 1 + fi + if [ "$SUPPLIED_HEAD_SCHEMA" != "$envelope_schema" ] || + [ "$SUPPLIED_HEAD_REF" != "$envelope_ref" ] || + [ "$SUPPLIED_HEAD_SHA" != "$envelope_sha" ]; then + printf '::error::repository_dispatch pr_head envelope disagrees with extracted workflow inputs.\n' + exit 1 + fi + if { [ -n "$SUPPLIED_LEGACY_HEAD_REF" ] || [ -n "$SUPPLIED_LEGACY_HEAD_SHA" ]; } && + { [ "$SUPPLIED_LEGACY_HEAD_REF" != "$envelope_ref" ] || + [ "$SUPPLIED_LEGACY_HEAD_SHA" != "$envelope_sha" ]; }; then + printf '::error::repository_dispatch rejected conflicting nested and legacy pr_head identity.\n' + exit 1 + fi + elif [ -n "$SUPPLIED_HEAD_SCHEMA" ]; then + printf '::error::repository_dispatch supplied unsupported pr_head schema=%s.\n' "$SUPPLIED_HEAD_SCHEMA" + exit 1 + fi + if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then printf '::error::PR metadata validation rejected a target outside ContextualWisdomLab or an invalid pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY:-}" "${PR_NUMBER:-}" exit 1 fi + if ! [[ "$SUPPLIED_PRODUCER_SOURCE_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::CodeQL producer source is missing or malformed." + exit 1 + fi matrix_json="$(printf '%s' "$SUPPLIED_MATRIX" | jq -c '.' 2>/dev/null || true)" jobs_json="$(printf '%s' "$SUPPLIED_REQUIRED_JOBS" | jq -c '.' 2>/dev/null || true)" + rerun_request_json="$(printf '%s' "$SUPPLIED_RERUN_REQUEST" | jq -c '.' 2>/dev/null || true)" if [ -z "$matrix_json" ] || [ "$(printf '%s' "$matrix_json" | jq 'type == "array" and length >= 1')" != "true" ] || [ "$(printf '%s' "$matrix_json" | jq '[.[] | select((.language | type == "string") and (.language | test("^[a-z0-9-]+$")) and (."build-mode" | type == "string"))] | length == ($ARGS.positional[0] | tonumber)' --args "$(printf '%s' "$matrix_json" | jq 'length')")" != "true" ] || @@ -192,6 +250,45 @@ jobs: printf '::error::CodeQL scan dispatch matrix must contain at least one valid language/build-mode shard with unique languages. matrix=%s\n' "${SUPPLIED_MATRIX:-}" exit 1 fi + rerun_mode="${SUPPLIED_RERUN_MODE:-failed}" + rerun_schema="legacy-0" + if [ "$rerun_mode" != "failed" ] && [ "$rerun_mode" != "all" ]; then + printf '::error::CodeQL rerun mode is invalid.\n' + exit 1 + fi + if [ -n "$rerun_request_json" ] && [ "$rerun_request_json" != "null" ]; then + if [ -n "$jobs_json" ] && [ "$(printf '%s' "$jobs_json" | jq '(. != null) and (. != [])')" = "true" ] || + [ -n "$SUPPLIED_RERUN_MODE" ] || [ -n "$SUPPLIED_REQUIRED_JOB_ID" ] || + [ -n "$SUPPLIED_REQUIRED_LANGUAGE" ]; then + printf '::error::CodeQL dispatch rejected conflicting legacy and nested rerun envelopes.\n' + exit 1 + fi + rerun_schema_type="$(printf '%s' "$rerun_request_json" | jq -r '.schema | type')" + if [ "$rerun_schema_type" = "null" ]; then + printf '::error::unsupported CodeQL rerun schema=.\n' + exit 1 + fi + if [ "$rerun_schema_type" != "string" ]; then + printf '::error::CodeQL rerun schema must be a string.\n' + exit 1 + fi + rerun_schema="$(printf '%s' "$rerun_request_json" | jq -r '.schema')" + if [ "$rerun_schema" != "1" ]; then + printf '::error::unsupported CodeQL rerun schema=%s.\n' "$rerun_schema" + exit 1 + fi + if [ "$(printf '%s' "$rerun_request_json" | jq ' + type == "object" + and ((keys | sort) == ["mode", "required_jobs", "schema"]) + and (.mode == "failed" or .mode == "all") + and (.required_jobs | type == "array") + ')" != "true" ]; then + printf '::error::CodeQL rerun mode or required job envelope is invalid.\n' + exit 1 + fi + rerun_mode="$(printf '%s' "$rerun_request_json" | jq -r '.mode')" + jobs_json="$(printf '%s' "$rerun_request_json" | jq -c '.required_jobs')" + fi if [ -z "$jobs_json" ] || [ "$(printf '%s' "$jobs_json" | jq '(. == null) or (. == [])')" = "true" ]; then if [ "$(printf '%s' "$matrix_json" | jq 'type == "array" and length == 1')" = "true" ] && @@ -214,6 +311,7 @@ jobs: )) and (($jobs | map(.language) | sort) == ($matrix | map(.language) | sort)) and (($jobs | map(.language) | unique | length) == ($jobs | length)) + and (($jobs | map(.job_id | tostring) | unique | length) == ($jobs | length)) ')" != "true" ]; then printf '::error::CodeQL wake identity is missing, non-canonical, or does not match the dispatched languages one-to-one.\n' exit 1 @@ -231,6 +329,7 @@ jobs: live_base_sha="$(jq -r '.base.sha // empty' <<<"$pull_request_json")" live_head_ref="$(jq -r '.head.ref // empty' <<<"$pull_request_json")" live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")" + live_merge_commit_sha="$(jq -r '.merge_commit_sha // empty' <<<"$pull_request_json")" live_state="$(jq -r '.state // empty' <<<"$pull_request_json")" if [ "$live_state" != "open" ] || @@ -253,6 +352,24 @@ jobs: printf '::error::repository_dispatch metadata does not match the live pull request: %s. supplied_base=%s/%s live_base=%s/%s supplied_head=%s/%s live_head=%s/%s\n' "$(IFS=,; printf '%s' "${mismatches[*]}")" "${SUPPLIED_BASE_REF:-}" "${SUPPLIED_BASE_SHA:-}" "$live_base_ref" "$live_base_sha" "${SUPPLIED_HEAD_REF:-}" "${SUPPLIED_HEAD_SHA:-}" "$live_head_ref" "$live_head_sha" exit 1 fi + if ! [[ "$live_merge_commit_sha" =~ ^[0-9a-fA-F]{40}$ ]] || + [ "${SUPPLIED_PRODUCER_SOURCE_SHA,,}" != "${live_merge_commit_sha,,}" ]; then + echo "::error::CodeQL producer revision does not match the live pull request merge revision." + exit 1 + fi + producer_commit_json="$(gh api "repos/${TARGET_REPOSITORY}/git/commits/${SUPPLIED_PRODUCER_SOURCE_SHA}")" + if ! printf '%s' "$producer_commit_json" | jq -e \ + --arg source "${SUPPLIED_PRODUCER_SOURCE_SHA,,}" \ + --arg base "${live_base_sha,,}" \ + --arg head "${live_head_sha,,}" ' + ((.sha // "" | ascii_downcase) == $source) + and ((.parents // []) | length == 2) + and ((.parents[0].sha // "" | ascii_downcase) == $base) + and ((.parents[1].sha // "" | ascii_downcase) == $head) + ' >/dev/null; then + echo "::error::CodeQL producer revision is not the exact live base/head merge." + exit 1 + fi { printf 'target_repository=%s\n' "$TARGET_REPOSITORY" @@ -265,6 +382,9 @@ jobs: printf '%s\n' "$matrix_json" echo "EOF" printf 'required_run_id=%s\n' "$SUPPLIED_REQUIRED_RUN_ID" + printf 'rerun_mode=%s\n' "$rerun_mode" + printf 'rerun_schema=%s\n' "$rerun_schema" + printf 'producer_source_sha=%s\n' "$SUPPLIED_PRODUCER_SOURCE_SHA" echo "required_jobs<>"$GITHUB_OUTPUT" - name: Re-validate live pull request metadata before privileged scan + id: live_metadata env: GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} @@ -428,16 +549,18 @@ jobs: run: python3 "$RUNNER_TEMP/codeql_sarif_gate.py" codeql-results-dispatch - name: Preserve CodeQL SARIF evidence + id: sarif_upload if: always() && hashFiles('codeql-results-dispatch/**/*.sarif') != '' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: codeql-dispatch-${{ matrix.language }}-${{ github.run_id }}-${{ github.run_attempt }} path: codeql-results-dispatch + if-no-files-found: error retention-days: 7 - name: Publish CodeQL dispatch status id: publish_status - if: always() + if: always() && steps.live_metadata.outcome == 'success' env: TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} GITHUB_STATUS_READ_TOKEN: ${{ github.token }} @@ -445,10 +568,18 @@ jobs: OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} HEAD_SHA: ${{ needs.validate-dispatch.outputs.head_sha }} + BASE_SHA: ${{ needs.validate-dispatch.outputs.base_sha }} + REQUIRED_RUN_ID: ${{ needs.validate-dispatch.outputs.required_run_id }} + PRODUCER_SOURCE_SHA: ${{ needs.validate-dispatch.outputs.producer_source_sha }} LANGUAGE: ${{ matrix.language }} GATE_OUTCOME: ${{ steps.gate.outcome }} + SARIF_UPLOAD_OUTCOME: ${{ steps.sarif_upload.outcome }} run: | set -euo pipefail + if [ "${SARIF_UPLOAD_OUTCOME:-}" != "success" ]; then + echo "::error::CodeQL SARIF evidence was not preserved; terminal status publication and exact-run settlement are blocked." + exit 1 + fi case "$GATE_OUTCOME" in success) state="success" @@ -463,6 +594,7 @@ jobs: description="CodeQL dispatch scan did not produce a verdict (${GATE_OUTCOME:-unknown})" ;; esac + receipt_description="cwl1;h=${HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${PRODUCER_SOURCE_SHA}" post_status() { token_label="$1" @@ -474,13 +606,34 @@ jobs: status_error="$(mktemp)" if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/statuses/${HEAD_SHA}" \ -f state="$state" \ - -f context="codeql-dispatch/${LANGUAGE}" \ - -f description="$description" \ + -f context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}" \ + -f description="$receipt_description" \ -f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ >"$status_response" 2>"$status_error"; then + actual_creator="$(jq -r '.creator.login // "" | ascii_downcase' "$status_response" 2>/dev/null || true)" + creator_trusted=false + case "$token_label" in + target-app-token|pr-review-merge-token|opencode-approve-token) + case "$actual_creator" in + opencode-agent|opencode-agent\[bot\]) creator_trusted=true ;; + esac + ;; + github-token) + if [ "${TARGET_REPOSITORY,,}" = "contextualwisdomlab/.github" ] && + [ "${GITHUB_REPOSITORY,,}" = "contextualwisdomlab/.github" ] && + [ "$actual_creator" = "github-actions[bot]" ]; then + creator_trusted=true + fi + ;; + esac + if [ "$creator_trusted" = true ]; then + rm -f "$status_response" "$status_error" + echo "Published CodeQL dispatch status to ${TARGET_REPOSITORY}@${HEAD_SHA} using ${token_label}." + return 0 + fi rm -f "$status_response" "$status_error" - echo "Published CodeQL dispatch status to ${TARGET_REPOSITORY}@${HEAD_SHA} using ${token_label}." - return 0 + echo "::notice::CodeQL dispatch status publish using ${token_label} returned unexpected creator=${actual_creator:-missing}; trying the next configured credential." + return 1 fi error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)" rm -f "$status_response" "$status_error" @@ -506,79 +659,264 @@ jobs: 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." + echo "::notice::Could not publish the CodeQL dispatch status after all configured credentials failed. The exact completed scan and preserved SARIF artifact remain the authenticated fallback evidence." 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 - - 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 != '' + settle-required-run: + name: settle exact required run + needs: [validate-dispatch, scan] + if: >- + always() + && needs.validate-dispatch.result == 'success' + && needs.scan.result != 'cancelled' + && needs.scan.result != 'skipped' + runs-on: ubuntu-24.04 + timeout-minutes: 8 + permissions: + actions: write + contents: read + id-token: write + steps: + - name: Exchange OpenCode app token for run settlement + id: target_app_token env: - GH_TOKEN: ${{ needs.validate-dispatch.outputs.target_repository == github.repository && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }} + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + run: | + set -euo pipefail + + mark_unavailable() { + echo "available=false" >>"$GITHUB_OUTPUT" + } + + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || + [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "OpenCode app token exchange unavailable: OIDC request environment is missing." + mark_unavailable + exit 0 + fi + + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + + if ! oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )"; then + echo "OpenCode app token exchange unavailable: OIDC token request did not complete." + mark_unavailable + exit 0 + fi + + oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" + if [ -z "$oidc_token" ]; then + echo "OpenCode app token exchange unavailable: OIDC token response was empty." + mark_unavailable + exit 0 + fi + + if ! token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )"; then + echo "OpenCode app token exchange unavailable: app token request did not complete." + mark_unavailable + exit 0 + fi + + app_token="$(jq -r '.token // empty' <<<"$token_response")" + if [ -z "$app_token" ]; then + echo "OpenCode app token exchange unavailable: app token response was empty." + mark_unavailable + exit 0 + fi + + echo "::add-mask::$app_token" + { + echo "available=true" + echo "token=$app_token" + } >>"$GITHUB_OUTPUT" + + - name: Settle exact CodeQL required run + env: + TARGET_APP_WAKE_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} + PR_REVIEW_MERGE_WAKE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} + OPENCODE_APPROVE_WAKE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} + GITHUB_WAKE_TOKEN: ${{ needs.validate-dispatch.outputs.target_repository == github.repository && github.token || '' }} + HANDLER_READ_TOKEN: ${{ github.token }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} PR_NUMBER: ${{ needs.validate-dispatch.outputs.pr_number }} + BASE_REF: ${{ needs.validate-dispatch.outputs.base_ref }} + BASE_SHA: ${{ needs.validate-dispatch.outputs.base_sha }} + HEAD_REF: ${{ needs.validate-dispatch.outputs.head_ref }} 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' }} + RERUN_MODE: ${{ needs.validate-dispatch.outputs.rerun_mode }} + PRODUCER_SOURCE_SHA: ${{ needs.validate-dispatch.outputs.producer_source_sha }} run: | set -euo pipefail - if [ -z "${GH_TOKEN:-}" ] || [ "$WAKE_TOKEN_SOURCE" = "unavailable" ]; then - echo "::error::Actions-capable CodeQL wake credential is unavailable." + + run_api() { + token_label="$1" + token="$2" + shift 2 + if [ -z "$token" ]; then + return 1 + fi + if GH_TOKEN="$token" gh api "$@"; then + echo "::notice::CodeQL settlement API used ${token_label}." >&2 + return 0 + fi + echo "::notice::CodeQL settlement API using ${token_label} did not succeed." >&2 + return 1 + } + + github_api() { + run_api "target-app-token" "$TARGET_APP_WAKE_TOKEN" "$@" || + run_api "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" "$@" || + run_api "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" "$@" || + run_api "github-token" "$GITHUB_WAKE_TOKEN" "$@" + } + + if ! pull="$(github_api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"; then + echo "::error::CodeQL settlement could not read the current pull request." 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 - echo "::error::CodeQL wake identity is non-canonical." + if [ "$(printf '%s' "$pull" | jq -r '.state // empty')" != "open" ] || + [ "$(printf '%s' "$pull" | jq -r '.base.repo.full_name // empty')" != "$TARGET_REPOSITORY" ] || + [ "$(printf '%s' "$pull" | jq -r '.base.ref // empty')" != "$BASE_REF" ] || + [ "$(printf '%s' "$pull" | jq -r '.base.sha // empty')" != "$BASE_SHA" ] || + [ "$(printf '%s' "$pull" | jq -r '.head.repo.full_name // empty')" != "$TARGET_REPOSITORY" ] || + [ "$(printf '%s' "$pull" | jq -r '.head.ref // empty')" != "$HEAD_REF" ] || + [ "$(printf '%s' "$pull" | jq -r '.head.sha // empty')" != "$HEAD_SHA" ]; then + echo "::error::CodeQL settlement rejected a closed PR, changed base, or stale head." exit 1 fi - pull="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" - live_state="$(printf '%s' "$pull" | jq -r '.state // empty')" - live_head="$(printf '%s' "$pull" | jq -r '.head.sha // empty')" - if [ "$live_state" != "open" ] || [ "$live_head" != "$HEAD_SHA" ]; then - echo "::error::CodeQL wake rejected a closed PR or stale head." + if ! required_run="$(github_api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}")"; then + echo "::error::CodeQL settlement could not read the required run." exit 1 fi - - run="$(gh api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}")" - run_identity="$(printf '%s' "$run" | jq -r --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" ' + if [ "$(printf '%s' "$required_run" | jq -r --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" ' select(.id == $run_id) | 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." + ')" != "$REQUIRED_RUN_ID" ]; then + echo "::error::CodeQL settlement rejected the required run identity." + exit 1 + fi + + if ! required_job_pages="$(github_api --paginate --slurp "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/jobs?per_page=100")"; then + echo "::error::CodeQL settlement could not read the required jobs." exit 1 fi + required_job_list="$(printf '%s' "$required_job_pages" | jq -c '[.[] | .jobs[]?]')" + while IFS= read -r required_job; do + language="$(printf '%s' "$required_job" | jq -r '.language // empty')" + job_id="$(printf '%s' "$required_job" | jq -r '.job_id // empty')" + expected_name="CodeQL compatibility analysis (${language})" + match_count="$(printf '%s' "$required_job_list" | jq --arg language "$language" --arg name "$expected_name" --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" --argjson job_id "$job_id" --arg mode "$RERUN_MODE" ' + [.[] | select( + .id == $job_id + and .run_id == $run_id + and .head_sha == $head + and .name == $name + and .status == "completed" + and ( + ($mode == "failed" and .conclusion == "failure") + or ($mode == "all" and (.conclusion == "success" or .conclusion == "failure")) + ) + )] | length + ')" + if [ "$match_count" -ne 1 ]; then + echo "::error::CodeQL settlement rejected missing or ambiguous exact job identity for ${language}." + exit 1 + fi + done < <(printf '%s' "$REQUIRED_JOBS" | jq -c '.[]') + + required_job_ids="$(printf '%s' "$REQUIRED_JOBS" | jq -c '[.[].job_id]')" + if [ "$RERUN_MODE" = "failed" ] && + [ "$(printf '%s' "$required_job_list" | jq --argjson required_ids "$required_job_ids" ' + [.[] | .id as $id | select(.status == "completed" and .conclusion == "failure" and ($required_ids | index($id) | not))] | length + ')" -ne 0 ]; then + echo "::error::CodeQL settlement rejected unrelated failed jobs outside the exact language map." + exit 1 + fi + + if ! handler_job_pages="$(GH_TOKEN="$HANDLER_READ_TOKEN" gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100")" || + ! handler_artifact_pages="$(GH_TOKEN="$HANDLER_READ_TOKEN" gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100")"; then + echo "::error::CodeQL settlement could not read exact handler evidence." + exit 1 + fi + handler_jobs="$(printf '%s' "$handler_job_pages" | jq -c '[.[] | .jobs[]?]')" + handler_artifacts="$(printf '%s' "$handler_artifact_pages" | jq -c '[.[] | .artifacts[]?]')" + while IFS= read -r required_job; do + language="$(printf '%s' "$required_job" | jq -r '.language')" + expected_job_name="CodeQL dispatch scan (${language})" + expected_artifact_name="codeql-dispatch-${language}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + handler_job_count="$(printf '%s' "$handler_jobs" | jq --arg name "$expected_job_name" --argjson attempt "$GITHUB_RUN_ATTEMPT" ' + [.[] | select( + .name == $name + and .status == "completed" + and (.conclusion == "success" or .conclusion == "failure") + and .run_attempt == $attempt + and ([.steps[]? | select(.name == "Enforce CodeQL Medium+ SARIF gate" and (.conclusion == "success" or .conclusion == "failure"))] | length) == 1 + and ([.steps[]? | select(.name == "Preserve CodeQL SARIF evidence" and .conclusion == "success")] | length) == 1 + )] | length + ')" + handler_artifact_count="$(printf '%s' "$handler_artifacts" | jq --arg name "$expected_artifact_name" ' + [.[] | select(.name == $name and (.expired == false) and (.size_in_bytes > 0))] | length + ')" + if [ "$handler_job_count" -ne 1 ] || [ "$handler_artifact_count" -ne 1 ]; then + echo "::error::CodeQL settlement rejected incomplete handler gate or SARIF evidence for ${language}." + exit 1 + fi + done < <(printf '%s' "$REQUIRED_JOBS" | jq -c '.[]') - 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}." + case "$RERUN_MODE" in + failed) rerun_endpoint="rerun-failed-jobs" ;; + all) rerun_endpoint="rerun" ;; + *) + echo "::error::CodeQL settlement rejected an unsupported rerun mode." + exit 1 + ;; + esac + + post_wake() { + token_label="$1" + token="$2" + if [ -z "$token" ]; then + return 1 + fi + if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/${rerun_endpoint}" >/dev/null; then + echo "Re-ran exact CodeQL required run ${REQUIRED_RUN_ID} mode=${RERUN_MODE} head=${HEAD_SHA} using ${token_label}." + return 0 + fi + echo "::notice::CodeQL settlement POST using ${token_label} did not succeed." + return 1 + } + + if post_wake "target-app-token" "$TARGET_APP_WAKE_TOKEN" || + post_wake "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" || + post_wake "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" || + post_wake "github-token" "$GITHUB_WAKE_TOKEN"; then + exit 0 + fi + + echo "::error::CodeQL settlement could not enqueue verified run-wide recovery." + exit 1 From c5b8d0049ee58a6b9328f10c568cb239424b876a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:17:28 +0900 Subject: [PATCH 03/28] docs(codeql): record handler-first rollout boundary --- CHANGELOG.md | 13 ++++++ ...required-workflow-dispatch-architecture.md | 40 +++++++++++++++++++ docs/product-technical-gap-baseline.md | 16 ++++++++ 3 files changed, 69 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf192f6a9e..a717e5516b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +### CodeQL handler rollout is staged before its producer + +- `codeql-scan-dispatch.yml` now owns one run-wide settlement after all + language shards finish, eliminating the observed split wake where actions + succeeded and Python received HTTP 403 on the same required run. +- Nested rerun authority requires exact schema `"1"`; the bounded top-level + form remains explicit `legacy-0`. Missing, numeric, unknown, or mixed schema + authority fails before checkout or Actions mutation. +- Exact live pull-request merge provenance, base/head/run/job identity, + terminal gate, SARIF preservation, returned status creator, and all-denied + credential behavior remain fail closed. This handler-only bootstrap is the + protected-main predecessor for `.github#2040`. + ### Failed-check finding names the Strix sandbox instead of the gateway - `opencode-review-dispatch.yml`'s `emit_strix_provider_failure_finding` rendered one fixed finding for every `STRIX_PROVIDER_UNAVAILABLE` line, whose Root cause read "The contextual-orchestrator gateway or its discovered provider pool was unavailable for this run". `#1953` had just given the Strix sandbox bootstrap failure its own second verdict token (`STRIX_SANDBOX_UNAVAILABLE`) precisely because that attribution is wrong for it -- the sandbox container never reaches its Caido proxy, so the run dies before the gateway serves anything -- and this consumer re-applied the wrong attribution one step downstream, into the review findings and the failure census. The emitter now branches on the second token: a sandbox verdict gets a finding that names Strix's sandbox, says the verdict does not name the gateway, and tells the reader not to change gateway or provider configuration on its strength. A `STRIX_PROVIDER_UNAVAILABLE` line without the token keeps its existing text verbatim, so the gateway class has no regression surface. No test covered this finding text at all before (`gateway or its discovered provider pool` matched nothing under `tests/`); `tests/test_opencode_dispatch_strix_sandbox_finding.py` now runs the production emitter from the published run block and pins both directions plus the no-signal case. Refs #1953, #1935. diff --git a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md index 5a11894767..4a291f951f 100644 --- a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md +++ b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md @@ -2,6 +2,46 @@ **Status:** Proposed, amended 2026-09-07 (one dispatch per pull request; language independence is the handler job matrix) · **Date:** 2026-09-03 · **Owner intent recorded:** loop-brief item 41 +## 2026-09-12 amendment — versioned handler-first rollout + +### Decision and sequence + +The protected handler must land before the producer that depends on its new +evidence and settlement contract. The bootstrap accepts exactly two rerun +protocols: the bounded top-level representation is identified as +`legacy-0`; the nested representation requires string schema `"1"` and exact +keys `schema`, `mode`, and `required_jobs`. Supplying both, omitting the nested +schema, or supplying a numeric or unknown schema fails before checkout or any +mutation. After the bootstrap merges ordinarily, `.github#2040` must be +non-force restacked and emit schema `"1"` for its nested request. + +One `settle-required-run` job runs after all scan shards. It authenticates the +live repository, pull request, base, head, required run, complete language/job +set, terminal Medium+ gates, preserved SARIF artifacts, producer merge +revision, and returned creator before issuing exactly one run-level +`rerun-failed-jobs` or whole-run `rerun`. Matrix jobs have `actions: read` and +cannot race each other at the mutation boundary. + +### Evidence, alternatives, and risks + +Protected handler run `34684228601` is the production RED: actions woke the +required run, then Python received HTTP 403 from the same matrix-owned wake +path. `.github#2040` CodeQL run `34684356386` repeated the non-terminal +consumer outcome on exact head `a9b18b4b24980c7ceb8b8cc0d143a24db20c90bf`. +Manual reruns, Draft/Ready toggles, synthetic statuses, and creator-only +head-bound receipts are rejected because they neither repair single-writer +settlement nor authenticate the evidence. An atomic producer+handler merge is +also rejected: `repository_dispatch` executes the handler from protected +default-branch source, so the first invocation cannot use the proposed +handler. + +The compatibility surface is temporary. `legacy-0` permits the staged +producer transition but does not authorize weaker status trust. Remove it +only after the schema-`"1"` producer is protected, queued legacy dispatches +have expired, and a fresh consumer canary proves terminal exact-head +settlement. Until protected integration and that canary, this amendment and +the bootstrap remain **Proposed**. + ## Problem `.github/workflows/codeql-pr.yml`'s `analyze-head`/`analyze-merge` jobs called diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1cc9e20313..ce56177104 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -7,6 +7,22 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +## 2026-09-12 — CodeQL handler-first rollout bootstrap (Proposed) + +- **Gap:** protected `.github/main@cb0872c9a20d5584703dffacca65c096fc034c6c` + still lets each CodeQL language matrix job mutate the same required run. + In handler run `34684228601`, actions woke the run and Python then received + HTTP 403. `.github#2040@a9b18b4b24980c7ceb8b8cc0d143a24db20c90bf` + reproduced the consumer failure in CodeQL run `34684356386`. +- **Owner repair:** land a handler-only protected-main predecessor with one + attempt-level settlement owner, exact evidence authentication, and explicit + rerun protocols: top-level `legacy-0` or nested schema `"1"`, never both. +- **Sequence:** ordinary-merge the bootstrap; non-force restack `.github#2040` + onto that protected revision; update its nested producer to schema `"1"`; + then obtain fresh exact-head producer→protected-handler evidence. +- **Status:** Proposed. Local RED→GREEN and repository verification do not + replace hosted exact-head Checks, independent review, or protected merge. + ## 1. 근거와 범위 ### 1.1 우선순위가 높은 근거 From a686a488fc65823549c0d902f37e300605f9e7e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:17:34 +0900 Subject: [PATCH 04/28] test(codeql): cap settlement below Actions rerun ceiling --- ..._codeql_scan_dispatch_workflow_contract.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 807ae6c4b1..75ea3516a9 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -1033,6 +1033,7 @@ def _run_settlement_step( } run = run or { "id": 42, + "run_attempt": 1, "event": "pull_request", "path": ".github/workflows/codeql-pr.yml", "head_sha": head_sha, @@ -1185,6 +1186,35 @@ def test_dispatch_settlement_reruns_two_languages_once(tmp_path: Path) -> None: ] +@pytest.mark.parametrize("run_attempt", [48, 49, 50]) +def test_dispatch_settlement_stops_before_github_rerun_ceiling( + tmp_path: Path, run_attempt: int +) -> None: + """An exhausted attempt budget fails before another Actions mutation.""" + result, post_log = _run_settlement_step( + tmp_path, + run={ + "id": 42, + "run_attempt": run_attempt, + "event": "pull_request", + "path": ".github/workflows/codeql-pr.yml", + "head_sha": "b" * 40, + "status": "completed", + "conclusion": "failure", + }, + extra_env={"RERUN_SCHEMA": "1"}, + ) + + assert result.returncode == 1 + assert not post_log.exists() + assert "phase=pre_mutation" in result.stdout + assert "reason=rerun_budget_exhausted" in result.stdout + assert "run_id=42" in result.stdout + assert f"run_attempt={run_attempt}" in result.stdout + assert "rerun_schema=1" in result.stdout + assert "languages=actions,python" in result.stdout + + def test_dispatch_settlement_fails_closed_when_no_credential( tmp_path: Path, ) -> None: From 6308f7aac08d5fc98ce8639c252949967db3b9fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:17:42 +0900 Subject: [PATCH 05/28] fix(codeql): stop settlement before rerun ceiling --- .github/workflows/codeql-scan-dispatch.yml | 14 ++++++++++++++ .../test_codeql_scan_dispatch_workflow_contract.py | 2 ++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 9146a7d5cc..13d355b29a 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -764,6 +764,8 @@ jobs: REQUIRED_RUN_ID: ${{ needs.validate-dispatch.outputs.required_run_id }} REQUIRED_JOBS: ${{ needs.validate-dispatch.outputs.required_jobs }} RERUN_MODE: ${{ needs.validate-dispatch.outputs.rerun_mode }} + RERUN_SCHEMA: ${{ needs.validate-dispatch.outputs.rerun_schema }} + MAX_CODEQL_RERUN_ATTEMPT: "48" PRODUCER_SOURCE_SHA: ${{ needs.validate-dispatch.outputs.producer_source_sha }} run: | set -euo pipefail @@ -815,12 +817,24 @@ jobs: | select(.path == ".github/workflows/codeql-pr.yml") | select(.head_sha == $head) | select(.status == "completed" and .conclusion == "failure") + | select((.run_attempt | type) == "number") + | select(.run_attempt == (.run_attempt | floor) and .run_attempt >= 1) | .id // empty ')" != "$REQUIRED_RUN_ID" ]; then echo "::error::CodeQL settlement rejected the required run identity." exit 1 fi + required_run_attempt="$(printf '%s' "$required_run" | jq -r '.run_attempt')" + if ! [[ "$MAX_CODEQL_RERUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]] || + [ "$required_run_attempt" -ge "$MAX_CODEQL_RERUN_ATTEMPT" ]; then + rerun_languages="$(printf '%s' "$REQUIRED_JOBS" | jq -r 'map(.language) | sort | join(",")')" + printf '::error::codeql_settlement phase=pre_mutation reason=rerun_budget_exhausted run_id=%s run_attempt=%s max_rerun_attempt=%s rerun_schema=%s languages=%s handler_run_id=%s handler_run_attempt=%s\n' \ + "$REQUIRED_RUN_ID" "$required_run_attempt" "$MAX_CODEQL_RERUN_ATTEMPT" \ + "$RERUN_SCHEMA" "$rerun_languages" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" + exit 1 + fi + if ! required_job_pages="$(github_api --paginate --slurp "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/jobs?per_page=100")"; then echo "::error::CodeQL settlement could not read the required jobs." exit 1 diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 75ea3516a9..3973037719 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -1168,6 +1168,8 @@ def _run_settlement_step( ] ), "RERUN_MODE": "failed", + "RERUN_SCHEMA": "legacy-0", + "MAX_CODEQL_RERUN_ATTEMPT": "48", } if extra_env: env.update(extra_env) From 99bc9dbd3d2526c68c4dcae5600c0a1d98b58b69 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:18:04 +0900 Subject: [PATCH 06/28] docs(codeql): record rerun exhaustion boundary --- CHANGELOG.md | 3 +++ ...odeql-required-workflow-dispatch-architecture.md | 13 +++++++++++++ docs/product-technical-gap-baseline.md | 2 ++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a717e5516b..880cf5aa40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ terminal gate, SARIF preservation, returned status creator, and all-denied credential behavior remain fail closed. This handler-only bootstrap is the protected-main predecessor for `.github#2040`. +- Settlement refuses another mutation at required-run attempt 48 or later, + leaving margin below GitHub's 50-rerun platform ceiling and emitting exact + run, attempt, schema, language, and handler-attempt failure telemetry. ### Failed-check finding names the Strix sandbox instead of the gateway diff --git a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md index 4a291f951f..566e99afea 100644 --- a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md +++ b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md @@ -22,12 +22,25 @@ revision, and returned creator before issuing exactly one run-level `rerun-failed-jobs` or whole-run `rerun`. Matrix jobs have `actions: read` and cannot race each other at the mutation boundary. +The same owner refuses another mutation at required-run attempt 48 or later. +GitHub permits at most 50 reruns of one workflow run; stopping below that +platform ceiling prevents the recovery mechanism from consuming the final +attempts and turning a repairable owner defect into a zero-job +`startup_failure`. Exhaustion is a deterministic non-passing result with +`phase=pre_mutation`, reason, exact run ID and attempt, schema, sorted language +set, and handler run/attempt. It never becomes a success receipt. A real source +repair creates a fresh exact-head run; no no-op commit or manual rerun is part +of recovery. + ### Evidence, alternatives, and risks Protected handler run `34684228601` is the production RED: actions woke the required run, then Python received HTTP 403 from the same matrix-owned wake path. `.github#2040` CodeQL run `34684356386` repeated the non-terminal consumer outcome on exact head `a9b18b4b24980c7ceb8b8cc0d143a24db20c90bf`. +The predecessor required run `34629071379` reached `run_attempt=50` and then +ended as a zero-job `startup_failure` despite authenticated language scan +evidence, proving that an unbounded wake loop can exhaust the platform limit. Manual reruns, Draft/Ready toggles, synthetic statuses, and creator-only head-bound receipts are rejected because they neither repair single-writer settlement nor authenticate the evidence. An atomic producer+handler merge is diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ce56177104..cee3dee1ed 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -17,6 +17,8 @@ - **Owner repair:** land a handler-only protected-main predecessor with one attempt-level settlement owner, exact evidence authentication, and explicit rerun protocols: top-level `legacy-0` or nested schema `"1"`, never both. + Refuse a new mutation when `run_attempt >= 48`, before GitHub's 50-rerun + ceiling, and report exact phase/reason/run/attempt/schema/languages/handler. - **Sequence:** ordinary-merge the bootstrap; non-force restack `.github#2040` onto that protected revision; update its nested producer to schema `"1"`; then obtain fresh exact-head producer→protected-handler evidence. From 1d3f5108c90332a7029498c093d0ff23fc7ad605 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:28:45 +0900 Subject: [PATCH 07/28] fix(codeql): add versioned single-settlement handler --- .github/workflows/codeql-scan-dispatch.yml | 487 ++++++++++++++++++--- 1 file changed, 429 insertions(+), 58 deletions(-) diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index c94fdf55c2..23154ebaec 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -12,17 +12,24 @@ # Exercise this handler end-to-end by POSTing a real repository_dispatch # event instead -- that always runs the default-branch version. name: CodeQL Scan Dispatch +# LEGACY_V1_REMOVAL_CONDITION: remove codeql-scan:legacy-v1 only after the +# protected v2 producer has landed and every in-flight v1 required run has +# reached a terminal conclusion. Both protocols share this protected handler. 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_base_sha || 'none' }}/${{ - github.event.client_payload.required_run_id || github.run_id }} + github.event.client_payload.pr_head.sha || github.event.client_payload.pr_head_sha || github.sha }}/${{ + github.event.action == 'codeql-scan-v2' && + format('{0}/{1}/{2}', github.event.client_payload.pr_base_sha || 'none', + github.event.client_payload.required_run_id || github.run_id, + github.event.client_payload.producer_source_sha || 'missing-source') || + format('{0}/{1}', github.event.client_payload.pr_base_sha || 'none', + github.event.client_payload.required_run_id || github.run_id) }} on: repository_dispatch: - types: [codeql-scan] + types: [codeql-scan, codeql-scan-v2] concurrency: group: >- @@ -52,6 +59,9 @@ jobs: matrix: ${{ steps.validate.outputs.matrix }} required_run_id: ${{ steps.validate.outputs.required_run_id }} required_jobs: ${{ steps.validate.outputs.required_jobs }} + rerun_mode: ${{ steps.validate.outputs.rerun_mode }} + producer_source_sha: ${{ steps.validate.outputs.producer_source_sha }} + dispatch_protocol: ${{ steps.validate.outputs.dispatch_protocol }} steps: - name: Exchange OpenCode app token for target repository metadata reads id: metadata_read_app_token @@ -140,15 +150,23 @@ jobs: DISPATCH_ACTOR: ${{ github.triggering_actor }} DISPATCH_SENDER: ${{ github.event.sender.login || '' }} ALLOWED_DISPATCH_ACTOR: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_ACTOR }} + DISPATCH_PROTOCOL: ${{ github.event.action }} TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository }} PR_NUMBER: ${{ github.event.client_payload.pr_number }} SUPPLIED_BASE_REF: ${{ github.event.client_payload.pr_base_ref || '' }} SUPPLIED_BASE_SHA: ${{ github.event.client_payload.pr_base_sha || '' }} - SUPPLIED_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }} - SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }} + SUPPLIED_HEAD_ENVELOPE: ${{ toJSON(github.event.client_payload.pr_head) }} + SUPPLIED_HEAD_SCHEMA: ${{ github.event.client_payload.pr_head.schema || '' }} + SUPPLIED_HEAD_REF: ${{ github.event.client_payload.pr_head.ref || github.event.client_payload.pr_head_ref || '' }} + SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head.sha || github.event.client_payload.pr_head_sha || '' }} + SUPPLIED_LEGACY_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }} + SUPPLIED_LEGACY_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }} + SUPPLIED_PRODUCER_SOURCE_SHA: ${{ github.event.client_payload.producer_source_sha || '' }} SUPPLIED_MATRIX: ${{ toJSON(github.event.client_payload.matrix) }} SUPPLIED_REQUIRED_RUN_ID: ${{ github.event.client_payload.required_run_id || '' }} SUPPLIED_REQUIRED_JOBS: ${{ toJSON(github.event.client_payload.required_jobs) }} + SUPPLIED_RERUN_MODE: ${{ github.event.client_payload.rerun_mode || '' }} + SUPPLIED_RERUN_REQUEST: ${{ toJSON(github.event.client_payload.rerun_request) }} # Pre-#2008 payloads still send scalar required_job_id + # required_language with a one-shard matrix. Synthesize # required_jobs from those only when the array is empty. @@ -177,14 +195,86 @@ jobs: fi printf 'Authorized repository_dispatch actor=%s sender=%s target=%s.\n' "$DISPATCH_ACTOR" "$DISPATCH_SENDER" "$TARGET_REPOSITORY" + case "$DISPATCH_PROTOCOL" in + codeql-scan) + dispatch_protocol=legacy-v1 + if [ "$SUPPLIED_HEAD_ENVELOPE" != "null" ] || + [ -n "$SUPPLIED_PRODUCER_SOURCE_SHA" ] || + { [ -n "$SUPPLIED_RERUN_REQUEST" ] && [ "$SUPPLIED_RERUN_REQUEST" != "null" ]; } || + [ -n "$SUPPLIED_RERUN_MODE" ]; then + echo "::error::Legacy CodeQL dispatch rejected v2-only identity fields." + exit 1 + fi + ;; + codeql-scan-v2) + dispatch_protocol=v2 + if [ "$SUPPLIED_HEAD_ENVELOPE" = "null" ]; then + echo "::error::CodeQL v2 dispatch requires the versioned pr_head envelope." + exit 1 + fi + ;; + *) + echo "::error::CodeQL dispatch protocol is unsupported." + exit 1 + ;; + esac + + if [ "$SUPPLIED_HEAD_ENVELOPE" != "null" ]; then + if [ "$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r ' + type == "object" + and ((.ref | type) == "string") + and ((.sha | type) == "string") + ' 2>/dev/null || true)" != "true" ]; then + printf '::error::repository_dispatch supplied invalid pr_head envelope; ref and sha must be strings.\n' + exit 1 + fi + envelope_schema_type="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.schema | type')" + if [ "$envelope_schema_type" = "null" ]; then + printf '::error::repository_dispatch supplied unsupported pr_head schema=.\n' + exit 1 + fi + if [ "$envelope_schema_type" != "string" ]; then + printf '::error::repository_dispatch supplied invalid pr_head envelope; schema must be a string.\n' + exit 1 + fi + envelope_schema="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.schema')" + envelope_ref="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.ref')" + envelope_sha="$(printf '%s' "$SUPPLIED_HEAD_ENVELOPE" | jq -r '.sha')" + if [ "$envelope_schema" != "1" ]; then + printf '::error::repository_dispatch supplied unsupported pr_head schema=%s.\n' "$envelope_schema" + exit 1 + fi + if [ "$SUPPLIED_HEAD_SCHEMA" != "$envelope_schema" ] || + [ "$SUPPLIED_HEAD_REF" != "$envelope_ref" ] || + [ "$SUPPLIED_HEAD_SHA" != "$envelope_sha" ]; then + printf '::error::repository_dispatch pr_head envelope disagrees with extracted workflow inputs.\n' + exit 1 + fi + if { [ -n "$SUPPLIED_LEGACY_HEAD_REF" ] || [ -n "$SUPPLIED_LEGACY_HEAD_SHA" ]; } && + { [ "$SUPPLIED_LEGACY_HEAD_REF" != "$envelope_ref" ] || + [ "$SUPPLIED_LEGACY_HEAD_SHA" != "$envelope_sha" ]; }; then + printf '::error::repository_dispatch rejected conflicting nested and legacy pr_head identity.\n' + exit 1 + fi + elif [ -n "$SUPPLIED_HEAD_SCHEMA" ]; then + printf '::error::repository_dispatch supplied unsupported pr_head schema=%s.\n' "$SUPPLIED_HEAD_SCHEMA" + exit 1 + fi + if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then printf '::error::PR metadata validation rejected a target outside ContextualWisdomLab or an invalid pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY:-}" "${PR_NUMBER:-}" exit 1 fi + if [ "$dispatch_protocol" = v2 ] && + ! [[ "$SUPPLIED_PRODUCER_SOURCE_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::CodeQL producer source is missing or malformed." + exit 1 + fi matrix_json="$(printf '%s' "$SUPPLIED_MATRIX" | jq -c '.' 2>/dev/null || true)" jobs_json="$(printf '%s' "$SUPPLIED_REQUIRED_JOBS" | jq -c '.' 2>/dev/null || true)" + rerun_request_json="$(printf '%s' "$SUPPLIED_RERUN_REQUEST" | jq -c '.' 2>/dev/null || true)" if [ -z "$matrix_json" ] || [ "$(printf '%s' "$matrix_json" | jq 'type == "array" and length >= 1')" != "true" ] || [ "$(printf '%s' "$matrix_json" | jq '[.[] | select((.language | type == "string") and (.language | test("^[a-z0-9-]+$")) and (."build-mode" | type == "string"))] | length == ($ARGS.positional[0] | tonumber)' --args "$(printf '%s' "$matrix_json" | jq 'length')")" != "true" ] || @@ -192,6 +282,30 @@ jobs: printf '::error::CodeQL scan dispatch matrix must contain at least one valid language/build-mode shard with unique languages. matrix=%s\n' "${SUPPLIED_MATRIX:-}" exit 1 fi + rerun_mode="${SUPPLIED_RERUN_MODE:-failed}" + if [ "$rerun_mode" != "failed" ] && [ "$rerun_mode" != "all" ]; then + printf '::error::CodeQL rerun mode is invalid.\n' + exit 1 + fi + if [ -n "$rerun_request_json" ] && [ "$rerun_request_json" != "null" ]; then + if [ -n "$jobs_json" ] && [ "$(printf '%s' "$jobs_json" | jq '(. != null) and (. != [])')" = "true" ] || + [ -n "$SUPPLIED_RERUN_MODE" ] || [ -n "$SUPPLIED_REQUIRED_JOB_ID" ] || + [ -n "$SUPPLIED_REQUIRED_LANGUAGE" ]; then + printf '::error::CodeQL dispatch rejected conflicting legacy and nested rerun envelopes.\n' + exit 1 + fi + if [ "$(printf '%s' "$rerun_request_json" | jq ' + type == "object" + and ((keys | sort) == ["mode", "required_jobs"]) + and (.mode == "failed" or .mode == "all") + and (.required_jobs | type == "array") + ')" != "true" ]; then + printf '::error::CodeQL rerun mode or required job envelope is invalid.\n' + exit 1 + fi + rerun_mode="$(printf '%s' "$rerun_request_json" | jq -r '.mode')" + jobs_json="$(printf '%s' "$rerun_request_json" | jq -c '.required_jobs')" + fi if [ -z "$jobs_json" ] || [ "$(printf '%s' "$jobs_json" | jq '(. == null) or (. == [])')" = "true" ]; then if [ "$(printf '%s' "$matrix_json" | jq 'type == "array" and length == 1')" = "true" ] && @@ -214,6 +328,7 @@ jobs: )) and (($jobs | map(.language) | sort) == ($matrix | map(.language) | sort)) and (($jobs | map(.language) | unique | length) == ($jobs | length)) + and (($jobs | map(.job_id | tostring) | unique | length) == ($jobs | length)) ')" != "true" ]; then printf '::error::CodeQL wake identity is missing, non-canonical, or does not match the dispatched languages one-to-one.\n' exit 1 @@ -231,6 +346,7 @@ jobs: live_base_sha="$(jq -r '.base.sha // empty' <<<"$pull_request_json")" live_head_ref="$(jq -r '.head.ref // empty' <<<"$pull_request_json")" live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")" + live_merge_commit_sha="$(jq -r '.merge_commit_sha // empty' <<<"$pull_request_json")" live_state="$(jq -r '.state // empty' <<<"$pull_request_json")" if [ "$live_state" != "open" ] || @@ -253,6 +369,26 @@ jobs: printf '::error::repository_dispatch metadata does not match the live pull request: %s. supplied_base=%s/%s live_base=%s/%s supplied_head=%s/%s live_head=%s/%s\n' "$(IFS=,; printf '%s' "${mismatches[*]}")" "${SUPPLIED_BASE_REF:-}" "${SUPPLIED_BASE_SHA:-}" "$live_base_ref" "$live_base_sha" "${SUPPLIED_HEAD_REF:-}" "${SUPPLIED_HEAD_SHA:-}" "$live_head_ref" "$live_head_sha" exit 1 fi + if [ "$dispatch_protocol" = v2 ]; then + if ! [[ "$live_merge_commit_sha" =~ ^[0-9a-fA-F]{40}$ ]] || + [ "${SUPPLIED_PRODUCER_SOURCE_SHA,,}" != "${live_merge_commit_sha,,}" ]; then + echo "::error::CodeQL producer revision does not match the live pull request merge revision." + exit 1 + fi + producer_commit_json="$(gh api "repos/${TARGET_REPOSITORY}/git/commits/${SUPPLIED_PRODUCER_SOURCE_SHA}")" + if ! printf '%s' "$producer_commit_json" | jq -e \ + --arg source "${SUPPLIED_PRODUCER_SOURCE_SHA,,}" \ + --arg base "${live_base_sha,,}" \ + --arg head "${live_head_sha,,}" ' + ((.sha // "" | ascii_downcase) == $source) + and ((.parents // []) | length == 2) + and ((.parents[0].sha // "" | ascii_downcase) == $base) + and ((.parents[1].sha // "" | ascii_downcase) == $head) + ' >/dev/null; then + echo "::error::CodeQL producer revision is not the exact live base/head merge." + exit 1 + fi + fi { printf 'target_repository=%s\n' "$TARGET_REPOSITORY" @@ -265,6 +401,9 @@ jobs: printf '%s\n' "$matrix_json" echo "EOF" printf 'required_run_id=%s\n' "$SUPPLIED_REQUIRED_RUN_ID" + printf 'rerun_mode=%s\n' "$rerun_mode" + printf 'producer_source_sha=%s\n' "$SUPPLIED_PRODUCER_SOURCE_SHA" + printf 'dispatch_protocol=%s\n' "$dispatch_protocol" echo "required_jobs<>"$GITHUB_OUTPUT" - name: Re-validate live pull request metadata before privileged scan + id: live_metadata env: GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} @@ -428,16 +568,18 @@ jobs: run: python3 "$RUNNER_TEMP/codeql_sarif_gate.py" codeql-results-dispatch - name: Preserve CodeQL SARIF evidence + id: sarif_upload if: always() && hashFiles('codeql-results-dispatch/**/*.sarif') != '' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: codeql-dispatch-${{ matrix.language }}-${{ github.run_id }}-${{ github.run_attempt }} path: codeql-results-dispatch + if-no-files-found: error retention-days: 7 - name: Publish CodeQL dispatch status id: publish_status - if: always() + if: always() && steps.live_metadata.outcome == 'success' env: TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} GITHUB_STATUS_READ_TOKEN: ${{ github.token }} @@ -445,10 +587,19 @@ jobs: OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} HEAD_SHA: ${{ needs.validate-dispatch.outputs.head_sha }} + BASE_SHA: ${{ needs.validate-dispatch.outputs.base_sha }} + REQUIRED_RUN_ID: ${{ needs.validate-dispatch.outputs.required_run_id }} + PRODUCER_SOURCE_SHA: ${{ needs.validate-dispatch.outputs.producer_source_sha }} + DISPATCH_PROTOCOL: ${{ needs.validate-dispatch.outputs.dispatch_protocol }} LANGUAGE: ${{ matrix.language }} GATE_OUTCOME: ${{ steps.gate.outcome }} + SARIF_UPLOAD_OUTCOME: ${{ steps.sarif_upload.outcome }} run: | set -euo pipefail + if [ "${SARIF_UPLOAD_OUTCOME:-}" != "success" ]; then + echo "::error::CodeQL SARIF evidence was not preserved; terminal status publication and exact-run settlement are blocked." + exit 1 + fi case "$GATE_OUTCOME" in success) state="success" @@ -463,6 +614,20 @@ jobs: description="CodeQL dispatch scan did not produce a verdict (${GATE_OUTCOME:-unknown})" ;; esac + case "$DISPATCH_PROTOCOL" in + legacy-v1) + receipt_context="codeql-dispatch/${LANGUAGE}" + receipt_description="$description" + ;; + v2) + receipt_context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}" + receipt_description="cwl1;h=${HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${PRODUCER_SOURCE_SHA}" + ;; + *) + echo "::error::CodeQL status publication rejected an unknown dispatch protocol." + exit 1 + ;; + esac post_status() { token_label="$1" @@ -474,13 +639,34 @@ jobs: status_error="$(mktemp)" if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/statuses/${HEAD_SHA}" \ -f state="$state" \ - -f context="codeql-dispatch/${LANGUAGE}" \ - -f description="$description" \ + -f context="$receipt_context" \ + -f description="$receipt_description" \ -f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ >"$status_response" 2>"$status_error"; then + actual_creator="$(jq -r '.creator.login // "" | ascii_downcase' "$status_response" 2>/dev/null || true)" + creator_trusted=false + case "$token_label" in + target-app-token|pr-review-merge-token|opencode-approve-token) + case "$actual_creator" in + opencode-agent|opencode-agent\[bot\]) creator_trusted=true ;; + esac + ;; + github-token) + if [ "${TARGET_REPOSITORY,,}" = "contextualwisdomlab/.github" ] && + [ "${GITHUB_REPOSITORY,,}" = "contextualwisdomlab/.github" ] && + [ "$actual_creator" = "github-actions[bot]" ]; then + creator_trusted=true + fi + ;; + esac + if [ "$creator_trusted" = true ]; then + rm -f "$status_response" "$status_error" + echo "Published CodeQL dispatch status to ${TARGET_REPOSITORY}@${HEAD_SHA} using ${token_label}." + return 0 + fi rm -f "$status_response" "$status_error" - echo "Published CodeQL dispatch status to ${TARGET_REPOSITORY}@${HEAD_SHA} using ${token_label}." - return 0 + echo "::notice::CodeQL dispatch status publish using ${token_label} returned unexpected creator=${actual_creator:-missing}; trying the next configured credential." + return 1 fi error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)" rm -f "$status_response" "$status_error" @@ -506,79 +692,264 @@ jobs: 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." + echo "::notice::Could not publish the CodeQL dispatch status after all configured credentials failed. The exact completed scan and preserved SARIF artifact remain the authenticated fallback evidence." 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 - - 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 != '' + settle-required-run: + name: settle exact required run + needs: [validate-dispatch, scan] + if: >- + always() + && needs.validate-dispatch.result == 'success' + && needs.scan.result != 'cancelled' + && needs.scan.result != 'skipped' + runs-on: ubuntu-24.04 + timeout-minutes: 8 + permissions: + actions: write + contents: read + id-token: write + steps: + - name: Exchange OpenCode app token for run settlement + id: target_app_token + env: + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + run: | + set -euo pipefail + + mark_unavailable() { + echo "available=false" >>"$GITHUB_OUTPUT" + } + + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || + [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "OpenCode app token exchange unavailable: OIDC request environment is missing." + mark_unavailable + exit 0 + fi + + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + + if ! oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )"; then + echo "OpenCode app token exchange unavailable: OIDC token request did not complete." + mark_unavailable + exit 0 + fi + + oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" + if [ -z "$oidc_token" ]; then + echo "OpenCode app token exchange unavailable: OIDC token response was empty." + mark_unavailable + exit 0 + fi + + if ! token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )"; then + echo "OpenCode app token exchange unavailable: app token request did not complete." + mark_unavailable + exit 0 + fi + + app_token="$(jq -r '.token // empty' <<<"$token_response")" + if [ -z "$app_token" ]; then + echo "OpenCode app token exchange unavailable: app token response was empty." + mark_unavailable + exit 0 + fi + + echo "::add-mask::$app_token" + { + echo "available=true" + echo "token=$app_token" + } >>"$GITHUB_OUTPUT" + + - name: Settle exact CodeQL required run env: - GH_TOKEN: ${{ needs.validate-dispatch.outputs.target_repository == github.repository && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN }} + TARGET_APP_WAKE_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} + PR_REVIEW_MERGE_WAKE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} + OPENCODE_APPROVE_WAKE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} + GITHUB_WAKE_TOKEN: ${{ needs.validate-dispatch.outputs.target_repository == github.repository && github.token || '' }} + HANDLER_READ_TOKEN: ${{ github.token }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} PR_NUMBER: ${{ needs.validate-dispatch.outputs.pr_number }} + BASE_REF: ${{ needs.validate-dispatch.outputs.base_ref }} + BASE_SHA: ${{ needs.validate-dispatch.outputs.base_sha }} + HEAD_REF: ${{ needs.validate-dispatch.outputs.head_ref }} 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' }} + RERUN_MODE: ${{ needs.validate-dispatch.outputs.rerun_mode }} + PRODUCER_SOURCE_SHA: ${{ needs.validate-dispatch.outputs.producer_source_sha }} run: | set -euo pipefail - if [ -z "${GH_TOKEN:-}" ] || [ "$WAKE_TOKEN_SOURCE" = "unavailable" ]; then - echo "::error::Actions-capable CodeQL wake credential is unavailable." + + run_api() { + token_label="$1" + token="$2" + shift 2 + if [ -z "$token" ]; then + return 1 + fi + if GH_TOKEN="$token" gh api "$@"; then + echo "::notice::CodeQL settlement API used ${token_label}." >&2 + return 0 + fi + echo "::notice::CodeQL settlement API using ${token_label} did not succeed." >&2 + return 1 + } + + github_api() { + run_api "target-app-token" "$TARGET_APP_WAKE_TOKEN" "$@" || + run_api "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" "$@" || + run_api "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" "$@" || + run_api "github-token" "$GITHUB_WAKE_TOKEN" "$@" + } + + if ! pull="$(github_api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"; then + echo "::error::CodeQL settlement could not read the current pull request." 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 - echo "::error::CodeQL wake identity is non-canonical." + if [ "$(printf '%s' "$pull" | jq -r '.state // empty')" != "open" ] || + [ "$(printf '%s' "$pull" | jq -r '.base.repo.full_name // empty')" != "$TARGET_REPOSITORY" ] || + [ "$(printf '%s' "$pull" | jq -r '.base.ref // empty')" != "$BASE_REF" ] || + [ "$(printf '%s' "$pull" | jq -r '.base.sha // empty')" != "$BASE_SHA" ] || + [ "$(printf '%s' "$pull" | jq -r '.head.repo.full_name // empty')" != "$TARGET_REPOSITORY" ] || + [ "$(printf '%s' "$pull" | jq -r '.head.ref // empty')" != "$HEAD_REF" ] || + [ "$(printf '%s' "$pull" | jq -r '.head.sha // empty')" != "$HEAD_SHA" ]; then + echo "::error::CodeQL settlement rejected a closed PR, changed base, or stale head." exit 1 fi - pull="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" - live_state="$(printf '%s' "$pull" | jq -r '.state // empty')" - live_head="$(printf '%s' "$pull" | jq -r '.head.sha // empty')" - if [ "$live_state" != "open" ] || [ "$live_head" != "$HEAD_SHA" ]; then - echo "::error::CodeQL wake rejected a closed PR or stale head." + if ! required_run="$(github_api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}")"; then + echo "::error::CodeQL settlement could not read the required run." exit 1 fi - - run="$(gh api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}")" - run_identity="$(printf '%s' "$run" | jq -r --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" ' + if [ "$(printf '%s' "$required_run" | jq -r --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" ' select(.id == $run_id) | 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." + ')" != "$REQUIRED_RUN_ID" ]; then + echo "::error::CodeQL settlement rejected the required run identity." + exit 1 + fi + + if ! required_job_pages="$(github_api --paginate --slurp "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/jobs?per_page=100")"; then + echo "::error::CodeQL settlement could not read the required jobs." + exit 1 + fi + required_job_list="$(printf '%s' "$required_job_pages" | jq -c '[.[] | .jobs[]?]')" + while IFS= read -r required_job; do + language="$(printf '%s' "$required_job" | jq -r '.language // empty')" + job_id="$(printf '%s' "$required_job" | jq -r '.job_id // empty')" + expected_name="CodeQL compatibility analysis (${language})" + match_count="$(printf '%s' "$required_job_list" | jq --arg language "$language" --arg name "$expected_name" --arg head "$HEAD_SHA" --argjson run_id "$REQUIRED_RUN_ID" --argjson job_id "$job_id" --arg mode "$RERUN_MODE" ' + [.[] | select( + .id == $job_id + and .run_id == $run_id + and .head_sha == $head + and .name == $name + and .status == "completed" + and ( + ($mode == "failed" and .conclusion == "failure") + or ($mode == "all" and (.conclusion == "success" or .conclusion == "failure")) + ) + )] | length + ')" + if [ "$match_count" -ne 1 ]; then + echo "::error::CodeQL settlement rejected missing or ambiguous exact job identity for ${language}." + exit 1 + fi + done < <(printf '%s' "$REQUIRED_JOBS" | jq -c '.[]') + + required_job_ids="$(printf '%s' "$REQUIRED_JOBS" | jq -c '[.[].job_id]')" + if [ "$RERUN_MODE" = "failed" ] && + [ "$(printf '%s' "$required_job_list" | jq --argjson required_ids "$required_job_ids" ' + [.[] | .id as $id | select(.status == "completed" and .conclusion == "failure" and ($required_ids | index($id) | not))] | length + ')" -ne 0 ]; then + echo "::error::CodeQL settlement rejected unrelated failed jobs outside the exact language map." 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}." + if ! handler_job_pages="$(GH_TOKEN="$HANDLER_READ_TOKEN" gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100")" || + ! handler_artifact_pages="$(GH_TOKEN="$HANDLER_READ_TOKEN" gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100")"; then + echo "::error::CodeQL settlement could not read exact handler evidence." + exit 1 + fi + handler_jobs="$(printf '%s' "$handler_job_pages" | jq -c '[.[] | .jobs[]?]')" + handler_artifacts="$(printf '%s' "$handler_artifact_pages" | jq -c '[.[] | .artifacts[]?]')" + while IFS= read -r required_job; do + language="$(printf '%s' "$required_job" | jq -r '.language')" + expected_job_name="CodeQL dispatch scan (${language})" + expected_artifact_name="codeql-dispatch-${language}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + handler_job_count="$(printf '%s' "$handler_jobs" | jq --arg name "$expected_job_name" --argjson attempt "$GITHUB_RUN_ATTEMPT" ' + [.[] | select( + .name == $name + and .status == "completed" + and (.conclusion == "success" or .conclusion == "failure") + and .run_attempt == $attempt + and ([.steps[]? | select(.name == "Enforce CodeQL Medium+ SARIF gate" and (.conclusion == "success" or .conclusion == "failure"))] | length) == 1 + and ([.steps[]? | select(.name == "Preserve CodeQL SARIF evidence" and .conclusion == "success")] | length) == 1 + )] | length + ')" + handler_artifact_count="$(printf '%s' "$handler_artifacts" | jq --arg name "$expected_artifact_name" ' + [.[] | select(.name == $name and (.expired == false) and (.size_in_bytes > 0))] | length + ')" + if [ "$handler_job_count" -ne 1 ] || [ "$handler_artifact_count" -ne 1 ]; then + echo "::error::CodeQL settlement rejected incomplete handler gate or SARIF evidence for ${language}." + exit 1 + fi + done < <(printf '%s' "$REQUIRED_JOBS" | jq -c '.[]') + + case "$RERUN_MODE" in + failed) rerun_endpoint="rerun-failed-jobs" ;; + all) rerun_endpoint="rerun" ;; + *) + echo "::error::CodeQL settlement rejected an unsupported rerun mode." + exit 1 + ;; + esac + + post_wake() { + token_label="$1" + token="$2" + if [ -z "$token" ]; then + return 1 + fi + if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/${rerun_endpoint}" >/dev/null; then + echo "Re-ran exact CodeQL required run ${REQUIRED_RUN_ID} mode=${RERUN_MODE} head=${HEAD_SHA} using ${token_label}." + return 0 + fi + echo "::notice::CodeQL settlement POST using ${token_label} did not succeed." + return 1 + } + + if post_wake "target-app-token" "$TARGET_APP_WAKE_TOKEN" || + post_wake "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" || + post_wake "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" || + post_wake "github-token" "$GITHUB_WAKE_TOKEN"; then + exit 0 + fi + + echo "::error::CodeQL settlement could not enqueue verified run-wide recovery." + exit 1 From b2ff9a97d2658ef52b60c41edb46e427bf7f9131 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:28:51 +0900 Subject: [PATCH 08/28] test(codeql): enforce versioned settlement contract --- ..._codeql_scan_dispatch_workflow_contract.py | 993 ++++++++++++++++-- 1 file changed, 910 insertions(+), 83 deletions(-) diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index dd30c8506d..d13191f5b8 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -17,6 +17,8 @@ import sys from pathlib import Path +import pytest + from scripts.ci import audit_central_required_workflows as ruleset_audit from tests.test_opencode_workflow_shell_syntax import _extract_run_block from tests.test_required_workflow_queue_contract import ( @@ -36,7 +38,8 @@ "Fetch the pinned CodeQL SARIF gate script", "Materialize pull request head for CodeQL scan", "Publish CodeQL dispatch status", - "Wake exact CodeQL required job", + "Exchange OpenCode app token for run settlement", + "Settle exact CodeQL required run", ) @@ -67,7 +70,7 @@ def test_codeql_scan_dispatch_workflow_structure(): workflow = WORKFLOW_PATH.read_text(encoding="utf-8") assert "name: CodeQL Scan Dispatch" in workflow - assert "types: [codeql-scan]" in workflow + assert "types: [codeql-scan, codeql-scan-v2]" in workflow # No workflow_dispatch: test_no_central_workflow_exposes_branch_selected_manual_dispatch # (tests/test_required_workflow_queue_contract.py) forbids it on every # central workflow because it lets a caller pick an arbitrary ref to run @@ -78,7 +81,11 @@ def test_codeql_scan_dispatch_workflow_structure(): assert workflow.count("github/codeql-action/init@") == 1 assert workflow.count("github/codeql-action/analyze@") == 1 assert "scripts/ci/codeql_sarif_gate.py" in workflow - assert 'context="codeql-dispatch/${LANGUAGE}"' in workflow + assert 'receipt_context="codeql-dispatch/${LANGUAGE}"' in workflow + assert 'receipt_context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}"' in workflow + assert '-f context="$receipt_context"' in workflow + assert "github.event.client_payload.producer_source_sha" in workflow + assert 'receipt_description="cwl1;h=${HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${PRODUCER_SOURCE_SHA}"' in workflow assert "OPENCODE_REPOSITORY_DISPATCH_ACTOR" in workflow # Deliberately NOT vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS: that allowlist # scopes a gradual ~12-repo OpenCode review rollout, while ruleset @@ -137,7 +144,12 @@ def _run_validate_step(tmp_path: Path, env_overrides: dict[str, str], pull_reque "#!/usr/bin/env bash\n" "set -euo pipefail\n" 'test "$1" = api\n' - 'printf \'%s\\n\' "$FAKE_PULL_JSON"\n', + 'endpoint="${!#}"\n' + 'case "$endpoint" in\n' + ' repos/ContextualWisdomLab/.github/compare/*) printf \'%s\\n\' "$FAKE_SOURCE_COMPARE_JSON" ;;\n' + ' repos/ContextualWisdomLab/*/git/commits/*) printf \'%s\\n\' "$FAKE_PRODUCER_COMMIT_JSON" ;;\n' + ' *) printf \'%s\\n\' "$FAKE_PULL_JSON" ;;\n' + 'esac\n', encoding="utf-8", ) fake_gh.chmod(0o755) @@ -147,19 +159,36 @@ def _run_validate_step(tmp_path: Path, env_overrides: dict[str, str], pull_reque **os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps(pull_request), + "FAKE_SOURCE_COMPARE_JSON": "{}", + "FAKE_PRODUCER_COMMIT_JSON": json.dumps( + { + "sha": "c" * 40, + "parents": [{"sha": "a" * 40}, {"sha": "b" * 40}], + } + ), "GITHUB_OUTPUT": str(output), "DISPATCH_ACTOR": "seonghobae", "DISPATCH_SENDER": "seonghobae", "ALLOWED_DISPATCH_ACTOR": "seonghobae", + "DISPATCH_PROTOCOL": "codeql-scan-v2", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", "PR_NUMBER": "42", "SUPPLIED_BASE_REF": "main", "SUPPLIED_BASE_SHA": "a" * 40, + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": "1", "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "1", "SUPPLIED_HEAD_REF": "feature", "SUPPLIED_HEAD_SHA": "b" * 40, + "SUPPLIED_LEGACY_HEAD_REF": "", + "SUPPLIED_LEGACY_HEAD_SHA": "", + "SUPPLIED_PRODUCER_SOURCE_SHA": "c" * 40, "SUPPLIED_MATRIX": json.dumps([{"language": "python", "build-mode": "none"}]), "SUPPLIED_REQUIRED_RUN_ID": "42", "SUPPLIED_REQUIRED_JOBS": json.dumps([{"language": "python", "job_id": 43}]), + "SUPPLIED_RERUN_MODE": "", + "SUPPLIED_RERUN_REQUEST": "null", "SUPPLIED_REQUIRED_JOB_ID": "", "SUPPLIED_REQUIRED_LANGUAGE": "", **env_overrides, @@ -173,11 +202,28 @@ def _matching_pull_request() -> dict: """A live PR payload that matches the default supplied metadata in _run_validate_step.""" return { "state": "open", + "merge_commit_sha": "c" * 40, "base": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "main", "sha": "a" * 40}, "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "feature", "sha": "b" * 40}, } +def _legacy_dispatch_env() -> dict[str, str]: + """Exact environment produced by the protected pre-v2 CodeQL client.""" + return { + "DISPATCH_PROTOCOL": "codeql-scan", + "SUPPLIED_HEAD_ENVELOPE": "null", + "SUPPLIED_HEAD_SCHEMA": "", + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + "SUPPLIED_LEGACY_HEAD_REF": "feature", + "SUPPLIED_LEGACY_HEAD_SHA": "b" * 40, + "SUPPLIED_PRODUCER_SOURCE_SHA": "", + "SUPPLIED_RERUN_MODE": "", + "SUPPLIED_RERUN_REQUEST": "null", + } + + def test_codeql_scan_dispatch_validate_step_accepts_matching_live_metadata(tmp_path): """A dispatch whose metadata matches the live PR produces the expected GITHUB_OUTPUT.""" result = _run_validate_step(tmp_path, {}, _matching_pull_request()) @@ -189,11 +235,362 @@ def test_codeql_scan_dispatch_validate_step_accepts_matching_live_metadata(tmp_p assert "head_sha=" + "b" * 40 in output_text assert '[{"language":"python","build-mode":"none"}]' in output_text assert "required_run_id=42" in output_text + assert "dispatch_protocol=v2" in output_text + assert "producer_source_sha=" + "c" * 40 in output_text assert '"job_id":43' in output_text.replace(" ", "") assert "required_job_id=" not in output_text assert "required_language=" not in output_text +def test_codeql_scan_dispatch_accepts_exact_protected_legacy_payload(tmp_path): + """The handler-first bootstrap keeps the current protected producer live.""" + result = _run_validate_step( + tmp_path, + _legacy_dispatch_env(), + _matching_pull_request(), + ) + + assert result.returncode == 0, result.stdout + result.stderr + output_text = result.output_path.read_text(encoding="utf-8") + assert "dispatch_protocol=legacy-v1" in output_text + assert "producer_source_sha=\n" in output_text + + +@pytest.mark.parametrize( + ("field_name", "field_value"), + [ + ("SUPPLIED_PRODUCER_SOURCE_SHA", "c" * 40), + ( + "SUPPLIED_HEAD_ENVELOPE", + json.dumps({"schema": "1", "ref": "feature", "sha": "b" * 40}), + ), + ( + "SUPPLIED_RERUN_REQUEST", + json.dumps( + { + "mode": "failed", + "required_jobs": [{"language": "python", "job_id": 43}], + } + ), + ), + ], +) +def test_codeql_scan_dispatch_legacy_protocol_rejects_v2_only_fields( + tmp_path, field_name, field_value +) -> None: + """A v2 payload cannot downgrade by selecting the legacy event type.""" + legacy_env = _legacy_dispatch_env() + legacy_env[field_name] = field_value + result = _run_validate_step(tmp_path, legacy_env, _matching_pull_request()) + + assert result.returncode == 1 + assert "Legacy CodeQL dispatch rejected v2-only identity fields" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_unknown_head_schema(tmp_path): + """Unknown nested-head schema versions fail before metadata can be trusted.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": "2", "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "2", + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "unsupported pr_head schema=2" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_accepts_versioned_head_envelope(tmp_path): + """Schema-one nested head metadata reaches the live validation success path.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": "1", "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "1", + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + }, + _matching_pull_request(), + ) + + assert result.returncode == 0 + assert ( + "Validated current live metadata for ContextualWisdomLab/naruon#42: base=main/" + in result.stdout + ) + assert "head=feature/" in result.stdout + + +@pytest.mark.parametrize( + ("legacy_ref", "legacy_sha"), + [ + ("feature-wrong", "b" * 40), + ("feature", "c" * 40), + ("feature", ""), + ("", "b" * 40), + ], +) +def test_codeql_scan_dispatch_validate_step_rejects_conflicting_dual_head_identity( + tmp_path, legacy_ref, legacy_sha +): + """Nested identity cannot shadow an unequal or partial legacy representation.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": "1", "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "1", + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + "SUPPLIED_LEGACY_HEAD_REF": legacy_ref, + "SUPPLIED_LEGACY_HEAD_SHA": legacy_sha, + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "conflicting nested and legacy pr_head identity" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_numeric_head_schema(tmp_path): + """The JSON envelope schema stays a version string, not a numeric alias.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps( + {"schema": 1, "ref": "feature", "sha": "b" * 40} + ), + "SUPPLIED_HEAD_SCHEMA": "1", + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "invalid pr_head envelope" in result.stdout + + +@pytest.mark.parametrize("missing_field", ["ref", "sha"]) +def test_codeql_scan_dispatch_validate_step_rejects_incomplete_head_envelope( + tmp_path, missing_field +): + """A present envelope cannot borrow a required value from legacy fields.""" + envelope = {"schema": "1", "ref": "feature", "sha": "b" * 40} + del envelope[missing_field] + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps(envelope), + "SUPPLIED_HEAD_SCHEMA": "1", + "SUPPLIED_LEGACY_HEAD_REF": "feature", + "SUPPLIED_LEGACY_HEAD_SHA": "b" * 40, + "SUPPLIED_HEAD_REF": "feature", + "SUPPLIED_HEAD_SHA": "b" * 40, + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "invalid pr_head envelope" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_unversioned_head_envelope(tmp_path): + """A nested head tuple without its schema version fails closed.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_HEAD_ENVELOPE": json.dumps({"ref": "feature", "sha": "b" * 40}), + "SUPPLIED_HEAD_SCHEMA": "", + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "unsupported pr_head schema=" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_accepts_nested_rerun_request(tmp_path): + """The bounded ten-key producer envelope normalizes mode and job identities.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_REQUIRED_JOBS": "null", + "SUPPLIED_RERUN_REQUEST": json.dumps( + { + "mode": "failed", + "required_jobs": [{"language": "python", "job_id": 43}], + } + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 0, result.stderr + output_text = result.output_path.read_text(encoding="utf-8") + assert "rerun_mode=failed" in output_text + assert '"job_id":43' in output_text.replace(" ", "") + + +def test_codeql_scan_dispatch_validate_step_binds_producer_revision(tmp_path): + """Only the exact live base/head merge revision can invoke the handler.""" + missing = _run_validate_step( + tmp_path / "missing", + {"SUPPLIED_PRODUCER_SOURCE_SHA": ""}, + _matching_pull_request(), + ) + wrong_revision = _run_validate_step( + tmp_path / "wrong-revision", + { + "SUPPLIED_PRODUCER_SOURCE_SHA": "d" * 40, + "FAKE_PRODUCER_COMMIT_JSON": json.dumps( + { + "sha": "d" * 40, + "parents": [{"sha": "a" * 40}, {"sha": "b" * 40}], + } + ), + }, + _matching_pull_request(), + ) + wrong_parents = _run_validate_step( + tmp_path / "wrong-parents", + { + "FAKE_PRODUCER_COMMIT_JSON": json.dumps( + { + "sha": "c" * 40, + "parents": [{"sha": "f" * 40}, {"sha": "b" * 40}], + } + ), + }, + _matching_pull_request(), + ) + + assert missing.returncode == 1 + assert wrong_revision.returncode == 1 + assert wrong_parents.returncode == 1 + assert "producer source" in missing.stdout.lower() + assert "producer revision" in wrong_revision.stdout.lower() + assert "producer revision" in wrong_parents.stdout.lower() + + +def test_codeql_scan_dispatch_accepts_exact_pull_request_merge_revision(tmp_path): + """Bind the producer revision to the live PR base/head merge, not handler ancestry.""" + merge_sha = "e" * 40 + pull_request = _matching_pull_request() + pull_request["merge_commit_sha"] = merge_sha + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_PRODUCER_SOURCE_SHA": merge_sha, + "FAKE_SOURCE_COMPARE_JSON": json.dumps( + { + "status": "diverged", + "behind_by": 1, + "base_commit": {"sha": "f" * 40}, + "merge_base_commit": {"sha": "f" * 40}, + } + ), + "FAKE_PRODUCER_COMMIT_JSON": json.dumps( + { + "sha": merge_sha, + "parents": [ + {"sha": "a" * 40}, + {"sha": "b" * 40}, + ], + } + ), + }, + pull_request, + ) + + assert result.returncode == 0, result.stdout + result.stderr + + +def test_codeql_scan_dispatch_validate_step_accepts_legacy_rerun_mode(tmp_path): + """An already queued top-level mode retains whole-attempt semantics.""" + result = _run_validate_step( + tmp_path, + {"SUPPLIED_RERUN_MODE": "all"}, + _matching_pull_request(), + ) + + assert result.returncode == 0, result.stderr + assert "rerun_mode=all" in result.output_path.read_text(encoding="utf-8") + + +def test_codeql_scan_dispatch_validate_step_rejects_conflicting_rerun_envelopes( + tmp_path, +): + """A caller cannot supply both legacy and nested rerun authority.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_RERUN_REQUEST": json.dumps( + { + "mode": "failed", + "required_jobs": [{"language": "python", "job_id": 43}], + } + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "conflicting legacy and nested rerun envelopes" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_unknown_rerun_mode(tmp_path): + """Only the two run-wide GitHub rerun operations are accepted.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_REQUIRED_JOBS": "null", + "SUPPLIED_RERUN_REQUEST": json.dumps( + { + "mode": "one-job", + "required_jobs": [{"language": "python", "job_id": 43}], + } + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "rerun mode" in result.stdout + + +def test_codeql_scan_dispatch_validate_step_rejects_duplicate_job_id(tmp_path): + """Two language labels cannot authorize mutation of the same required job.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_MATRIX": json.dumps( + [ + {"language": "python", "build-mode": "none"}, + {"language": "actions", "build-mode": "none"}, + ] + ), + "SUPPLIED_REQUIRED_JOBS": json.dumps( + [ + {"language": "python", "job_id": 43}, + {"language": "actions", "job_id": 43}, + ] + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "wake identity is missing" in result.stdout + + def test_codeql_scan_dispatch_validate_step_rejects_actor_mismatch(tmp_path): """A dispatch from an unauthorized actor is rejected before any live PR read.""" result = _run_validate_step(tmp_path, {"DISPATCH_ACTOR": "someone-else"}, _matching_pull_request()) @@ -357,6 +754,28 @@ def test_codeql_scan_dispatch_validate_step_accepts_multi_language_payload(tmp_p assert '"job_id":43' in output_text.replace(" ", "") +def test_codeql_scan_dispatch_validate_step_rejects_unproven_matrix_subset(tmp_path): + """A partial scan cannot authorize waking an unscanned required language.""" + result = _run_validate_step( + tmp_path, + { + "SUPPLIED_MATRIX": json.dumps( + [{"language": "actions", "build-mode": "none"}] + ), + "SUPPLIED_REQUIRED_JOBS": json.dumps( + [ + {"language": "python", "job_id": 43}, + {"language": "actions", "job_id": 44}, + ] + ), + }, + _matching_pull_request(), + ) + + assert result.returncode == 1 + assert "does not match the dispatched languages one-to-one" in result.stdout + + def test_codeql_scan_dispatch_validate_step_accepts_legacy_single_language_payload(tmp_path): """A queued pre-cutover payload still validates after required_jobs became mandatory. @@ -369,6 +788,7 @@ def test_codeql_scan_dispatch_validate_step_accepts_legacy_single_language_paylo result = _run_validate_step( tmp_path / case_name, { + **_legacy_dispatch_env(), "SUPPLIED_REQUIRED_JOBS": empty_jobs, "SUPPLIED_REQUIRED_LANGUAGE": "python", "SUPPLIED_REQUIRED_JOB_ID": "43", @@ -392,6 +812,7 @@ def test_codeql_scan_dispatch_validate_step_ignores_legacy_fields_when_required_ result = _run_validate_step( tmp_path, { + **_legacy_dispatch_env(), "SUPPLIED_MATRIX": json.dumps( [ {"language": "python", "build-mode": "none"}, @@ -422,12 +843,13 @@ def test_codeql_scan_dispatch_validate_step_rejects_unusable_legacy_payload(tmp_ """Empty required_jobs still fail closed when the scalar identity cannot be synthesized.""" missing_both = _run_validate_step( tmp_path / "missing-both", - {"SUPPLIED_REQUIRED_JOBS": "null"}, + {**_legacy_dispatch_env(), "SUPPLIED_REQUIRED_JOBS": "null"}, _matching_pull_request(), ) language_mismatch = _run_validate_step( tmp_path / "language-mismatch", { + **_legacy_dispatch_env(), "SUPPLIED_REQUIRED_JOBS": "[]", "SUPPLIED_REQUIRED_LANGUAGE": "javascript-typescript", "SUPPLIED_REQUIRED_JOB_ID": "43", @@ -437,6 +859,7 @@ def test_codeql_scan_dispatch_validate_step_rejects_unusable_legacy_payload(tmp_ multi_language_legacy = _run_validate_step( tmp_path / "multi-language-legacy", { + **_legacy_dispatch_env(), "SUPPLIED_MATRIX": json.dumps( [ {"language": "python", "build-mode": "none"}, @@ -452,6 +875,7 @@ def test_codeql_scan_dispatch_validate_step_rejects_unusable_legacy_payload(tmp_ invalid_job_id = _run_validate_step( tmp_path / "invalid-job-id", { + **_legacy_dispatch_env(), "SUPPLIED_REQUIRED_JOBS": "null", "SUPPLIED_REQUIRED_LANGUAGE": "python", "SUPPLIED_REQUIRED_JOB_ID": "0", @@ -505,8 +929,8 @@ 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. +def test_codeql_scan_dispatch_run_name_versions_source_without_changing_concurrency() -> None: + """v2 adds source identity while both protocols retain one PR writer. The required shard cannot read client_payload. Encoding those fields in run-name lets it reject a same-head retarget or a different waiting @@ -520,10 +944,13 @@ def test_codeql_scan_dispatch_run_name_binds_base_and_required_run() -> None: 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.producer_source_sha" in header + assert "github.event.action == 'codeql-scan-v2'" 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 + assert "github.event.action" not in group_value def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> None: @@ -534,65 +961,106 @@ def test_dispatch_publish_keeps_successful_scan_when_status_write_is_denied() -> """ 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 + "\n\n settle-required-run:\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 "exact completed scan and preserved SARIF artifact remain" 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: +def test_dispatch_publish_rejects_superseded_metadata_and_versions_context() -> None: + """A stale handler cannot poison HEAD and v2 cannot reuse a legacy status. + + Run 34235814716 proved that a scan can become superseded after initial + validation but before publication. #1902's evidence-complete producer is + integrated into the same successor, so publication requires successful + live-metadata revalidation and emits only the base-bound receipt. + """ workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - wake = workflow.split(" - name: Wake exact CodeQL required job\n", 1)[1].split( - "\n\n - name:", 1 + revalidate = workflow.split( + " - name: Re-validate live pull request metadata before privileged scan\n", + 1, + )[1].split(" - name: Fetch the pinned CodeQL SARIF gate script\n", 1)[0] + publish = workflow.split(" - name: Publish CodeQL dispatch status\n", 1)[1].split( + "\n\n settle-required-run:\n", 1 )[0] - assert "steps.publish_status.outcome == '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 '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 "while " not in wake - assert "sleep " not in wake - - -def test_dispatch_wake_has_only_trusted_actions_write_boundary() -> None: + assert " id: live_metadata\n" in revalidate + assert "if: always() && steps.live_metadata.outcome == 'success'" in publish + assert 'receipt_context="codeql-dispatch/${LANGUAGE}"' in publish + assert 'receipt_context="codeql-dispatch/${LANGUAGE}/${BASE_SHA}"' in publish + assert '-f context="$receipt_context"' in publish + assert "SARIF_UPLOAD_OUTCOME: ${{ steps.sarif_upload.outcome }}" in publish + assert 'if [ "${SARIF_UPLOAD_OUTCOME:-}" != "success" ]; then' in publish + assert 'actual_creator="$(jq -r' in publish + assert "unexpected creator" in publish + + +def test_dispatch_settles_all_languages_with_one_run_wide_mutation() -> None: + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + settlement = workflow.split(" settle-required-run:\n", 1)[1] + + assert "needs: [validate-dispatch, scan]" in settlement + assert "always()" in settlement.split(" runs-on:", 1)[0] + assert "actions: write" in settlement.split(" steps:\n", 1)[0] + assert 'github_api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"' in settlement + assert 'github_api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}"' in settlement + assert 'github_api --paginate --slurp "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}/jobs?per_page=100"' in settlement + assert "rerun-failed-jobs" in settlement + assert '"rerun"' in settlement + assert "actions/jobs/${REQUIRED_JOB_ID}/rerun" not in workflow + assert "sleep " not in settlement + + +def test_dispatch_settlement_has_only_trusted_actions_write_boundary() -> None: workflow = WORKFLOW_PATH.read_text(encoding="utf-8") scan = workflow.split(" scan:\n", 1)[1] scan_permissions = scan.split(" strategy:\n", 1)[0] + settlement = workflow.split(" settle-required-run:\n", 1)[1] + settlement_permissions = settlement.split(" steps:\n", 1)[0] - assert "actions: write" in scan_permissions + assert "actions: write" not in scan_permissions + assert "actions: read" in scan_permissions + assert "actions: write" in settlement_permissions assert "pull_request:" not in workflow assert "pull_request_target:" not in workflow - assert "needs.validate-dispatch.outputs.required_run_id != ''" in scan - assert "needs.validate-dispatch.outputs.required_jobs != ''" in scan + assert "needs.validate-dispatch.outputs.required_run_id" in settlement + assert "needs.validate-dispatch.outputs.required_jobs" in settlement assert "github.event.client_payload.required_job_id" not in scan -def _run_wake_step( +def _run_settlement_step( tmp_path: Path, *, pull: dict | None = None, run: dict | None = None, - job: dict | None = None, + required_jobs: list[dict] | None = None, + handler_jobs: list[dict] | None = None, + handler_artifacts: list[dict] | None = None, + extra_env: dict[str, str] | None = None, ) -> tuple[subprocess.CompletedProcess[str], Path]: - """Execute the exact wake block against fixture-backed GitHub API responses.""" + """Execute the run-wide settlement block against fixture-backed API responses.""" bash = shutil.which("bash") jq = shutil.which("jq") assert bash is not None and jq is not None, "bash and jq are required to run this test" head_sha = "b" * 40 - pull = pull or {"state": "open", "head": {"sha": head_sha}} + pull = pull or { + "state": "open", + "base": { + "repo": {"full_name": "ContextualWisdomLab/naruon"}, + "ref": "main", + "sha": "a" * 40, + }, + "head": { + "repo": {"full_name": "ContextualWisdomLab/naruon"}, + "ref": "feature", + "sha": head_sha, + }, + } run = run or { "id": 42, "event": "pull_request", @@ -601,16 +1069,60 @@ def _run_wake_step( "status": "completed", "conclusion": "failure", } - job = job or { - "id": 43, - "run_id": 42, - "head_sha": head_sha, - "name": "CodeQL compatibility analysis (python)", - "status": "completed", - "conclusion": "failure", - } + required_jobs = required_jobs or [ + { + "id": 43, + "run_id": 42, + "head_sha": head_sha, + "name": "CodeQL compatibility analysis (python)", + "status": "completed", + "conclusion": "failure", + }, + { + "id": 44, + "run_id": 42, + "head_sha": head_sha, + "name": "CodeQL compatibility analysis (actions)", + "status": "completed", + "conclusion": "failure", + }, + ] + handler_jobs = handler_jobs or [ + { + "name": "CodeQL dispatch scan (python)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [ + {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, + ], + }, + { + "name": "CodeQL dispatch scan (actions)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [ + {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, + ], + }, + ] + handler_artifacts = handler_artifacts or [ + { + "name": "codeql-dispatch-python-100-1", + "expired": False, + "size_in_bytes": 10, + }, + { + "name": "codeql-dispatch-actions-100-1", + "expired": False, + "size_in_bytes": 10, + }, + ] script = _extract_run_block( - WORKFLOW_PATH.read_text(encoding="utf-8"), "Wake exact CodeQL required job" + WORKFLOW_PATH.read_text(encoding="utf-8"), "Settle exact CodeQL required run" ) fake_bin = tmp_path / "bin" fake_bin.mkdir(parents=True) @@ -620,15 +1132,30 @@ def _run_wake_step( "#!/usr/bin/env bash\n" "set -euo pipefail\n" 'test "$1" = api\n' - 'if [ "${2:-}" = "-X" ]; then\n' - ' test "$3" = POST\n' - ' printf \'%s\\n\' "$4" >>"$FAKE_POST_LOG"\n' + 'endpoint="${!#}"\n' + 'if printf \'%s\\n\' "$@" | grep -qx POST; then\n' + ' printf \'%s\\n\' "$endpoint" >>"$FAKE_POST_LOG"\n' + ' if [ -n "${FAKE_WAKE_POST_FAIL_TOKEN:-}" ] && ' + '[ "${GH_TOKEN:-}" = "$FAKE_WAKE_POST_FAIL_TOKEN" ]; then\n' + " exit 1\n" + " fi\n" + ' if [ -n "${FAKE_DENIED_TOKEN:-}" ] && ' + '[ "${GH_TOKEN:-}" = "$FAKE_DENIED_TOKEN" ]; then\n' + " exit 1\n" + " fi\n" + ' if [ "${FAKE_WAKE_POST_FAIL_ALL:-}" = "1" ]; then\n' + " exit 1\n" + " fi\n" + ' test "${FAKE_POST_EXIT:-0}" = 0 || exit "$FAKE_POST_EXIT"\n' " exit 0\n" "fi\n" - 'case "$2" in\n' + 'test "${GH_TOKEN:-}" != "${FAKE_DENIED_TOKEN:-}" || exit 1\n' + 'case "$endpoint" 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' + ' repos/ContextualWisdomLab/naruon/actions/runs/42/jobs*) printf \'%s\\n\' "$FAKE_REQUIRED_JOB_PAGES" ;;\n' + ' repos/ContextualWisdomLab/naruon/actions/runs/42) printf \'%s\\n\' "$FAKE_RUN_JSON" ;;\n' + ' repos/ContextualWisdomLab/.github/actions/runs/100/jobs*) printf \'%s\\n\' "$FAKE_HANDLER_JOB_PAGES" ;;\n' + ' repos/ContextualWisdomLab/.github/actions/runs/100/artifacts*) printf \'%s\\n\' "$FAKE_HANDLER_ARTIFACT_PAGES" ;;\n' " *) exit 1 ;;\n" "esac\n", encoding="utf-8", @@ -639,12 +1166,28 @@ def _run_wake_step( "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps(pull), "FAKE_RUN_JSON": json.dumps(run), - "FAKE_JOB_JSON": json.dumps(job), + "FAKE_REQUIRED_JOB_PAGES": json.dumps([{"jobs": required_jobs}]), + "FAKE_HANDLER_JOB_PAGES": json.dumps([{"jobs": handler_jobs}]), + "FAKE_HANDLER_ARTIFACT_PAGES": json.dumps( + [{"artifacts": handler_artifacts}] + ), "FAKE_POST_LOG": str(post_log), + "FAKE_POST_EXIT": "0", + "FAKE_DENIED_TOKEN": "", "GH_TOKEN": "fake-token", - "WAKE_TOKEN_SOURCE": "PR_REVIEW_MERGE_TOKEN", + "TARGET_APP_WAKE_TOKEN": "", + "PR_REVIEW_MERGE_WAKE_TOKEN": "", + "OPENCODE_APPROVE_WAKE_TOKEN": "", + "GITHUB_WAKE_TOKEN": "fake-token", + "HANDLER_READ_TOKEN": "handler-token", + "GITHUB_REPOSITORY": "ContextualWisdomLab/.github", + "GITHUB_RUN_ID": "100", + "GITHUB_RUN_ATTEMPT": "1", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", "PR_NUMBER": "42", + "BASE_REF": "main", + "BASE_SHA": "a" * 40, + "HEAD_REF": "feature", "HEAD_SHA": head_sha, "REQUIRED_RUN_ID": "42", "REQUIRED_JOBS": json.dumps( @@ -653,28 +1196,126 @@ def _run_wake_step( {"language": "actions", "job_id": 44}, ] ), - "REQUIRED_LANGUAGE": "python", + "RERUN_MODE": "failed", } + if extra_env: + env.update(extra_env) result = subprocess.run( [bash], input=script, text=True, capture_output=True, check=False, env=env ) return result, post_log -def test_dispatch_wake_reruns_only_fixture_bound_exact_job(tmp_path: Path) -> None: - result, post_log = _run_wake_step(tmp_path) +def test_dispatch_settlement_reruns_two_languages_once(tmp_path: Path) -> None: + result, post_log = _run_settlement_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" ] -def test_dispatch_wake_rejects_stale_head_and_closed_pr(tmp_path: Path) -> None: - stale_result, stale_log = _run_wake_step( +def test_dispatch_settlement_fails_closed_when_no_credential( + tmp_path: Path, +) -> None: + result, post_log = _run_settlement_step( + tmp_path, + extra_env={ + "GH_TOKEN": "", + "TARGET_APP_WAKE_TOKEN": "", + "PR_REVIEW_MERGE_WAKE_TOKEN": "", + "OPENCODE_APPROVE_WAKE_TOKEN": "", + "GITHUB_WAKE_TOKEN": "", + }, + ) + + assert result.returncode == 1 + assert "could not read the current pull request" in result.stdout + assert not post_log.exists() + + +def test_dispatch_settlement_falls_back_when_target_app_token_cannot_rerun( + tmp_path: Path, +) -> None: + """A nonempty App token without Actions write must not shadow fallbacks.""" + result, post_log = _run_settlement_step( + tmp_path, + extra_env={ + "TARGET_APP_WAKE_TOKEN": "forbidden-app-token", + "PR_REVIEW_MERGE_WAKE_TOKEN": "actions-write-token", + "OPENCODE_APPROVE_WAKE_TOKEN": "", + "GITHUB_WAKE_TOKEN": "", + "GH_TOKEN": "", + "FAKE_WAKE_POST_FAIL_TOKEN": "forbidden-app-token", + }, + ) + + assert result.returncode == 0, result.stderr + assert ( + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs" + in post_log.read_text(encoding="utf-8") + ) + assert "pr-review-merge-token" in result.stdout + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + ] + + +def test_dispatch_settlement_fails_closed_after_every_wake_is_denied( + tmp_path: Path, +) -> None: + """A clean scan is not authoritative until one exact-job wake is accepted.""" + result, post_log = _run_settlement_step( + tmp_path, + extra_env={ + "TARGET_APP_WAKE_TOKEN": "app-token", + "PR_REVIEW_MERGE_WAKE_TOKEN": "merge-token", + "OPENCODE_APPROVE_WAKE_TOKEN": "approve-token", + "GITHUB_WAKE_TOKEN": "github-token", + "GH_TOKEN": "", + "FAKE_WAKE_POST_FAIL_ALL": "1", + }, + ) + + assert result.returncode == 1 + assert "could not enqueue verified run-wide recovery" in result.stdout + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + ] + + +def test_dispatch_settlement_retries_reads_with_next_configured_credential( + tmp_path: Path, +) -> None: + result, post_log = _run_settlement_step( + tmp_path, + extra_env={ + "GH_TOKEN": "target-token", + "TARGET_APP_WAKE_TOKEN": "target-token", + "PR_REVIEW_MERGE_WAKE_TOKEN": "fallback-token", + "OPENCODE_APPROVE_WAKE_TOKEN": "", + "GITHUB_WAKE_TOKEN": "", + "FAKE_DENIED_TOKEN": "target-token", + }, + ) + + assert result.returncode == 0, result.stderr + assert "pr-review-merge-token" in result.stdout + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + ] + + +def test_dispatch_settlement_rejects_stale_head_and_closed_pr(tmp_path: Path) -> None: + stale_result, stale_log = _run_settlement_step( tmp_path / "stale", pull={"state": "open", "head": {"sha": "c" * 40}} ) - closed_result, closed_log = _run_wake_step( + closed_result, closed_log = _run_settlement_step( tmp_path / "closed", pull={"state": "closed", "head": {"sha": "b" * 40}} ) @@ -684,10 +1325,52 @@ 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: - wrong_job_result, wrong_job_log = _run_wake_step( - tmp_path / "wrong-job", - job={ +def test_dispatch_settlement_rejects_changed_repository_or_head_ref(tmp_path: Path) -> None: + """Settlement revalidates the complete live PR repository/ref identity.""" + wrong_repository, wrong_repository_log = _run_settlement_step( + tmp_path / "wrong-repository", + pull={ + "state": "open", + "base": {"repo": {"full_name": "ContextualWisdomLab/other"}, "ref": "main", "sha": "a" * 40}, + "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "feature", "sha": "b" * 40}, + }, + ) + changed_ref, changed_ref_log = _run_settlement_step( + tmp_path / "changed-ref", + pull={ + "state": "open", + "base": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "main", "sha": "a" * 40}, + "head": {"repo": {"full_name": "ContextualWisdomLab/naruon"}, "ref": "other", "sha": "b" * 40}, + }, + ) + + assert wrong_repository.returncode == 1 + assert changed_ref.returncode == 1 + assert not wrong_repository_log.exists() + assert not changed_ref_log.exists() + + +def test_dispatch_settlement_rejects_successful_required_run(tmp_path: Path) -> None: + """A completed success cannot be mutated as though it were a failed attempt.""" + result, post_log = _run_settlement_step( + tmp_path, + run={ + "id": 42, + "event": "pull_request", + "path": ".github/workflows/codeql-pr.yml", + "head_sha": "b" * 40, + "status": "completed", + "conclusion": "success", + }, + ) + + assert result.returncode == 1 + assert not post_log.exists() + + +def test_dispatch_settlement_rejects_wrong_or_nonfailed_job_identity(tmp_path: Path) -> None: + wrong_jobs = [ + { "id": 43, "run_id": 999, "head_sha": "b" * 40, @@ -695,42 +1378,171 @@ def test_dispatch_wake_rejects_ambiguous_or_nonfailed_job_identity(tmp_path: Pat "status": "completed", "conclusion": "failure", }, - ) - successful_job_result, successful_job_log = _run_wake_step( - tmp_path / "successful-job", - job={ - "id": 43, + { + "id": 44, "run_id": 42, "head_sha": "b" * 40, - "name": "CodeQL compatibility analysis (python)", + "name": "CodeQL compatibility analysis (actions)", "status": "completed", - "conclusion": "success", + "conclusion": "failure", }, + ] + wrong_job_result, wrong_job_log = _run_settlement_step( + tmp_path / "wrong-job", + required_jobs=wrong_jobs, + ) + successful_jobs = [dict(job) for job in wrong_jobs] + successful_jobs[0].update(run_id=42, conclusion="success") + successful_job_result, successful_job_log = _run_settlement_step( + tmp_path / "successful-job", + required_jobs=successful_jobs, ) 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 "missing or ambiguous exact job identity" 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.""" - result, post_log = _run_wake_step( - tmp_path, - run={ - "id": 42, - "event": "pull_request", - "path": ".github/workflows/codeql-pr.yml", +def test_dispatch_settlement_all_mode_reruns_success_and_failure_jobs(tmp_path: Path) -> None: + all_jobs = [ + { + "id": 43, + "run_id": 42, + "head_sha": "b" * 40, + "name": "CodeQL compatibility analysis (python)", + "status": "completed", + "conclusion": "success", + }, + { + "id": 44, + "run_id": 42, "head_sha": "b" * 40, - "status": "in_progress", - "conclusion": None, + "name": "CodeQL compatibility analysis (actions)", + "status": "completed", + "conclusion": "failure", }, + ] + result, post_log = _run_settlement_step( + tmp_path, + required_jobs=all_jobs, + extra_env={"RERUN_MODE": "all"}, ) assert result.returncode == 0, result.stderr - assert post_log.exists() + assert post_log.read_text(encoding="utf-8").splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun" + ] + + +def test_dispatch_settlement_rejects_missing_handler_artifact(tmp_path: Path) -> None: + result, post_log = _run_settlement_step( + tmp_path, + handler_artifacts=[ + { + "name": "codeql-dispatch-python-100-1", + "expired": False, + "size_in_bytes": 10, + } + ], + ) + + assert result.returncode == 1 + assert "incomplete handler gate or SARIF evidence for actions" in result.stdout + assert not post_log.exists() + + +def test_dispatch_settlement_rejects_missing_handler_gate_steps(tmp_path: Path) -> None: + """A terminal scan name alone is not authenticated gate evidence.""" + result, post_log = _run_settlement_step( + tmp_path, + handler_jobs=[ + { + "name": "CodeQL dispatch scan (python)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [], + }, + { + "name": "CodeQL dispatch scan (actions)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [ + {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, + ], + }, + ], + ) + + assert result.returncode == 1 + assert "incomplete handler gate or SARIF evidence for python" in result.stdout + assert not post_log.exists() + + +def test_dispatch_settlement_rejects_unproven_matrix_subset(tmp_path: Path) -> None: + """Every required shard needs current handler gate and artifact evidence.""" + result, post_log = _run_settlement_step( + tmp_path, + handler_jobs=[ + { + "name": "CodeQL dispatch scan (actions)", + "status": "completed", + "conclusion": "success", + } + ], + handler_artifacts=[ + { + "name": "codeql-dispatch-actions-100-1", + "expired": False, + "size_in_bytes": 10, + } + ], + ) + + assert result.returncode == 1 + assert "incomplete handler gate or SARIF evidence for python" in result.stdout + assert not post_log.exists() + + +def test_dispatch_settlement_rejects_unrelated_failed_job(tmp_path: Path) -> None: + unrelated = { + "id": 45, + "run_id": 42, + "head_sha": "b" * 40, + "name": "unrelated required job", + "status": "completed", + "conclusion": "failure", + } + result, post_log = _run_settlement_step( + tmp_path, + required_jobs=[ + { + "id": 43, + "run_id": 42, + "head_sha": "b" * 40, + "name": "CodeQL compatibility analysis (python)", + "status": "completed", + "conclusion": "failure", + }, + { + "id": 44, + "run_id": 42, + "head_sha": "b" * 40, + "name": "CodeQL compatibility analysis (actions)", + "status": "completed", + "conclusion": "failure", + }, + unrelated, + ], + ) + + assert result.returncode == 1 + assert "unrelated failed jobs" in result.stdout + assert not post_log.exists() def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: @@ -759,6 +1571,10 @@ def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: "SUPPLIED_REQUIRED_JOBS: ${{ toJSON(github.event.client_payload.required_jobs) }}" in workflow ), "SUPPLIED_REQUIRED_JOBS must be serialised with toJSON(); a bare array breaks template validation" + assert ( + "SUPPLIED_RERUN_REQUEST: ${{ toJSON(github.event.client_payload.rerun_request) }}" + in workflow + ), "The bounded nested rerun envelope must be serialized before shell validation" assert ( "SUPPLIED_REQUIRED_JOB_ID: ${{ github.event.client_payload.required_job_id || '' }}" in workflow @@ -767,3 +1583,14 @@ def test_codeql_scan_dispatch_serialises_the_matrix_payload() -> None: "SUPPLIED_REQUIRED_LANGUAGE: ${{ github.event.client_payload.required_language || '' }}" in workflow ), "Queued pre-cutover payloads still supply required_language as a scalar" + assert "SUPPLIED_LEGACY_HEAD_REF: ${{ github.event.client_payload.pr_head_ref || '' }}" in workflow + assert "SUPPLIED_LEGACY_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }}" in workflow + assert "conflicting nested and legacy pr_head identity" in workflow + + +def test_codeql_scan_dispatch_bridge_has_explicit_removal_condition() -> None: + """The legacy compatibility port cannot become permanent hidden policy.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + + assert "LEGACY_V1_REMOVAL_CONDITION" in workflow + assert "protected v2 producer" in workflow From 1d6b6303c48dc7209e0fa00c8d78c3633d554055 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:28:53 +0900 Subject: [PATCH 09/28] test(codeql): enforce versioned settlement contract --- ...est_scheduler_and_codeql_dispatch_runner_image_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py index ba0b2598a9..3070e7ff21 100644 --- a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py +++ b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py @@ -51,10 +51,10 @@ def test_codeql_pr_uses_explicit_supported_image(self) -> None: self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: - """Require both CodeQL Scan Dispatch jobs to pin Ubuntu 24.04.""" + """Require validation, scan, and attempt wake jobs to pin Ubuntu 24.04.""" workflow = CODEQL_SCAN_DISPATCH.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 2) + self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) def test_python_security_uses_explicit_supported_image(self) -> None: """Require all three Python Security jobs to pin Ubuntu 24.04.""" From c695a115661bfa2fac39ac6e0d764e8f68e6ac93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:29:33 +0900 Subject: [PATCH 10/28] docs(codeql): define handler-first migration --- ...required-workflow-dispatch-architecture.md | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md index 5a11894767..b9a156417b 100644 --- a/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md +++ b/docs/adr/0025-codeql-required-workflow-dispatch-architecture.md @@ -1,6 +1,6 @@ # 0025 — Restore central CodeQL as a required workflow via repository_dispatch -**Status:** Proposed, amended 2026-09-07 (one dispatch per pull request; language independence is the handler job matrix) · **Date:** 2026-09-03 · **Owner intent recorded:** loop-brief item 41 +**Status:** Proposed, amended 2026-09-12 (versioned handler-first bootstrap) · **Date:** 2026-09-03 · **Owner intent recorded:** loop-brief item 41 ## Problem @@ -306,3 +306,55 @@ blocker for this one. required `workflows` list (admin:org PUT, same mechanism used to remove it) and verify a real PR observes a successful, correctly-named required check before declaring this ADR's status Accepted. + +## 2026-09-12 amendment: versioned handler-first bootstrap + +The initial rollout created a protected-branch/client dependency cycle. A +candidate producer can dispatch a stronger evidence envelope, but +`repository_dispatch` always executes the handler from protected `main`. +Conversely, landing the stronger handler first would reject the protected +client's legacy payload and status context. This ADR therefore adopts a +staged protocol on the single canonical handler; it does not create a copied +workflow or permit branch-selected execution. + +The protected bootstrap accepts exactly two event types: + +- `codeql-scan` is temporary legacy v1. It keeps the protected client's + current run title, top-level `required_jobs`, and + `codeql-dispatch/` status context. It rejects nested `pr_head`, + `producer_source_sha`, `rerun_request`, and explicit `rerun_mode` fields so + a v2 caller cannot downgrade its identity checks. +- `codeql-scan-v2` is the proposed v2 contract. The event type is the version + discriminator and consumes no `client_payload` property. It requires the + versioned head envelope, exact synthetic merge `producer_source_sha`, live + base/head parent binding, base-bound status context, and exact handler + gate/SARIF/artifact evidence. + +Both modes share one repository-and-PR concurrency group and one post-matrix +`settle-required-run` job. The matrix scan has `actions:read`; only settlement +has `actions:write`. Settlement revalidates the open PR, repository, base ref +and SHA, head ref and SHA, required run, complete required-job map, terminal +handler jobs, gate steps, and non-expired SARIF artifacts before issuing one +run-wide rerun request. The common concurrency identity prevents v1 and v2 +from becoming simultaneous writers during cutover. + +Live evidence for the amendment is recorded in +`docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md`. In short, +handler run `34684228601` completed both language scans but its matrix-owned +legacy wakes raced: Actions started the required run and Python received HTTP +403. Later same-tuple handler runs were repeatedly cancelled by concurrency, +including `34684575249`, leaving a clean scan without a converged terminal +receipt. This is a settlement-timing defect, not a CodeQL finding. + +Landing sequence is normative: + +1. Land this dual-event, legacy-compatible handler from fresh protected main. +2. Non-force restack the complete successor (#2040), switch its producer to + `codeql-scan-v2`, and generate fresh exact-head end-to-end evidence. +3. Keep legacy v1 until the protected v2 producer is live, all in-flight v1 + required runs are terminal, and repository-wide caller inventory is zero; + then remove v1 with its bridge tests in a separate proven cleanup. + +The ADR remains **Proposed** until that sequence passes ordinary protection +and a real consumer reaches a successful required CodeQL conclusion. Open PR +code is not production authority. From ce3353804990402e093812d0674624aaef0ec1c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:29:43 +0900 Subject: [PATCH 11/28] docs(codeql): define handler-first migration --- docs/product-technical-gap-baseline.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1cc9e20313..2c3e076c23 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2778,6 +2778,28 @@ prose" convention already stated in `CLAUDE.md`. ## Item 41: CodeQL PR `startup_failure` blocking merges org-wide — dispatch-safe re-admission in progress +**2026-09-12 control-plane update — handler-first bootstrap Proposed.** +Protected `main@691fb78932eff5fbe52db69077848134b0b4e053` still runs the +legacy handler while complete successor #2040 is open at +`6476b919d3febf79cc53e71d6d60f15d7e83ced4` (Draft at the latest live +revalidation). Exact predecessor run `34684228601` +proved the current per-language wake cannot converge: Actions woke the shared +required run, then Python received HTTP 403; subsequent same-tuple handler +runs were cancelled and redispatched, including `34684575249`. This is a +canonical `.github` control-plane defect, not a consumer CodeQL finding. + +The minimum repair is one versioned handler, not a workflow copy. Temporary +`codeql-scan` v1 preserves the protected client title/payload/status contract; +`codeql-scan-v2` requires the source/base/head/SARIF evidence carried by +#2040. Both share one repository/PR concurrency identity and a single +post-matrix `actions:write` settlement. The scan matrix is read-only. v1 is +removed only after the protected v2 producer lands, all v1 attempts terminate, +and caller inventory reaches zero. Current status remains **Proposed**: +bootstrap PR ordinary merge, #2040 non-force restack, and a fresh successful +exact-head required CodeQL run are still required. ADR-0025 and +`docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md` carry the +decision and exact evidence. + **2026-09-04 correction.** The emergency ruleset removal below fixed the old entrypoint, but became stale after `.github#1778` moved `github/codeql-action` into the native `codeql-scan-dispatch.yml` handler. Seven current PR heads then From 0102377684ab91f56ef6c7692c872de9182bb65a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:29:49 +0900 Subject: [PATCH 12/28] docs(changelog): record versioned CodeQL bootstrap --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 707c18532e..dc7a41605b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,6 @@ - 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] -- **Define an evidence-backed repository README quality standard.** Added `docs/repository-readme-quality-standard.md` as the shared review contract for product-first structure, code-current onboarding, authority boundaries, durable quality signals, and repository/source/dependency license due diligence. Product repositories continue to own their own README prose; the standard is linked from the root documentation map and does not centralize or generate product claims. - 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 @@ -168,6 +167,14 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- **Define an evidence-backed repository README quality standard.** Added `docs/repository-readme-quality-standard.md` as the shared review contract for product-first structure, code-current onboarding, authority boundaries, durable quality signals, and repository/source/dependency license due diligence. Product repositories continue to own their own README prose; the standard is linked from the root documentation map and does not centralize or generate product claims. +- Add a backward-compatible `codeql-scan`/`codeql-scan-v2` protocol bridge to + the single protected CodeQL dispatch handler. Legacy clients keep their + exact title, payload, and status context while v2 requires source/base/head + provenance. Language scans are `actions:read`; one post-matrix settlement + revalidates the live PR, required run/jobs, handler gate steps, and SARIF + artifacts before one run-wide rerun. The legacy path has an explicit + protected-v2/in-flight-drain/zero-caller removal condition. ADR-0025. - **Pin `opencode-review-dispatch.yml` off the starved floating `ubuntu-latest` image.** The 2026-09-01 floating-image fix (see that entry below) pinned `strix.yml`, `opencode-review.yml`, and `noema-review.yml` -- the three required-check From ae3d0a5c6104bc084735827165a081a6af9ce5af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:29:59 +0900 Subject: [PATCH 13/28] docs(codeql): record versioned handler bootstrap evidence --- ...ql-versioned-handler-bootstrap-20260912.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md diff --git a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md new file mode 100644 index 0000000000..b9601cc067 --- /dev/null +++ b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md @@ -0,0 +1,66 @@ +# CodeQL versioned handler bootstrap — 2026-09-12 + +## Status + +Proposed repair from protected `main@691fb78932eff5fbe52db69077848134b0b4e053`. +No merge or production claim is made here. The complete consumer successor is +PR #2040, revalidated at current head +`6476b919d3febf79cc53e71d6d60f15d7e83ced4`. + +## Exact live evidence + +PR #2040 predecessor head `a9b18b4b24980c7ceb8b8cc0d143a24db20c90bf` +had successful Runtime Quality run `34684155351` (3,127 passed, 1 skipped, +21 subtests; 100% statement, branch, and public-doc coverage), Security run +`34684356405`, SAST run `34684356377`, and Python Security run `34684356416`. +It had no unresolved review threads. The later current head `6476b919...` +moves replay-guard tests without changing this handler source, but historical +hosted results are not inherited. The current head is Draft and had no +associated pull-request workflow runs in the connector snapshot. It therefore +remains unmergeable through ordinary protection. + +Protected handler run `34684228601` is the smallest causal trace. Its Actions +and Python scan, SARIF gate, artifact preservation, and status paths reached +terminal completion. The Actions shard then woke the shared required run. The +Python shard's independent wake received HTTP 403 because that run was no +longer in the terminal-failed state. Same-repository/PR handler runs +`34684373526`, `34684458709`, `34684518320`, and `34684575249` were then +cancelled by the stable concurrency group while retries kept dispatching. In +`34684575249`, Python produced clean scan evidence while its sibling and wake +path did not converge. The repeated consumer symptom was a dispatched success +with a pending terminal verdict. + +## Root cause + +The protected handler woke the required run independently from each matrix +scan job. The first wake changed the run state before the second language +could validate and mutate it. In addition, a candidate stronger consumer +could not prove itself against protected `main`: the old handler lacked its +source-bound title and base-bound receipt, while replacing the handler in one +step would reject the still-protected legacy producer. Re-running either side +alone reproduces the dependency cycle. + +## Repair contract + +One existing handler accepts `codeql-scan` legacy v1 and `codeql-scan-v2`. +The event type is the explicit protocol version, avoiding an eleventh +top-level `client_payload` property. v1 retains the current title, payload, +and status context byte-for-byte at the boundary, while rejecting all v2-only +identity fields. v2 requires the exact source/base/head evidence implemented +by #2040. Both use the same scan implementation and one post-matrix settlement +writer. No scan shard has `actions:write`. + +The bridge removal condition is executable policy: remove v1 only after a +protected v2 producer is live, every in-flight v1 required run is terminal, +and a caller inventory finds zero `codeql-scan` producers. Until then, v1 is a +bounded compatibility port, not production authority for v2 consumers. + +## Verification and next action + +The bootstrap contract executes both payload shapes, rejects v2-to-v1 +downgrade fields, verifies one actions writer after the matrix, and preserves +the legacy and base-bound contexts separately. The full repository suite and +hosted exact-head checks must pass before ordinary merge. After bootstrap +merge, #2040 must non-force absorb protected main, change only its producer +event to `codeql-scan-v2`, and generate new end-to-end evidence; existing +failed or queued runs are not inherited. From d7d92258fe98de57f2fd7c2371d6555bfbc923ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:03:23 +0900 Subject: [PATCH 14/28] test(codeql): reproduce fallback stdout contamination --- tests/test_codeql_scan_dispatch_workflow_contract.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index d13191f5b8..8b048bb189 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -1141,6 +1141,7 @@ def _run_settlement_step( " fi\n" ' if [ -n "${FAKE_DENIED_TOKEN:-}" ] && ' '[ "${GH_TOKEN:-}" = "$FAKE_DENIED_TOKEN" ]; then\n' + ' printf \'%s\\n\' "${FAKE_DENIED_BODY:-}"\n' " exit 1\n" " fi\n" ' if [ "${FAKE_WAKE_POST_FAIL_ALL:-}" = "1" ]; then\n' @@ -1149,7 +1150,10 @@ def _run_settlement_step( ' test "${FAKE_POST_EXIT:-0}" = 0 || exit "$FAKE_POST_EXIT"\n' " exit 0\n" "fi\n" - 'test "${GH_TOKEN:-}" != "${FAKE_DENIED_TOKEN:-}" || exit 1\n' + 'if [ "${GH_TOKEN:-}" = "${FAKE_DENIED_TOKEN:-}" ]; then\n' + ' printf \'%s\\n\' "${FAKE_DENIED_BODY:-}"\n' + " exit 1\n" + "fi\n" 'case "$endpoint" in\n' ' */pulls/*) printf \'%s\\n\' "$FAKE_PULL_JSON" ;;\n' ' repos/ContextualWisdomLab/naruon/actions/runs/42/jobs*) printf \'%s\\n\' "$FAKE_REQUIRED_JOB_PAGES" ;;\n' @@ -1174,6 +1178,7 @@ def _run_settlement_step( "FAKE_POST_LOG": str(post_log), "FAKE_POST_EXIT": "0", "FAKE_DENIED_TOKEN": "", + "FAKE_DENIED_BODY": "", "GH_TOKEN": "fake-token", "TARGET_APP_WAKE_TOKEN": "", "PR_REVIEW_MERGE_WAKE_TOKEN": "", @@ -1300,11 +1305,13 @@ def test_dispatch_settlement_retries_reads_with_next_configured_credential( "OPENCODE_APPROVE_WAKE_TOKEN": "", "GITHUB_WAKE_TOKEN": "", "FAKE_DENIED_TOKEN": "target-token", + "FAKE_DENIED_BODY": '{"message":"Forbidden"}', }, ) assert result.returncode == 0, result.stderr assert "pr-review-merge-token" in result.stdout + assert "jq:" not in result.stderr assert post_log.read_text(encoding="utf-8").splitlines() == [ "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", From 6b476a812150449af6c4214edf95eca51b7737cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:03:28 +0900 Subject: [PATCH 15/28] fix(codeql): isolate failed credential response bodies --- .github/workflows/codeql-scan-dispatch.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 23154ebaec..7efe212955 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -808,7 +808,9 @@ jobs: if [ -z "$token" ]; then return 1 fi - if GH_TOKEN="$token" gh api "$@"; then + api_response="" + if api_response="$(GH_TOKEN="$token" gh api "$@")"; then + printf '%s\n' "$api_response" echo "::notice::CodeQL settlement API used ${token_label}." >&2 return 0 fi From 9bf6a87f707aa5041a241085b31be1644943d3bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:03:31 +0900 Subject: [PATCH 16/28] docs(codeql): record credential fallback RCA --- .../codeql-versioned-handler-bootstrap-20260912.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md index b9601cc067..363296171a 100644 --- a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md +++ b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md @@ -64,3 +64,12 @@ hosted exact-head checks must pass before ordinary merge. After bootstrap merge, #2040 must non-force absorb protected main, change only its producer event to `codeql-scan-v2`, and generate new end-to-end evidence; existing failed or queued runs are not inherited. + +PR #2106 review then exposed a credential-fallback contamination edge case: +`gh api` may emit an HTTP error body to stdout before returning nonzero, so a +failed credential's JSON could precede the later credential's successful +response. The RED fixture makes the rejected credential emit a JSON error body +and rejects any resulting `jq` diagnostic. `run_api` now captures each attempt +and emits its body only after that exact attempt succeeds, preserving stderr +diagnostics and the existing credential order without a temporary-file +lifecycle. From bedce1323ad5872950372942f6ebc22ab1e53994 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:03:38 +0900 Subject: [PATCH 17/28] docs(changelog): record settlement response isolation --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc7a41605b..a9093f1a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,7 +174,9 @@ Semantic Versioning where the repository publishes a release. provenance. Language scans are `actions:read`; one post-matrix settlement revalidates the live PR, required run/jobs, handler gate steps, and SARIF artifacts before one run-wide rerun. The legacy path has an explicit - protected-v2/in-flight-drain/zero-caller removal condition. ADR-0025. + protected-v2/in-flight-drain/zero-caller removal condition. Failed + credential attempts retain their diagnostics but cannot leak an HTTP error + body into a later successful API response. ADR-0025. - **Pin `opencode-review-dispatch.yml` off the starved floating `ubuntu-latest` image.** The 2026-09-01 floating-image fix (see that entry below) pinned `strix.yml`, `opencode-review.yml`, and `noema-review.yml` -- the three required-check From 2c163e92983156739bdeeac87bd8c21383c38145 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:11:04 +0900 Subject: [PATCH 18/28] test(codeql): exercise consumed-field fallback contamination --- .../codeql-versioned-handler-bootstrap-20260912.md | 12 +++++++----- docs/product-technical-gap-baseline.md | 5 ++++- tests/test_codeql_scan_dispatch_workflow_contract.py | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md index 363296171a..8200d2f5c8 100644 --- a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md +++ b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md @@ -68,8 +68,10 @@ failed or queued runs are not inherited. PR #2106 review then exposed a credential-fallback contamination edge case: `gh api` may emit an HTTP error body to stdout before returning nonzero, so a failed credential's JSON could precede the later credential's successful -response. The RED fixture makes the rejected credential emit a JSON error body -and rejects any resulting `jq` diagnostic. `run_api` now captures each attempt -and emits its body only after that exact attempt succeeds, preserving stderr -diagnostics and the existing credential order without a temporary-file -lifecycle. +response. A generic `{"message":"Forbidden"}` body was already discarded by +the current `jq` projections and therefore was not RED. The corrected RED +fixture emits `{"state":"closed"}`, a field the PR validator consumes: before +the repair it is concatenated with the authorized response and rejects that +valid fallback. `run_api` now captures each attempt and emits its body only +after that exact attempt succeeds, preserving stderr diagnostics and the +existing credential order without a temporary-file lifecycle. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2c3e076c23..8d257ccbd6 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2798,7 +2798,10 @@ and caller inventory reaches zero. Current status remains **Proposed**: bootstrap PR ordinary merge, #2040 non-force restack, and a fresh successful exact-head required CodeQL run are still required. ADR-0025 and `docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md` carry the -decision and exact evidence. +decision and exact evidence. Settlement credential fallback releases only the +successful `gh api` body; its RED fixture uses a rejected +`{"state":"closed"}` document because a generic error message does not exercise +the consumed-field contamination path. **2026-09-04 correction.** The emergency ruleset removal below fixed the old entrypoint, but became stale after `.github#1778` moved `github/codeql-action` diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 8b048bb189..f1b8d3b11a 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -1305,7 +1305,9 @@ def test_dispatch_settlement_retries_reads_with_next_configured_credential( "OPENCODE_APPROVE_WAKE_TOKEN": "", "GITHUB_WAKE_TOKEN": "", "FAKE_DENIED_TOKEN": "target-token", - "FAKE_DENIED_BODY": '{"message":"Forbidden"}', + # Use a field consumed by the PR validator: a generic GitHub + # message body was already ignored and did not reproduce the bug. + "FAKE_DENIED_BODY": '{"state":"closed"}', }, ) From 792d65b624b5482995c29cc12633b040b79c25e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:22:53 +0900 Subject: [PATCH 19/28] style(codeql): align rerun-mode fixture mapping --- tests/test_codeql_scan_dispatch_workflow_contract.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 2aeea25a59..0d67bd2bac 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -596,12 +596,12 @@ def test_codeql_scan_dispatch_validate_step_rejects_unknown_rerun_mode(tmp_path) tmp_path, { "SUPPLIED_REQUIRED_JOBS": "null", - "SUPPLIED_RERUN_REQUEST": json.dumps( - { - "schema": "1", - "mode": "one-job", - "required_jobs": [{"language": "python", "job_id": 43}], - } + "SUPPLIED_RERUN_REQUEST": json.dumps( + { + "schema": "1", + "mode": "one-job", + "required_jobs": [{"language": "python", "job_id": 43}], + } ), }, _matching_pull_request(), From 24bb6591ab7df23558cb793b4af60c567ff9da97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:29:14 +0900 Subject: [PATCH 20/28] docs(codeql): correct native rerun allowance --- CHANGELOG.md | 5 +++-- ...eql-versioned-handler-bootstrap-20260912.md | 18 +++++++++++++----- ...t_codeql_scan_dispatch_workflow_contract.py | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f746d7c954..80ce8e0fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -177,8 +177,9 @@ Semantic Versioning where the repository publishes a release. protected-v2/in-flight-drain/zero-caller removal condition. Failed credential attempts retain their diagnostics but cannot leak an HTTP error body into a later successful API response. Nested rerun authority is bound - to string schema `"1"`, and settlement stops before mutation at required-run - attempt 48 so GitHub's 50-attempt ceiling cannot be consumed. ADR-0025. + to string schema `"1"`, and settlement stops before mutation when the + required run reaches attempt 48, preserving capacity below GitHub's limit of + 50 re-runs. ADR-0025. - **Pin `opencode-review-dispatch.yml` off the starved floating `ubuntu-latest` image.** The 2026-09-01 floating-image fix (see that entry below) pinned `strix.yml`, `opencode-review.yml`, and `noema-review.yml` -- the three required-check diff --git a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md index a0336cf7cf..47b4482c9a 100644 --- a/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md +++ b/docs/doctoring/codeql-versioned-handler-bootstrap-20260912.md @@ -81,8 +81,16 @@ guards that the first #2106 tree did not carry. Nested rerun authority now requires the exact string schema `"1"`; missing, numeric, and unknown schemas are rejected before checkout or mutation. The single settlement writer also validates the required run's positive integer `run_attempt` and stops before -mutation at attempt 48, leaving attempts 48–50 unavailable to an automatic -recovery loop. The structured failure records the run, attempt, schema, -languages, and handler identity without changing GitHub's native 50-attempt -limit. This integrates the valid #2105 delta into the backward-compatible -legacy/v2 bridge rather than choosing either incomplete branch unchanged. +mutation when it reaches 48. GitHub documents that `run_attempt` begins at 1 +and increments for every re-run, while one workflow run permits at most 50 +re-runs; the cutoff therefore preserves attempts 49–51 for human recovery +rather than consuming the native allowance automatically. The structured +failure records the run, attempt, schema, languages, and handler identity. +This integrates the valid #2105 delta into the backward-compatible legacy/v2 +bridge rather than choosing either incomplete branch unchanged. + +GitHub. (2026). *Re-running workflows and jobs*. +https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs + +GitHub. (2026). *Variables reference*. +https://docs.github.com/en/actions/reference/workflows-and-actions/variables diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 0d67bd2bac..0448e9846e 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -1268,7 +1268,7 @@ def test_dispatch_settlement_reruns_two_languages_once(tmp_path: Path) -> None: ] -@pytest.mark.parametrize("run_attempt", [48, 49, 50]) +@pytest.mark.parametrize("run_attempt", [48, 49, 50, 51]) def test_dispatch_settlement_stops_before_github_rerun_ceiling( tmp_path: Path, run_attempt: int ) -> None: From 298e6c14c9a56b51c61df2d2e35d7545f0f4f288 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 21:23:29 +0900 Subject: [PATCH 21/28] test(codeql): compare hardened-runner endpoints as exact lines --- ...ization_commercial_readiness_loop_receipt_contract.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_organization_commercial_readiness_loop_receipt_contract.py b/tests/test_organization_commercial_readiness_loop_receipt_contract.py index ce0956bba5..1f694d1e9d 100644 --- a/tests/test_organization_commercial_readiness_loop_receipt_contract.py +++ b/tests/test_organization_commercial_readiness_loop_receipt_contract.py @@ -31,6 +31,7 @@ def test_product_entrypoint_rejects_missing_model_key_or_manual_trigger() -> Non def test_json_receipt_is_retained_as_an_immutable_short_lived_artifact() -> None: """The machine-readable fleet receipt must outlive ephemeral runner storage.""" source = WORKFLOW_PATH.read_text(encoding="utf-8") + source_lines = {line.strip() for line in source.splitlines()} assert ( "uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" @@ -40,6 +41,8 @@ def test_json_receipt_is_retained_as_an_immutable_short_lived_artifact() -> None assert "path: ${{ runner.temp }}/organization-commercial-readiness-loop.json" in source assert "if-no-files-found: error" in source assert "retention-days: 3" in source - assert "results-receiver.actions.githubusercontent.com:443" in source - assert "*.actions.githubusercontent.com:443" in source - assert "*.blob.core.windows.net:443" in source + assert { + "results-receiver.actions.githubusercontent.com:443", + "*.actions.githubusercontent.com:443", + "*.blob.core.windows.net:443", + }.issubset(source_lines) From 877031679ffe74a9bbd568f5ffe87c0583f20a51 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 21:08:10 +0900 Subject: [PATCH 22/28] test(traceability): require owner-qualified cross-repo evidence --- ...t_technical_gap_baseline_evidence_identity.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/test_product_technical_gap_baseline_evidence_identity.py diff --git a/tests/test_product_technical_gap_baseline_evidence_identity.py b/tests/test_product_technical_gap_baseline_evidence_identity.py new file mode 100644 index 0000000000..4845d38ed7 --- /dev/null +++ b/tests/test_product_technical_gap_baseline_evidence_identity.py @@ -0,0 +1,16 @@ +"""Traceability contracts for cross-repository evidence in the product/technical baseline.""" + +import re +from pathlib import Path + +BASELINE_PATH = Path(__file__).resolve().parents[1] / "docs" / "product-technical-gap-baseline.md" + + +def test_cross_repository_evidence_uses_fully_qualified_owner_identity() -> None: + """Cross-repository exact-head evidence must retain owner/repository identity.""" + source = BASELINE_PATH.read_text(encoding="utf-8") + + assert "ContextualWisdomLab/contextual-orchestrator#1149@684cf28f" in source + assert "ContextualWisdomLab/fast-mlsirm@09f762d" in source + assert re.search(r"(? Date: Wed, 16 Sep 2026 21:09:59 +0900 Subject: [PATCH 23/28] revert(traceability): keep baseline repair with canonical document owner --- ...t_technical_gap_baseline_evidence_identity.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 tests/test_product_technical_gap_baseline_evidence_identity.py diff --git a/tests/test_product_technical_gap_baseline_evidence_identity.py b/tests/test_product_technical_gap_baseline_evidence_identity.py deleted file mode 100644 index 4845d38ed7..0000000000 --- a/tests/test_product_technical_gap_baseline_evidence_identity.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Traceability contracts for cross-repository evidence in the product/technical baseline.""" - -import re -from pathlib import Path - -BASELINE_PATH = Path(__file__).resolve().parents[1] / "docs" / "product-technical-gap-baseline.md" - - -def test_cross_repository_evidence_uses_fully_qualified_owner_identity() -> None: - """Cross-repository exact-head evidence must retain owner/repository identity.""" - source = BASELINE_PATH.read_text(encoding="utf-8") - - assert "ContextualWisdomLab/contextual-orchestrator#1149@684cf28f" in source - assert "ContextualWisdomLab/fast-mlsirm@09f762d" in source - assert re.search(r"(? Date: Thu, 17 Sep 2026 02:06:15 +0900 Subject: [PATCH 24/28] test(docs): require owner-qualified cross-repo evidence --- ...product_technical_gap_baseline_identity.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/test_product_technical_gap_baseline_identity.py diff --git a/tests/test_product_technical_gap_baseline_identity.py b/tests/test_product_technical_gap_baseline_identity.py new file mode 100644 index 0000000000..b987bb0562 --- /dev/null +++ b/tests/test_product_technical_gap_baseline_identity.py @@ -0,0 +1,22 @@ +from pathlib import Path + + +BASELINE = Path(__file__).parents[1] / "docs" / "product-technical-gap-baseline.md" +OWNER_QUALIFIED_EVIDENCE = ( + "ContextualWisdomLab/contextual-orchestrator#1149@684cf28f", + "ContextualWisdomLab/fast-mlsirm@09f762d", +) +OWNERLESS_EVIDENCE = ( + "contextual-orchestrator#1149@684cf28f", + "fast-mlsirm@09f762d", +) + + +def test_cross_repository_evidence_keeps_owner_identity() -> None: + """Cross-repository receipts must remain resolvable outside local repo context.""" + baseline = BASELINE.read_text(encoding="utf-8") + + for reference in OWNER_QUALIFIED_EVIDENCE: + assert f"`{reference}`" in baseline + for reference in OWNERLESS_EVIDENCE: + assert f"`{reference}`" not in baseline From 1336eae994859203b08ec40c174b55a0f435ef92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 02:13:51 +0900 Subject: [PATCH 25/28] chore: revert incomplete evidence-identity rehearsal --- ...product_technical_gap_baseline_identity.py | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 tests/test_product_technical_gap_baseline_identity.py diff --git a/tests/test_product_technical_gap_baseline_identity.py b/tests/test_product_technical_gap_baseline_identity.py deleted file mode 100644 index b987bb0562..0000000000 --- a/tests/test_product_technical_gap_baseline_identity.py +++ /dev/null @@ -1,22 +0,0 @@ -from pathlib import Path - - -BASELINE = Path(__file__).parents[1] / "docs" / "product-technical-gap-baseline.md" -OWNER_QUALIFIED_EVIDENCE = ( - "ContextualWisdomLab/contextual-orchestrator#1149@684cf28f", - "ContextualWisdomLab/fast-mlsirm@09f762d", -) -OWNERLESS_EVIDENCE = ( - "contextual-orchestrator#1149@684cf28f", - "fast-mlsirm@09f762d", -) - - -def test_cross_repository_evidence_keeps_owner_identity() -> None: - """Cross-repository receipts must remain resolvable outside local repo context.""" - baseline = BASELINE.read_text(encoding="utf-8") - - for reference in OWNER_QUALIFIED_EVIDENCE: - assert f"`{reference}`" in baseline - for reference in OWNERLESS_EVIDENCE: - assert f"`{reference}`" not in baseline From 84297876feb1fe08b4ed12552d937258af375210 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 02:33:31 +0900 Subject: [PATCH 26/28] test(docs): lock owner-qualified evidence identities --- ...p_baseline_repository_identity_contract.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/test_product_technical_gap_baseline_repository_identity_contract.py diff --git a/tests/test_product_technical_gap_baseline_repository_identity_contract.py b/tests/test_product_technical_gap_baseline_repository_identity_contract.py new file mode 100644 index 0000000000..2acdc2657e --- /dev/null +++ b/tests/test_product_technical_gap_baseline_repository_identity_contract.py @@ -0,0 +1,38 @@ +"""Regression contract for owner-qualified cross-repository evidence identities.""" + +from pathlib import Path +import unittest + + +BASELINE_PATH = ( + Path(__file__).resolve().parents[1] / "docs" / "product-technical-gap-baseline.md" +) + + +class ProductTechnicalGapBaselineRepositoryIdentityContractTests(unittest.TestCase): + """Keep durable cross-repository evidence unambiguous outside its owner repo.""" + + def test_control_opencode_evidence_uses_owner_qualified_repository_identities(self) -> None: + """Reject the two legacy bare repository tokens and require their durable forms.""" + baseline = BASELINE_PATH.read_text(encoding="utf-8") + + legacy_tokens = ( + "`contextual-orchestrator#1149@684cf28f`", + "`fast-mlsirm@09f762d`", + ) + durable_tokens = ( + "`ContextualWisdomLab/contextual-orchestrator#1149@684cf28f`", + "`ContextualWisdomLab/fast-mlsirm@09f762d`", + ) + + for token in legacy_tokens: + with self.subTest(token=token): + self.assertNotIn(token, baseline) + + for token in durable_tokens: + with self.subTest(token=token): + self.assertIn(token, baseline) + + +if __name__ == "__main__": + unittest.main() From 5e4dfe4bf63747159df70adec8a6d6d1b81abee9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 05:17:47 +0900 Subject: [PATCH 27/28] docs: qualify cross-repository evidence identities --- docs/product-technical-gap-baseline.md | 35 ++++++++++++-------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 43027eef50..64c46fd2e5 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -11,7 +11,7 @@ | Gap ID | 상태 | exact-head evidence | causal owner / next gate | |---|---|---|---| -| CONTROL-OPENCODE-VCS-PYROOT-01 | **Proposed / source repaired; hosted exact-head validation pending** | `contextual-orchestrator#1149@684cf28f`의 중앙 [OpenCode run 34701472466](https://github.com/ContextualWisdomLab/.github/actions/runs/34701472466) `coverage-evidence` job `103574547257`은 PR 코드를 실행하기 전에 immutable `fast-mlsirm@09f762d`의 `python/fast_mlsirm` import root를 찾지 못해 종료했다. 같은 head의 제품 테스트는 `3602 passed, 2 skipped`, native CodeQL·fuzz·SBOM·SAST·Strix는 성공했다. | `.github`의 `opencode-review-dispatch.yml`이 root/`src/`만 허용한 계약 drift를 소유한다. RED contract `b1fe97c4`, 최소 source repair `af04581c`, exact workflow-blob trust pin `683cb053` 뒤, 이 문서 head의 integrated CI가 GREEN이고 protected `main`에 ordinary merge된 다음 affected consumer exact head를 다시 검증한다. | +| CONTROL-OPENCODE-VCS-PYROOT-01 | **Proposed / source repaired; hosted exact-head validation pending** | `ContextualWisdomLab/contextual-orchestrator#1149@684cf28f`의 중앙 [OpenCode run 34701472466](https://github.com/ContextualWisdomLab/.github/actions/runs/34701472466) `coverage-evidence` job `103574547257`은 PR 코드를 실행하기 전에 immutable `ContextualWisdomLab/fast-mlsirm@09f762d`의 `python/fast_mlsirm` import root를 찾지 못해 종료했다. 같은 head의 제품 테스트는 `3602 passed, 2 skipped`, native CodeQL·fuzz·SBOM·SAST·Strix는 성공했다. | `.github`의 `opencode-review-dispatch.yml`이 root/`src/`만 허용한 계약 drift를 소유한다. RED contract `b1fe97c4`, 최소 source repair `af04581c`, exact workflow-blob trust pin `683cb053` 뒤, 이 문서 head의 integrated CI가 GREEN이고 protected `main`에 ordinary merge된 다음 affected consumer exact head를 다시 검증한다. | ## 1. 근거와 범위 @@ -272,7 +272,7 @@ flowchart LR `contextual_orchestrator.orchestrator.load_agents()` requires an `{"agents": [...]}` catalog envelope, while the launcher wrote a bare list. Follow-up #1370 fixes the launcher and the standalone policy catalog writer. - Its exact head `0f40d415b112ca0055f5db5b2f434788b08f01f1` merged as + Its exact head `0f40d415b112ca0055f5b2f434788b08f01f1` merged as `24ee38b097dbfc1a895e1199ade48cff36431d05`. - #1370's earlier PR-target Noema run `33140830199` executed the pre-fix trusted base launcher and is retained only as bootstrap reproduction evidence. A @@ -698,7 +698,7 @@ recurrence" section below out of the file entirely; both are restored here.) out of the file entirely; that section is restored verbatim above as part of this correction. - **No PR was merged this pass.** Every refreshed PR's required - `opencode-review`/`noema-review` verdict depends on an asynchronous model + `opencode-review`/`noema-review`/`strix` verdict depends on an asynchronous model dispatch (observed taking on the order of minutes just for sidecar bootstrap and model discovery before any verdict posts) that had not completed for any of the 15 refreshed PRs by the time this pass ended; @@ -709,8 +709,7 @@ recurrence" section below out of the file entirely; both are restored here.) ## 2026-08-30 discovery-error visibility gap in the review sidecar launcher -- While investigating the "2026-08-30 orchestrator/free pool exhausted by - upstream ZDR hardening" entry above, a local reproduction of that incident +- While investigating the "2026-08-30 orchestrator/free pool exhausted by upstream ZDR hardening" entry above, a local reproduction of that incident showed only 3 of the 5 configured providers (`openrouter`, `nvidia_nim`, `nvidia_nim_sub`) and never `bytez`/`openai`, despite all 5 credentials being registered — worth investigating further, since it did not match the @@ -790,12 +789,10 @@ recurrence" section below out of the file entirely; both are restored here.) - This PR bumps `ORCHESTRATOR_PIN_SHA` from `5f2753ace756ddd81049a5221d55e8977572a416` (the #1422 pin) to `30c6d71680e659f25a0a433d4726ad0d437f9757` in the same three places #1422 - established as the contract: the sidecar script default - (`scripts/ci/contextual_orchestrator_review_sidecar.sh`), the contract - test's `ORCH_PIN_SHA` - (`tests/test_contextual_orchestrator_review_sidecar_contract.py`), and - `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md`'s "today" - reference. `requirements.lock` needs no separate sync for the same reason + established as the contract: the sidecar script default, + `tests/test_contextual_orchestrator_review_sidecar_contract.py`'s + `ORCH_PIN_SHA`, and `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md`'s + "today" reference. `requirements.lock` needs no separate sync for the same reason #1422 recorded — the sidecar installs it fresh from the freshly checked-out pinned commit. - Acceptance is open the same way #1422's entry describes: this closes the @@ -1671,8 +1668,8 @@ is chosen. **Decision (same pass): both #1454 and #1455 accepted as known, tracked residual risks — not blocking PR #1452.** This design is a genuine, verified improvement over the status quo it replaces (no diagnostic retry at all, the 120s-timeout bug reproducing repeatedly); it does not need to close every residual -failure mode to be worth merging. #1454's risk is partially mitigated today by `TaskOrchestrator`'s -existing per-request failover/circuit-breaker. #1455's failure mode requires two unlikely conditions to +failure mode to be worth merging. #1454's risk is partially mitigated today by `TaskOrchestrator`'s existing +per-request failover/circuit-breaker. #1455's failure mode requires two unlikely conditions to coincide in one run (discovery near its own worst case *and* probing separately needing close to its full escalation budget) — a tail case, not the common path. Both stay open, decision and reasoning recorded on the issues themselves, cross-referenced from the ADR's Consequences section and both source files. @@ -2218,10 +2215,10 @@ structure end-to-end: two simulated hung calls are killed by `timeout` and grace `gh` starts succeeding. Validation: `coverage run -m pytest tests -q` -- 2173 passed, 1 skipped, 21 subtests passed (up from the -prior 2169-passed baseline by the 3 new tests plus one already landed by a concurrent commit this -session rebased onto); `coverage report` -- 100% on `scripts/ci/` (no `.py` production files touched; the -fix and its tests are entirely in `.github/workflows/opencode-review.yml` and `tests/`); `interrogate` -- -100% docstring coverage (minimum 100.0%, actual 100.0%). `actionlint v1.7.12` (built locally via +prior 2169-passed baseline by the 3 new tests plus one already landed by a concurrent commit this session +rebased onto); `coverage report` -- 100% on `scripts/ci/` (no `.py` production files touched; the fix and its +tests are entirely in `.github/workflows/opencode-review.yml` and `tests/`); `interrogate` -- 100% +docstring coverage (minimum 100.0%, actual 100.0%). `actionlint v1.7.12` (built locally via `go install`, since no prebuilt binary or cached module was reachable through the outbound proxy) reports no findings on the modified workflow file (exit 0). `yaml.safe_load` and `bash -n` both re-confirmed clean on the modified step, and the existing `tests/test_opencode_workflow_shell_syntax.py` suite passes @@ -2331,8 +2328,8 @@ not disputing its correctness -- found assignment under this step's own `set -euo pipefail`, unlike every other `gh api` call in this same step and in the sibling `cancel-closed-pr-runs` job, which are all wrapped in `if ! ... ; then warn; continue/return; fi`. Reproduced concretely: a fake `gh` that fails only this one call (simulating a -transient rate limit or network blip) makes the whole step exit 1, which -- since no later step in this -job declares `continue-on-error` or `if: always()` -- fails the entire `noema-review` job, blocking a +transient rate limit or network blip) makes the whole step exit 1, which -- since no later step in this job +declares `continue-on-error` or `if: always()` -- fails the entire `noema-review` job, blocking a perfectly valid, live-head Noema review over a housekeeping API hiccup unrelated to the review itself (Devin review on #1507). From fdab6730d72dacbe375f466f7434cf085bd4cd2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 05:19:12 +0900 Subject: [PATCH 28/28] fix: restore exact baseline before repository identity repair --- docs/product-technical-gap-baseline.md | 35 ++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 64c46fd2e5..43027eef50 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -11,7 +11,7 @@ | Gap ID | 상태 | exact-head evidence | causal owner / next gate | |---|---|---|---| -| CONTROL-OPENCODE-VCS-PYROOT-01 | **Proposed / source repaired; hosted exact-head validation pending** | `ContextualWisdomLab/contextual-orchestrator#1149@684cf28f`의 중앙 [OpenCode run 34701472466](https://github.com/ContextualWisdomLab/.github/actions/runs/34701472466) `coverage-evidence` job `103574547257`은 PR 코드를 실행하기 전에 immutable `ContextualWisdomLab/fast-mlsirm@09f762d`의 `python/fast_mlsirm` import root를 찾지 못해 종료했다. 같은 head의 제품 테스트는 `3602 passed, 2 skipped`, native CodeQL·fuzz·SBOM·SAST·Strix는 성공했다. | `.github`의 `opencode-review-dispatch.yml`이 root/`src/`만 허용한 계약 drift를 소유한다. RED contract `b1fe97c4`, 최소 source repair `af04581c`, exact workflow-blob trust pin `683cb053` 뒤, 이 문서 head의 integrated CI가 GREEN이고 protected `main`에 ordinary merge된 다음 affected consumer exact head를 다시 검증한다. | +| CONTROL-OPENCODE-VCS-PYROOT-01 | **Proposed / source repaired; hosted exact-head validation pending** | `contextual-orchestrator#1149@684cf28f`의 중앙 [OpenCode run 34701472466](https://github.com/ContextualWisdomLab/.github/actions/runs/34701472466) `coverage-evidence` job `103574547257`은 PR 코드를 실행하기 전에 immutable `fast-mlsirm@09f762d`의 `python/fast_mlsirm` import root를 찾지 못해 종료했다. 같은 head의 제품 테스트는 `3602 passed, 2 skipped`, native CodeQL·fuzz·SBOM·SAST·Strix는 성공했다. | `.github`의 `opencode-review-dispatch.yml`이 root/`src/`만 허용한 계약 drift를 소유한다. RED contract `b1fe97c4`, 최소 source repair `af04581c`, exact workflow-blob trust pin `683cb053` 뒤, 이 문서 head의 integrated CI가 GREEN이고 protected `main`에 ordinary merge된 다음 affected consumer exact head를 다시 검증한다. | ## 1. 근거와 범위 @@ -272,7 +272,7 @@ flowchart LR `contextual_orchestrator.orchestrator.load_agents()` requires an `{"agents": [...]}` catalog envelope, while the launcher wrote a bare list. Follow-up #1370 fixes the launcher and the standalone policy catalog writer. - Its exact head `0f40d415b112ca0055f5b2f434788b08f01f1` merged as + Its exact head `0f40d415b112ca0055f5db5b2f434788b08f01f1` merged as `24ee38b097dbfc1a895e1199ade48cff36431d05`. - #1370's earlier PR-target Noema run `33140830199` executed the pre-fix trusted base launcher and is retained only as bootstrap reproduction evidence. A @@ -698,7 +698,7 @@ recurrence" section below out of the file entirely; both are restored here.) out of the file entirely; that section is restored verbatim above as part of this correction. - **No PR was merged this pass.** Every refreshed PR's required - `opencode-review`/`noema-review`/`strix` verdict depends on an asynchronous model + `opencode-review`/`noema-review` verdict depends on an asynchronous model dispatch (observed taking on the order of minutes just for sidecar bootstrap and model discovery before any verdict posts) that had not completed for any of the 15 refreshed PRs by the time this pass ended; @@ -709,7 +709,8 @@ recurrence" section below out of the file entirely; both are restored here.) ## 2026-08-30 discovery-error visibility gap in the review sidecar launcher -- While investigating the "2026-08-30 orchestrator/free pool exhausted by upstream ZDR hardening" entry above, a local reproduction of that incident +- While investigating the "2026-08-30 orchestrator/free pool exhausted by + upstream ZDR hardening" entry above, a local reproduction of that incident showed only 3 of the 5 configured providers (`openrouter`, `nvidia_nim`, `nvidia_nim_sub`) and never `bytez`/`openai`, despite all 5 credentials being registered — worth investigating further, since it did not match the @@ -789,10 +790,12 @@ recurrence" section below out of the file entirely; both are restored here.) - This PR bumps `ORCHESTRATOR_PIN_SHA` from `5f2753ace756ddd81049a5221d55e8977572a416` (the #1422 pin) to `30c6d71680e659f25a0a433d4726ad0d437f9757` in the same three places #1422 - established as the contract: the sidecar script default, - `tests/test_contextual_orchestrator_review_sidecar_contract.py`'s - `ORCH_PIN_SHA`, and `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md`'s - "today" reference. `requirements.lock` needs no separate sync for the same reason + established as the contract: the sidecar script default + (`scripts/ci/contextual_orchestrator_review_sidecar.sh`), the contract + test's `ORCH_PIN_SHA` + (`tests/test_contextual_orchestrator_review_sidecar_contract.py`), and + `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md`'s "today" + reference. `requirements.lock` needs no separate sync for the same reason #1422 recorded — the sidecar installs it fresh from the freshly checked-out pinned commit. - Acceptance is open the same way #1422's entry describes: this closes the @@ -1668,8 +1671,8 @@ is chosen. **Decision (same pass): both #1454 and #1455 accepted as known, tracked residual risks — not blocking PR #1452.** This design is a genuine, verified improvement over the status quo it replaces (no diagnostic retry at all, the 120s-timeout bug reproducing repeatedly); it does not need to close every residual -failure mode to be worth merging. #1454's risk is partially mitigated today by `TaskOrchestrator`'s existing -per-request failover/circuit-breaker. #1455's failure mode requires two unlikely conditions to +failure mode to be worth merging. #1454's risk is partially mitigated today by `TaskOrchestrator`'s +existing per-request failover/circuit-breaker. #1455's failure mode requires two unlikely conditions to coincide in one run (discovery near its own worst case *and* probing separately needing close to its full escalation budget) — a tail case, not the common path. Both stay open, decision and reasoning recorded on the issues themselves, cross-referenced from the ADR's Consequences section and both source files. @@ -2215,10 +2218,10 @@ structure end-to-end: two simulated hung calls are killed by `timeout` and grace `gh` starts succeeding. Validation: `coverage run -m pytest tests -q` -- 2173 passed, 1 skipped, 21 subtests passed (up from the -prior 2169-passed baseline by the 3 new tests plus one already landed by a concurrent commit this session -rebased onto); `coverage report` -- 100% on `scripts/ci/` (no `.py` production files touched; the fix and its -tests are entirely in `.github/workflows/opencode-review.yml` and `tests/`); `interrogate` -- 100% -docstring coverage (minimum 100.0%, actual 100.0%). `actionlint v1.7.12` (built locally via +prior 2169-passed baseline by the 3 new tests plus one already landed by a concurrent commit this +session rebased onto); `coverage report` -- 100% on `scripts/ci/` (no `.py` production files touched; the +fix and its tests are entirely in `.github/workflows/opencode-review.yml` and `tests/`); `interrogate` -- +100% docstring coverage (minimum 100.0%, actual 100.0%). `actionlint v1.7.12` (built locally via `go install`, since no prebuilt binary or cached module was reachable through the outbound proxy) reports no findings on the modified workflow file (exit 0). `yaml.safe_load` and `bash -n` both re-confirmed clean on the modified step, and the existing `tests/test_opencode_workflow_shell_syntax.py` suite passes @@ -2328,8 +2331,8 @@ not disputing its correctness -- found assignment under this step's own `set -euo pipefail`, unlike every other `gh api` call in this same step and in the sibling `cancel-closed-pr-runs` job, which are all wrapped in `if ! ... ; then warn; continue/return; fi`. Reproduced concretely: a fake `gh` that fails only this one call (simulating a -transient rate limit or network blip) makes the whole step exit 1, which -- since no later step in this job -declares `continue-on-error` or `if: always()` -- fails the entire `noema-review` job, blocking a +transient rate limit or network blip) makes the whole step exit 1, which -- since no later step in this +job declares `continue-on-error` or `if: always()` -- fails the entire `noema-review` job, blocking a perfectly valid, live-head Noema review over a housekeeping API hiccup unrelated to the review itself (Devin review on #1507).