From 25a3cc954da0b8307baf5dceb22f07df0b441133 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 13:19:59 +0900 Subject: [PATCH 1/3] fix(security): harden central secret and trusted checkout gates --- .github/workflows/noema-review.yml | 54 ++++++++++++++---- .github/workflows/opencode-review.yml | 24 +++++++- .../workflows/pr-review-merge-scheduler.yml | 55 +++++++++++++++---- .github/workflows/secret-scan.yml | 22 ++++++++ .gitleaks.toml | 17 ++++++ .gitleaksignore | 24 ++++++++ scripts/ci/test_strix_quick_gate.sh | 5 +- tests/test_pr_review_merge_scheduler.py | 54 ++++++++++++------ .../test_required_workflow_queue_contract.py | 14 +++++ 9 files changed, 226 insertions(+), 43 deletions(-) create mode 100644 .gitleaks.toml create mode 100644 .gitleaksignore diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 240c693a3..c740cce12 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -18,7 +18,7 @@ on: default: "" type: string canonical_ref: - description: Ref of ContextualWisdomLab/.github to use for trusted review scripts + description: Deprecated; trusted review scripts are resolved from this workflow's trusted source SHA required: false default: main type: string @@ -76,23 +76,55 @@ jobs: if: env.PR_NUMBER != '' id: trusted_source env: - INPUT_CANONICAL_REF: ${{ github.event.inputs.canonical_ref || '' }} - WORKFLOW_REF: ${{ github.workflow_ref }} + JOB_CONTEXT_JSON: ${{ toJSON(job) }} + GITHUB_CONTEXT_JSON: ${{ toJSON(github) }} run: | set -euo pipefail - trusted_ref="${INPUT_CANONICAL_REF:-main}" - case "$WORKFLOW_REF" in - ContextualWisdomLab/.github/.github/workflows/noema-review.yml@*) - trusted_ref="${WORKFLOW_REF##*@}" - ;; - esac - printf 'ref=%s\n' "$trusted_ref" >>"$GITHUB_OUTPUT" + python3 <<'PY' >>"$GITHUB_OUTPUT" + import json + import os + import re + import sys + + try: + job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}") + github_context = json.loads(os.environ.get("GITHUB_CONTEXT_JSON") or "{}") + except json.JSONDecodeError as exc: + print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) + raise SystemExit(1) + + trusted_repository = str( + job_context.get("workflow_repository") or "ContextualWisdomLab/.github" + ).strip() + trusted_ref = str( + job_context.get("workflow_sha") or github_context.get("workflow_sha") or "" + ).strip() + workflow_ref = str( + job_context.get("workflow_ref") or github_context.get("workflow_ref") or "" + ).strip() + + if not trusted_ref: + trusted_ref = "main" + prefix = "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@" + if workflow_ref.startswith(prefix): + trusted_ref = workflow_ref.split("@", 1)[1] + + if trusted_repository != "ContextualWisdomLab/.github": + print("::error::Trusted Noema workflow repository resolved outside ContextualWisdomLab/.github.", file=sys.stderr) + raise SystemExit(1) + if not re.fullmatch(r"[0-9a-fA-F]{40}|refs/[^\s]+|[A-Za-z0-9._/-]+", trusted_ref): + print("::error::Trusted Noema workflow ref resolved to an invalid value.", file=sys.stderr) + raise SystemExit(1) + + print(f"repository={trusted_repository}") + print(f"ref={trusted_ref}") + PY - name: Checkout trusted Noema review gate if: env.PR_NUMBER != '' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: ContextualWisdomLab/.github + repository: ${{ steps.trusted_source.outputs.repository }} ref: ${{ steps.trusted_source.outputs.ref }} fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 1c7eaef01..4dc74695b 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1564,7 +1564,29 @@ jobs: FAILED_CHECK_EVIDENCE_SLEEP_SECONDS: "15" run: | set -euo pipefail - printf 'OPENCODE_CHANGED_FILES_FILE=%s\n' "$OPENCODE_CHANGED_FILES_FILE" >>"$GITHUB_ENV" + + case "${GH_REPOSITORY:-}" in + */*) ;; + *) + echo "::error::Invalid GH_REPOSITORY for OpenCode evidence collection." + exit 1 + ;; + esac + case "${PR_NUMBER:-}" in + ''|*[!0-9]*) + echo "::error::Invalid PR_NUMBER for OpenCode evidence collection." + exit 1 + ;; + esac + for candidate_sha in "$PR_BASE_SHA" "$PR_HEAD_SHA" "$HEAD_SHA"; do + case "$candidate_sha" in + [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) ;; + *) + echo "::error::Invalid commit SHA for OpenCode evidence collection." + exit 1 + ;; + esac + done current_peer_checks_still_running() { local owner="${GH_REPOSITORY%%/*}" diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index 99be104fe..6d0a49f33 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -66,7 +66,7 @@ on: default: "" type: string canonical_ref: - description: Ref of ContextualWisdomLab/.github to use for scheduler code + description: Deprecated; scheduler code is resolved from this workflow's trusted source SHA required: false default: "main" type: string @@ -247,24 +247,57 @@ jobs: - name: Resolve trusted scheduler source ref id: trusted_source env: - INPUT_CANONICAL_REF: ${{ inputs.canonical_ref || '' }} - WORKFLOW_REF: ${{ github.workflow_ref }} + JOB_CONTEXT_JSON: ${{ toJSON(job) }} + GITHUB_CONTEXT_JSON: ${{ toJSON(github) }} run: | set -euo pipefail - trusted_ref="${INPUT_CANONICAL_REF:-main}" - case "$WORKFLOW_REF" in - ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@*) - trusted_ref="${WORKFLOW_REF##*@}" - ;; - esac - printf 'ref=%s\n' "$trusted_ref" >>"$GITHUB_OUTPUT" + python3 <<'PY' >>"$GITHUB_OUTPUT" + import json + import os + import re + import sys + + try: + job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}") + github_context = json.loads(os.environ.get("GITHUB_CONTEXT_JSON") or "{}") + except json.JSONDecodeError as exc: + print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) + raise SystemExit(1) + + trusted_repository = str( + job_context.get("workflow_repository") or "ContextualWisdomLab/.github" + ).strip() + trusted_ref = str( + job_context.get("workflow_sha") or github_context.get("workflow_sha") or "" + ).strip() + workflow_ref = str( + job_context.get("workflow_ref") or github_context.get("workflow_ref") or "" + ).strip() + + if not trusted_ref: + trusted_ref = "main" + prefix = "ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@" + if workflow_ref.startswith(prefix): + trusted_ref = workflow_ref.split("@", 1)[1] + + if trusted_repository != "ContextualWisdomLab/.github": + print("::error::Trusted scheduler workflow repository resolved outside ContextualWisdomLab/.github.", file=sys.stderr) + raise SystemExit(1) + if not re.fullmatch(r"[0-9a-fA-F]{40}|refs/[^\s]+|[A-Za-z0-9._/-]+", trusted_ref): + print("::error::Trusted scheduler workflow ref resolved to an invalid value.", file=sys.stderr) + raise SystemExit(1) + + print(f"repository={trusted_repository}") + print(f"ref={trusted_ref}") + PY - name: Checkout trusted scheduler uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: ContextualWisdomLab/.github + repository: ${{ steps.trusted_source.outputs.repository }} ref: ${{ steps.trusted_source.outputs.ref }} fetch-depth: 1 + persist-credentials: false - name: Self-test scheduler run: python3 scripts/ci/pr_review_merge_scheduler.py --self-test diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 9005ae3e5..65f46ad95 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -78,9 +78,14 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set +e + config_args=() + if [ -f .gitleaks.toml ]; then + config_args=(--config .gitleaks.toml) + fi if [ "${IS_PR}" = "true" ]; then # Diff-scoped: only the commits this PR introduces. ./gitleaks git . \ + "${config_args[@]}" \ --log-opts="${BASE_SHA}..${HEAD_SHA}" \ --redact \ --report-format sarif \ @@ -89,6 +94,7 @@ jobs: else # Full git history on schedule / push to a protected branch. ./gitleaks git . \ + "${config_args[@]}" \ --redact \ --report-format sarif \ --report-path gitleaks-results.sarif \ @@ -96,6 +102,22 @@ jobs: fi echo "rc=$?" >> "$GITHUB_OUTPUT" set -e + - name: Summarize redacted gitleaks findings + if: always() && hashFiles('gitleaks-results.sarif') != '' + run: | + set -euo pipefail + count="$(jq '[.runs[].results[]?] | length' gitleaks-results.sarif)" + if [ "$count" = "0" ]; then + echo "::notice::gitleaks completed with no findings." + exit 0 + fi + echo "::error::gitleaks reported ${count} redacted finding(s). Rule, path, and line summary follows; secret values are not printed." + jq -r ' + .runs[].results[]? + | "- rule: `" + (.ruleId // "unknown") + "`" + + ", path: `" + (.locations[0].physicalLocation.artifactLocation.uri // "unknown") + "`" + + ", line: `" + ((.locations[0].physicalLocation.region.startLine // "unknown") | tostring) + "`" + ' gitleaks-results.sarif | sort | uniq -c - name: Upload gitleaks SARIF to code scanning if: always() && hashFiles('gitleaks-results.sarif') != '' continue-on-error: true diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 000000000..dae881829 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,17 @@ +title = "ContextualWisdomLab central gitleaks configuration" + +[extend] +useDefault = true + +[allowlist] +description = "False-positive token-like strings used by scheduler scrubber regression tests only." +regexTarget = "match" +paths = [ + '''^\.gitleaks\.toml$''', + '''(^|/)__pycache__/''', + '''\.pyc$''', +] +regexes = [ + '''gh[pousr]_(?:1234567890abcdef(?:1234)?|abcdef1234567890abcdef1234567890abcdef|installation_token_value|user_token_value|server_token_value|runner_token_value|placeholder_token_with_underscores_123)''', + '''github_pat_11AAAAA_(?:abcdefg|abcdefg1234567890)''', +] diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 000000000..0987e9e9c --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,24 @@ +# Historical false-positive GitHub-token fixtures from scheduler secret-scrubbing tests. +# The live tests now construct these token-like strings at runtime; new findings remain blocking. +123bddb83426e21367f61affa004f9259b101fe8:tests/test_pr_review_merge_scheduler.py:github-pat:224 +123bddb83426e21367f61affa004f9259b101fe8:tests/test_pr_review_merge_scheduler.py:github-pat:227 +14655a9304c2dbe1e2ad6c963af4a2a0b7bd4b89:tests/test_pr_review_merge_scheduler.py:github-pat:2770 +14655a9304c2dbe1e2ad6c963af4a2a0b7bd4b89:tests/test_pr_review_merge_scheduler.py:github-pat:2778 +14655a9304c2dbe1e2ad6c963af4a2a0b7bd4b89:tests/test_pr_review_merge_scheduler.py:github-pat:2793 +14655a9304c2dbe1e2ad6c963af4a2a0b7bd4b89:tests/test_pr_review_merge_scheduler.py:github-pat:2798 +2ce72097a608290af5828c205f009c430bde7f9e:tests/test_pr_review_merge_scheduler.py:github-pat:2488 +2ce72097a608290af5828c205f009c430bde7f9e:tests/test_pr_review_merge_scheduler.py:github-pat:2496 +2ce72097a608290af5828c205f009c430bde7f9e:tests/test_pr_review_merge_scheduler.py:github-pat:2511 +2ce72097a608290af5828c205f009c430bde7f9e:tests/test_pr_review_merge_scheduler.py:github-pat:2516 +3909b97ae8e42b5e7a0ea6ba78b6f4f4e4a294d4:tests/test_pr_review_merge_scheduler.py:github-pat:2770 +3909b97ae8e42b5e7a0ea6ba78b6f4f4e4a294d4:tests/test_pr_review_merge_scheduler.py:github-pat:2778 +3909b97ae8e42b5e7a0ea6ba78b6f4f4e4a294d4:tests/test_pr_review_merge_scheduler.py:github-pat:2793 +3909b97ae8e42b5e7a0ea6ba78b6f4f4e4a294d4:tests/test_pr_review_merge_scheduler.py:github-pat:2798 +697d44d6a06d148e086340cd25fd026d5d603899:tests/test_pr_review_merge_scheduler.py:github-pat:2488 +697d44d6a06d148e086340cd25fd026d5d603899:tests/test_pr_review_merge_scheduler.py:github-pat:2496 +697d44d6a06d148e086340cd25fd026d5d603899:tests/test_pr_review_merge_scheduler.py:github-pat:2511 +697d44d6a06d148e086340cd25fd026d5d603899:tests/test_pr_review_merge_scheduler.py:github-pat:2516 +79885ae2741c2c8f44b73ba7f7c7b20e7b675966:tests/test_pr_review_merge_scheduler.py:github-pat:697 +79885ae2741c2c8f44b73ba7f7c7b20e7b675966:tests/test_pr_review_merge_scheduler.py:github-pat:701 +79885ae2741c2c8f44b73ba7f7c7b20e7b675966:tests/test_pr_review_merge_scheduler.py:github-pat:718 +79885ae2741c2c8f44b73ba7f7c7b20e7b675966:tests/test_pr_review_merge_scheduler.py:github-pat:722 diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 85b97b9b0..839ef5f43 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1157,9 +1157,10 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "--review-dispatch-limit" "scheduler passes the dispatch budget to the canonical script" assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ github.token }}' "scheduler uses the caller workflow token so mutations are attributed to GitHub Actions in the target repository" assert_file_contains "$workflow_file" "Resolve trusted scheduler source ref" "scheduler required workflow resolves the central trusted source ref" - assert_file_contains "$workflow_file" "github.workflow_ref" "scheduler required workflow can reuse the required-workflow source ref" - assert_file_contains "$workflow_file" 'repository: ContextualWisdomLab/.github' "scheduler checks out the canonical implementation instead of relying on repo-local copies" + assert_file_contains "$workflow_file" "workflow_sha" "scheduler required workflow pins trusted source checkout to the job workflow commit SHA when available" + assert_file_contains "$workflow_file" 'repository: ${{ steps.trusted_source.outputs.repository }}' "scheduler checks out the validated central implementation instead of relying on repo-local copies" assert_file_contains "$workflow_file" 'ref: ${{ steps.trusted_source.outputs.ref }}' "scheduler checks out the resolved central ref" + assert_file_contains "$workflow_file" "persist-credentials: false" "scheduler trusted checkout does not persist workflow credentials into the checkout" assert_file_contains "$workflow_file" "contents: write" "scheduler has write permission for GitHub Actions bot branch updates" assert_file_contains "$workflow_file" "pull-requests: write" "scheduler has pull-request write permission for update-branch and auto-merge" assert_file_not_contains "$workflow_file" "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" "scheduler does not keep stale head-specific concurrency groups" diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 94055d154..b44da04cd 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -7,6 +7,14 @@ from scripts.ci import pr_review_merge_scheduler as sched +def fake_github_token(prefix, body): + return f"{prefix}_{body}" + + +def fake_github_pat(body): + return f"github_pat_{body}" + + def make_pr(**overrides): value = { "number": 1, @@ -1175,7 +1183,7 @@ def mock_run(args, **kwargs): assert sched.run(["success"]) == "success" - token_placeholder = "ghp_placeholder_token_with_underscores_123" + token_placeholder = fake_github_token("ghp", "placeholder_token_with_underscores_123") with pytest.raises(RuntimeError) as exc_info: sched.run(["gh", "api", "fail", "-H", f"Authorization: token {token_placeholder}"]) @@ -3083,18 +3091,18 @@ def fake_inspect(repo, pr, **kwargs): def test_scrub_sensitive_data_and_run_error(): assert sched.scrub_sensitive_data("Authorization: Bearer mytoken123") == "Authorization: Bearer ***" assert sched.scrub_sensitive_data("token mytoken123") == "token ***" - assert sched.scrub_sensitive_data("ghp_1234567890abcdef") == "***" - assert sched.scrub_sensitive_data("ghs_1234567890abcdef") == "***" - assert sched.scrub_sensitive_data("gho_1234567890abcdef") == "***" - assert sched.scrub_sensitive_data("ghp_1234567890abcdef1234") == "***" - assert sched.scrub_sensitive_data("gho_1234567890abcdef1234567890extra") == "***" - assert sched.scrub_sensitive_data("github_pat_11AAAAA_abcdefg1234567890") == "***" - assert sched.scrub_sensitive_data("ghp_placeholder_token_with_underscores_123") == "***" - assert sched.scrub_sensitive_data("gho_installation_token_value") == "***" - assert sched.scrub_sensitive_data("ghu_user_token_value") == "***" - assert sched.scrub_sensitive_data("ghs_server_token_value") == "***" - assert sched.scrub_sensitive_data("ghr_runner_token_value") == "***" - assert sched.scrub_sensitive_data("github_pat_11AAAAA_abcdefg") == "***" + assert sched.scrub_sensitive_data(fake_github_token("ghp", "1234567890abcdef")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("ghs", "1234567890abcdef")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("gho", "1234567890abcdef")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("ghp", "1234567890abcdef1234")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("gho", "1234567890abcdef1234567890extra")) == "***" + assert sched.scrub_sensitive_data(fake_github_pat("11AAAAA_abcdefg1234567890")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("ghp", "placeholder_token_with_underscores_123")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("gho", "installation_token_value")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("ghu", "user_token_value")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("ghs", "server_token_value")) == "***" + assert sched.scrub_sensitive_data(fake_github_token("ghr", "runner_token_value")) == "***" + assert sched.scrub_sensitive_data(fake_github_pat("11AAAAA_abcdefg")) == "***" assert sched.scrub_sensitive_data("sk-1234567890abcdef") == "***" assert sched.scrub_sensitive_data("xoxb-1234567890-1234") == "***" assert sched.scrub_sensitive_data("AKIA1234567890ABCDEF") == "***" @@ -3105,7 +3113,15 @@ def test_scrub_sensitive_data_and_run_error(): assert sched.scrub_sensitive_data(None) is None with pytest.raises(RuntimeError, match=r"Command failed \([12]\): .* \*\*\*"): - sched.run([sys.executable, "-c", "import sys; sys.exit(1)", "ghp_1234567890abcdef1234"], stdin=None) + sched.run( + [ + sys.executable, + "-c", + "import sys; sys.exit(1)", + fake_github_token("ghp", "1234567890abcdef1234"), + ], + stdin=None, + ) def test_main_keeps_scanning_after_update_branch_403_and_422(monkeypatch, capsys): @@ -3214,6 +3230,7 @@ def test_parse_conflict_reason_missing_branches(): def test_run_masks_secrets(): + token = fake_github_token("ghp", "abcdef1234567890abcdef1234567890abcdef") with pytest.raises(RuntimeError) as exc_info: sched.run( [ @@ -3221,7 +3238,7 @@ def test_run_masks_secrets(): "-c", ( "import sys; " - "sys.stderr.write('ghp_abcdef1234567890abcdef1234567890abcdef\\n" + f"sys.stderr.write({token!r} + '\\n" "Bearer super_secret\\ntoken my_secret\\n'); " "sys.exit(1)" ), @@ -3229,7 +3246,7 @@ def test_run_masks_secrets(): ) err_msg = str(exc_info.value) - assert "ghp_abcdef1234567890abcdef1234567890abcdef" not in err_msg + assert token not in err_msg assert "***" in err_msg assert "Bearer super_secret" not in err_msg assert "Bearer ***" in err_msg @@ -3238,16 +3255,17 @@ def test_run_masks_secrets(): def test_run_masks_secrets_in_args(): + token = fake_github_token("ghp", "abcdef1234567890abcdef1234567890abcdef") with pytest.raises(RuntimeError) as exc_info: sched.run( [ sys.executable, "-c", "import sys; sys.exit(1)", - "ghp_abcdef1234567890abcdef1234567890abcdef", + token, ] ) err_msg = str(exc_info.value) - assert "ghp_abcdef1234567890abcdef1234567890abcdef" not in err_msg + assert token not in err_msg assert "***" in err_msg diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 68d580626..08871b7fe 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -159,6 +159,20 @@ def test_noema_workflow_run_without_pull_request_skips_before_token_exchange() - assert workflow.count("if: env.PR_NUMBER != ''") >= 4 +def test_noema_and_scheduler_trusted_checkouts_use_workflow_sha() -> None: + noema = workflow_text("noema-review.yml") + scheduler = workflow_text("pr-review-merge-scheduler.yml") + + for workflow in (noema, scheduler): + assert "workflow_sha" in workflow + assert "workflow_repository" in workflow + assert "Trusted" in workflow or "trusted" in workflow + assert "repository: ${{ steps.trusted_source.outputs.repository }}" in workflow + assert "ref: ${{ steps.trusted_source.outputs.ref }}" in workflow + assert "persist-credentials: false" in workflow + assert "INPUT_CANONICAL_REF" not in workflow + + def test_unassociated_review_workflow_runs_do_not_scan_the_whole_pr_queue() -> None: workflow = workflow_text("pr-review-merge-scheduler.yml") From eacd25e3b809cd8fd0dae172dd15c7a2e04940a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 13:39:13 +0900 Subject: [PATCH 2/3] fix(strix): skip GitHub Models rate-limit backoff --- scripts/ci/strix_quick_gate.sh | 17 ++++++++++ scripts/ci/test_strix_quick_gate.sh | 48 +++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 506cfa556..610b7b29d 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2550,6 +2550,18 @@ is_transient_same_model_retry_error() { return 1 } +github_models_rate_limit_should_skip_same_model_retry() { + local model="$1" + + if ! is_rate_limit_error; then + return 1 + fi + if ! is_github_models_api_compatible_model "$model"; then + return 1 + fi + github_models_api_base_is_active +} + run_strix_with_transient_retry() { local model="$1" local max_attempts=$((STRIX_TRANSIENT_RETRY_PER_MODEL + 1)) @@ -2578,6 +2590,11 @@ run_strix_with_transient_retry() { return 1 fi + if github_models_rate_limit_should_skip_same_model_retry "$model"; then + echo "GitHub Models rate limit detected for model '$model'; skipping same-model retry and moving directly to fallback models or current-head neutral classification." >&2 + return 1 + fi + if ! is_transient_same_model_retry_error "$model"; then return 1 fi diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index f104ec3dd..1c7c0a9c4 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -5127,6 +5127,17 @@ PY "scenario=$scenario does not rewrite logs through symlinked report directories" fi + if [ "$scenario" = "github-models-primary-ratelimit-fallback-success" ]; then + assert_file_contains \ + "$output_log" \ + "GitHub Models rate limit detected for model 'openai/gpt-5'; skipping same-model retry and moving directly to fallback models or current-head neutral classification." \ + "scenario=$scenario logs why same-model retry was skipped" + assert_file_not_contains \ + "$output_log" \ + "Retrying model 'openai/gpt-5' due to rate limit" \ + "scenario=$scenario does not sleep in same-model retry after GitHub Models rate limiting" + fi + if [ "$scenario" = "pr-changed-scope-full-set" ]; then assert_internal_pr_scope_targets "$target_log" "$repo_root_dir" "$expected_calls" fi @@ -5275,6 +5286,37 @@ run_filtered_gate_case_if_requested() { "pull_request" \ "frontend/src/components/CalendarLayout.tsx" ;; + github-models-primary-ratelimit-fallback-success) + run_gate_case "github-models-primary-ratelimit-fallback-success" \ + "openai/gpt-5" \ + "" \ + "0" \ + "REGEX:Strix quick scan succeeded with fallback model 'deepseek/deepseek-r1-0528' in [0-9]+s\\." \ + "2" \ + "openai/gpt-5|openai/deepseek/deepseek-r1-0528" \ + "https://models.github.ai/inference|https://models.github.ai/inference" \ + "openai" \ + "https://models.github.ai/inference" \ + "" \ + "2" \ + "CRITICAL" \ + "0" \ + "" \ + "" \ + "1200" \ + "0" \ + "" \ + "" \ + "" \ + "" \ + "0" \ + "" \ + "" \ + "" \ + "__SAME_AS_FALLBACK_MODELS__" \ + "deepseek/deepseek-r1-0528 deepseek/deepseek-v3-0324" \ + "1" + ;; github-models-fallback-baseline-vulnerability-before-next-success-continues) run_gate_case "github-models-fallback-baseline-vulnerability-before-next-success-continues" \ "openai/gpt-5" \ @@ -8245,9 +8287,9 @@ run_gate_case "github-models-primary-ratelimit-fallback-success" \ "" \ "0" \ "REGEX:Strix quick scan succeeded with fallback model 'deepseek/deepseek-r1-0528' in [0-9]+s\\." \ - "4" \ - "openai/gpt-5|openai/gpt-5|openai/gpt-5|openai/deepseek/deepseek-r1-0528" \ - "https://models.github.ai/inference|https://models.github.ai/inference|https://models.github.ai/inference|https://models.github.ai/inference" \ + "2" \ + "openai/gpt-5|openai/deepseek/deepseek-r1-0528" \ + "https://models.github.ai/inference|https://models.github.ai/inference" \ "openai" \ "https://models.github.ai/inference" \ "" \ From 8ecbec5e34ec5833e9d46a6f74e3d7382cc65288 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 14:12:38 +0900 Subject: [PATCH 3/3] fix(actions): harden central review checkouts --- .github/workflows/noema-review.yml | 2 +- .github/workflows/opencode-review.yml | 1 - .github/workflows/pr-review-merge-scheduler.yml | 2 +- scripts/ci/test_strix_quick_gate.sh | 3 ++- tests/test_opencode_agent_contract.py | 1 + tests/test_required_workflow_queue_contract.py | 3 ++- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 0e909ddaa..257b934d6 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -119,7 +119,7 @@ jobs: if: env.PR_NUMBER != '' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: ${{ steps.trusted_source.outputs.repository }} + repository: ContextualWisdomLab/.github ref: ${{ steps.trusted_source.outputs.ref }} fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 71724fd63..0e3dd1fea 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1663,7 +1663,6 @@ jobs: done <"$context_env_file" printf 'Resolved bounded OpenCode review context for %s#%s at %s.\n' \ "$GH_REPOSITORY" "$PR_NUMBER" "$PR_HEAD_SHA" - printf 'OPENCODE_CHANGED_FILES_FILE=%s\n' "$OPENCODE_CHANGED_FILES_FILE" >>"$GITHUB_ENV" current_peer_checks_still_running() { local owner="${GH_REPOSITORY%%/*}" diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index 3a832467e..c2b270e51 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -289,7 +289,7 @@ jobs: - name: Checkout trusted scheduler uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: ${{ steps.trusted_source.outputs.repository }} + repository: ContextualWisdomLab/.github ref: ${{ steps.trusted_source.outputs.ref }} fetch-depth: 1 persist-credentials: false diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 53aaf9edb..d7e21f142 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1166,7 +1166,8 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "workflow_sha" "scheduler trusted source ref prefers the immutable workflow commit when available" assert_file_not_contains "$workflow_file" "INPUT_CANONICAL_REF" "scheduler trusted source checkout must not be controlled by workflow input" assert_file_not_contains "$workflow_file" "inputs.canonical_ref" "scheduler no longer accepts checkout-ref override input" - assert_file_contains "$workflow_file" 'repository: ${{ steps.trusted_source.outputs.repository }}' "scheduler checks out the validated central implementation instead of relying on repo-local copies" + assert_file_contains "$workflow_file" 'repository: ContextualWisdomLab/.github' "scheduler checks out the canonical implementation instead of relying on repo-local copies" + assert_file_not_contains "$workflow_file" 'repository: ${{ steps.trusted_source.outputs.repository }}' "scheduler does not pass a dynamic repository expression to privileged checkout" assert_file_contains "$workflow_file" 'ref: ${{ steps.trusted_source.outputs.ref }}' "scheduler checks out the resolved central ref" assert_file_contains "$workflow_file" "persist-credentials: false" "scheduler trusted checkout does not persist workflow credentials into the checkout" assert_file_contains "$workflow_file" "contents: write" "scheduler has write permission for GitHub Actions bot branch updates" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 3be3f7e26..264919c0a 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -161,6 +161,7 @@ def test_opencode_bounded_evidence_context_is_resolved_from_event_payload(): assert "GITHUB_EVENT_PATH" in step assert "Invalid OpenCode review context value for" in step assert "Resolved bounded OpenCode review context for %s#%s at %s." in step + assert "GITHUB_ENV" not in step def test_opencode_target_coverage_materializes_merge_tree_without_checkout_action(): diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 3af715d32..6314b454d 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -180,7 +180,8 @@ def test_noema_and_scheduler_trusted_checkouts_use_workflow_sha() -> None: assert "workflow_sha" in workflow assert "workflow_repository" in workflow assert "Trusted" in workflow or "trusted" in workflow - assert "repository: ${{ steps.trusted_source.outputs.repository }}" in workflow + assert "repository: ContextualWisdomLab/.github" in workflow + assert "repository: ${{ steps.trusted_source.outputs.repository }}" not in workflow assert "ref: ${{ steps.trusted_source.outputs.ref }}" in workflow assert "persist-credentials: false" in workflow assert "INPUT_CANONICAL_REF" not in workflow