From d0754191560cfc7b6e09ba0e2c537d85f4a2b62e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 17:28:17 +0900 Subject: [PATCH 1/2] fix(ci): harden current-head security queue evidence --- .github/workflows/opencode-review.yml | 2 +- .../workflows/pr-review-merge-scheduler.yml | 2 +- .github/workflows/strix.yml | 15 +++++----- scripts/ci/sandboxed_web_e2e.py | 1 + scripts/ci/strix_quick_gate.sh | 4 +++ scripts/ci/strix_required_workflow_smoke.sh | 28 ++----------------- scripts/ci/test_strix_quick_gate.sh | 3 +- tests/test_opencode_agent_contract.py | 4 ++- .../test_required_workflow_queue_contract.py | 4 ++- 9 files changed, 26 insertions(+), 37 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index dc6e11e35..f4b448e85 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3366,7 +3366,7 @@ jobs: CHECK_LOOKUP_RETRY_SLEEP_SECONDS: "5" REVIEW_PUBLISH_RETRY_ATTEMPTS: "3" REVIEW_PUBLISH_RETRY_SLEEP_SECONDS: "20" - OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5 github-models/openai/gpt-5 github-models/openai/o3 github-models/deepseek/deepseek-r1-0528" + OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" OPENCODE_MODEL_ATTEMPTS: "1" OPENCODE_RUN_TIMEOUT_SECONDS: "5400" OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index e074c6390..a6d21b15c 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -316,7 +316,7 @@ jobs: SCHEDULER_READ_TOKEN: ${{ github.token }} SCHEDULER_MUTATION_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.scheduler_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }} SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github - SCHEDULER_REQUIRED_WORKFLOW_REF: main + SCHEDULER_REQUIRED_WORKFLOW_REF: ${{ steps.trusted_source.outputs.ref }} SCHEDULER_ALLOW_CROSS_REPO_WORKFLOW_DISPATCH: ${{ (secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '') && 'true' || 'false' }} run: | set -euo pipefail diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 696c69fb3..1b20f91a0 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -85,18 +85,15 @@ on: concurrency: # Include the event name so manual workflow_dispatch evidence cannot cancel # the required pull_request_target Strix context that branch protection reads. - # Within each event class, PR-number scope still keeps only the current head. + # PR-number scope keeps the queue on the current HEAD within each event class. group: >- strix-${{ github.event_name }}-${{ github.event.inputs.target_repository || github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number != '' && format('pr-{0}', github.event.inputs.pr_number) || github.ref }} - # PR-number scope keeps the queue on the current HEAD: a synchronize event - # cancels older Strix evidence for the same PR before it burns reviewer time. cancel-in-progress: true # Scorecard Token-Permissions (alert #43): keep the workflow-level token -# read-only and grant status publication only through exchanged app/secret -# tokens. GITHUB_TOKEN can read status evidence but must not write it. +# read-only and scope same-repo status publication to the Strix scan job. permissions: actions: read contents: read @@ -121,8 +118,8 @@ jobs: timeout-minutes: 45 runs-on: ubuntu-latest # Least-privilege token scoped to this job (Scorecard alert #43): the scan - # exchanges an OIDC token (id-token) and reads commit status evidence here; - # publication uses exchanged app/secret tokens below. + # exchanges an OIDC token (id-token) and publishes same-repo status evidence + # from the scan job only. permissions: actions: read contents: read @@ -739,6 +736,7 @@ jobs: GITHUB_STATUS_TOKEN: ${{ (github.event.inputs.target_repository == '' || github.event.inputs.target_repository == github.repository) && github.token || '' }} PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} + GITHUB_STATUS_TOKEN: ${{ github.token }} TARGET_REPOSITORY: ${{ github.event.inputs.target_repository || github.repository }} PR_HEAD_SHA: ${{ github.event.inputs.pr_head_sha }} STRIX_RESULT: ${{ job.status }} @@ -804,6 +802,9 @@ jobs: if post_strix_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then exit 0 fi + if post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then + exit 0 + fi echo "::warning::Could not publish manual Strix status from scan job; keeping scan evidence result authoritative in the workflow run." publish-manual-pr-evidence-status: diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index 682202fad..820ba5c67 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -11,6 +11,7 @@ import sys import tempfile import time +import typing import urllib.error import urllib.request from collections.abc import Sequence diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 610b7b29d..226d03edb 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -3820,6 +3820,10 @@ run_current_target_scan() { fi fi + if fail_reported_vulnerabilities_before_fallback_success; then + return 1 + fi + case "$PR_FINDINGS_DECISION" in block_changed | block_unmapped | block_manifest_unverified) return 1 diff --git a/scripts/ci/strix_required_workflow_smoke.sh b/scripts/ci/strix_required_workflow_smoke.sh index f51b8c353..714858829 100755 --- a/scripts/ci/strix_required_workflow_smoke.sh +++ b/scripts/ci/strix_required_workflow_smoke.sh @@ -83,7 +83,6 @@ if any(line.strip() == "statuses: write" for line in top_level_permissions): print("Strix workflow top-level GITHUB_TOKEN must not grant statuses: write.", file=sys.stderr) raise SystemExit(1) -status_read_jobs: list[str] = [] status_write_jobs: list[str] = [] current_job = "" inside_permissions = False @@ -99,37 +98,16 @@ for line in lines[jobs_index + 1 :]: if not inside_permissions: continue if line.startswith(" "): - if line.strip() == "statuses: read": - status_read_jobs.append(current_job) if line.strip() == "statuses: write": status_write_jobs.append(current_job) continue if line.strip(): inside_permissions = False -if status_read_jobs and status_read_jobs != ["strix"]: +if status_write_jobs != ["strix"]: print( - "Strix workflow must scope statuses: read only to the strix scan job when read is used; found: " - + ", ".join(status_read_jobs), - file=sys.stderr, - ) - raise SystemExit(1) -if status_write_jobs and status_write_jobs != ["strix"]: - print( - "Strix workflow must scope statuses: write only to the strix scan job when write is used; found: " - + ", ".join(status_write_jobs), - file=sys.stderr, - ) - raise SystemExit(1) -if status_read_jobs and status_write_jobs: - print( - "Strix workflow must choose statuses: read or statuses: write for the strix scan job, not both.", - file=sys.stderr, - ) - raise SystemExit(1) -if not status_read_jobs and not status_write_jobs: - print( - "Strix workflow must scope statuses: read or statuses: write to the strix scan job; found: none", + "Strix workflow must scope statuses: write only to the strix scan job; found: " + + (", ".join(status_write_jobs) if status_write_jobs else "none"), file=sys.stderr, ) raise SystemExit(1) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index e932317cd..87afc7385 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -429,7 +429,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'install_deps <- c("Depends", "Imports", "LinkingTo")' "opencode R coverage avoids installing oversized suggested dependencies" assert_file_contains "$workflow_file" 'read.dcf("DESCRIPTION")' "opencode R coverage installs target package dependencies from DESCRIPTION" assert_file_contains "$workflow_file" "R package testthat suite" "opencode R package coverage requires package testthat evidence" - assert_file_contains "$workflow_file" "R coverage tooling install unavailable or exceeded the runner time budget; deferring to required peer R CMD check evidence." "opencode R coverage defers runner package-install failures to required peer R checks" + assert_file_contains "$workflow_file" "R coverage tooling install did not complete or exceeded 780 seconds" "opencode R coverage defers runner package-install failures to required peer R checks" assert_file_contains "$workflow_file" "testthat unavailable in coverage runner; deferring to required peer R CMD check evidence." "opencode R package tests defer only when testthat cannot be installed in the coverage runner" assert_file_contains "$workflow_file" "covr package_coverage unavailable after package tests; treating missing-line report as advisory." "opencode R package coverage does not block on covr installation reproduction after tests pass" assert_file_contains "$workflow_file" "R coverage tooling packages unavailable after install" "opencode R coverage verifies covr/testthat are loadable after installation" @@ -1091,6 +1091,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_not_contains "$workflow_file" "MODEL: github-models/gpt-4.1" "opencode review must not fall back to GPT-4.1" assert_file_contains "$workflow_file" "github-models/openai/gpt-5-chat" "opencode review includes GitHub Models GPT-5 chat as a catalog fallback" assert_file_not_contains "$workflow_file" "github-models/openai/gpt-4.1-mini" "opencode review does not fall back to GPT-4.1 mini review evidence" + assert_file_contains "$workflow_file" "github-models/openai/gpt-5" "opencode review includes GitHub Models GPT-5 as a catalog fallback" assert_file_not_contains "$workflow_file" "github-models/openai/gpt-5-mini" "opencode review excludes GitHub Models GPT-5 mini from the high-sensitivity review pool" assert_file_contains "$opencode_config" '"mcp"' "opencode config declares MCP servers" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index cb2564fb3..872f3eb58 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -489,8 +489,10 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' "openai/gpt-5 " "github-models/openai/gpt-5 " + "github-models/openai/gpt-5-chat " "github-models/openai/o3 " - 'github-models/deepseek/deepseek-r1-0528"' + "github-models/deepseek/deepseek-r1-0528 " + 'github-models/deepseek/deepseek-r1' ) in workflow assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow assert re.search(r'check-runs" \\\n\s+-f per_page=100 \\\n\s+--paginate \\\n\s+--slurp \|\n\s+jq -r "\$jq_filter"', workflow) diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 14a080b28..38c36c79f 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -101,6 +101,7 @@ def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() - strix_workflow = workflow_text("strix.yml") assert "cancel-in-progress: true" in strix_workflow + assert "PR-number scope keeps the queue on the current HEAD" in strix_workflow def test_close_empty_pr_metadata_lookup_retries_and_fails_open() -> None: @@ -114,6 +115,7 @@ def test_close_empty_pr_metadata_lookup_retries_and_fails_open() -> None: assert "exit 0" in workflow + def test_cancelled_review_workflow_runs_do_not_spawn_more_queue_work() -> None: for filename in ("noema-review.yml", "pr-review-merge-scheduler.yml"): workflow = workflow_text(filename) @@ -175,7 +177,7 @@ def test_noema_workflow_run_without_pull_request_skips_before_token_exchange() - assert "Noema review skipped: no pull request number is associated with this event." in workflow assert "if: env.PR_NUMBER == ''" in workflow - assert workflow.count("if: env.PR_NUMBER != ''") >= 3 + assert workflow.count("if: env.PR_NUMBER != ''") >= 4 def test_noema_and_scheduler_trusted_checkouts_use_static_main() -> None: From feb7e75236eabc5863b90f8a1d276ca6ecf38004 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 20:08:17 +0900 Subject: [PATCH 2/2] ci: address review gate follow-ups --- scripts/ci/sandboxed_web_e2e.py | 1 - scripts/ci/strix_quick_gate.sh | 4 ---- scripts/ci/test_strix_quick_gate.sh | 19 +++++++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index 820ba5c67..682202fad 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -11,7 +11,6 @@ import sys import tempfile import time -import typing import urllib.error import urllib.request from collections.abc import Sequence diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 226d03edb..610b7b29d 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -3820,10 +3820,6 @@ run_current_target_scan() { fi fi - if fail_reported_vulnerabilities_before_fallback_success; then - return 1 - fi - case "$PR_FINDINGS_DECISION" in block_changed | block_unmapped | block_manifest_unverified) return 1 diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 87afc7385..1cff76cf0 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -15,6 +15,20 @@ GATE_SCRIPT="$REPO_ROOT/scripts/ci/strix_quick_gate.sh" FAILURES=0 +# Keep local developer/provider secrets from changing fake Strix model routing. +unset STRIX_LLM +unset LLM_API_KEY +unset LLM_API_BASE +unset OPENAI_API_KEY +unset STRIX_GITHUB_MODELS_TOKEN +unset LITELLM_API_KEY +unset LITELLM_MASTER_KEY +unset GEMINI_API_KEY +unset GOOGLE_APPLICATION_CREDENTIALS +if ! python3 -c 'import pathlib' >/dev/null 2>&1; then + export PATH="/opt/homebrew/bin:/usr/bin:/bin:$PATH" +fi + record_failure() { echo "FAIL: $1" >&2 FAILURES=$((FAILURES + 1)) @@ -542,6 +556,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'timeout-minutes: 420' "opencode review target keeps a bounded runner budget so deep reviews can finish and stalled reviews still release queue capacity" assert_file_contains "$workflow_file" 'timeout-minutes: 40' "opencode evidence preparation fails closed before it ties up the review queue" assert_file_contains "$workflow_file" 'timeout-minutes: 350' "opencode model pool gives deep tool-using reviews more than 30 minutes while capping stalled model attempts" + assert_file_contains "$workflow_file" 'timeout-minutes: 45' "opencode approval step keeps a bounded post-review publication budget" assert_file_contains "$workflow_file" 'continue-on-error: true' "opencode approval gate still runs after model-pool failure to publish a reason" assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode primary review gives each model 90 minutes before trying fallback models" assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000"' "opencode model pool exits before the job timeout so the approval gate can publish a reason" @@ -920,6 +935,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }}' "opencode evidence step passes HEAD_SHA to failed-check evidence collection" assert_file_contains "$workflow_file" "FAILED_CHECK_EVIDENCE_ATTEMPTS" "opencode review workflow bounds waiting for peer check failures before model review" assert_file_contains "$workflow_file" 'timeout-minutes: 350' "opencode model stage has a bounded but deep-review-capable timeout" + assert_file_contains "$workflow_file" 'timeout-minutes: 40' "opencode evidence preparation has a bounded peer-check wait timeout" assert_file_contains "$workflow_file" 'FAILED_CHECK_EVIDENCE_ATTEMPTS: "20"' "opencode review workflow keeps pre-model peer-check waiting bounded for required workflow DX" assert_file_contains "$workflow_file" 'FAILED_CHECK_EVIDENCE_SLEEP_SECONDS: "15"' "opencode review workflow retries peer-check evidence without stalling the model stage for Strix-scale durations" assert_file_contains "$workflow_file" "found completed failed peer-check evidence while other peer checks are still running" "opencode evidence preparation retries stale failed checks while peer checks are pending" @@ -2781,6 +2797,9 @@ run_gate_case() { if [ -n "${STRIX_TEST_CASE_FILTER:-}" ] && [ "$scenario" != "$STRIX_TEST_CASE_FILTER" ]; then return fi + if [ "${STRIX_TEST_TRACE_CASES:-0}" = "1" ]; then + printf 'RUN_GATE_CASE: %s\n' "$scenario" >&2 + fi local tmp_dir tmp_dir="$(mktemp -d)"