diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 0602e21ad..36769bdef 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -2957,12 +2957,11 @@ jobs: # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. OPENCODE_MODEL_ATTEMPTS: "1" - # Three minutes per model is enough for healthy providers to emit the - # required control block and short enough to avoid queue pileups when a - # provider stalls silently. - OPENCODE_RUN_TIMEOUT_SECONDS: "180" - OPENCODE_EXPORT_TIMEOUT_SECONDS: "60" - OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "540" + # 90 minutes per model gives deep tool-using reviews room to finish; + # stale providers still yield within the bounded retry budget. + OPENCODE_RUN_TIMEOUT_SECONDS: "5400" + OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" + OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000" # Keep cycling through the high-sensitivity catalog until a model # emits a valid control block or the bounded retry/step timeout fires. # A single invalid no-control response is provider/tool state, not a @@ -3364,7 +3363,7 @@ jobs: CHECK_LOOKUP_RETRY_SLEEP_SECONDS: "5" REVIEW_PUBLISH_RETRY_ATTEMPTS: "3" REVIEW_PUBLISH_RETRY_SLEEP_SECONDS: "20" - OPENCODE_MODEL_CANDIDATES: "openai/gpt-5 github-models/openai/gpt-5 github-models/openai/o3 github-models/deepseek/deepseek-r1-0528" + OPENCODE_MODEL_CANDIDATES: "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 github-models/deepseek/deepseek-v3-0324 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct" OPENCODE_MODEL_ATTEMPTS: "1" OPENCODE_RUN_TIMEOUT_SECONDS: "120" OPENCODE_EXPORT_TIMEOUT_SECONDS: "60" 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 867e24fa0..298a1e0ee 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,14 +118,14 @@ 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 id-token: write models: read - statuses: read + statuses: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: @@ -738,6 +735,7 @@ jobs: TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.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 }} @@ -800,6 +798,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 e9752ba91..bea9fb39f 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" @@ -543,9 +543,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { 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: 45' "opencode model pool leaves approval-gate headroom while capping stalled model attempts" 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: "600"' "opencode primary review has a bounded per-model timeout before trying fallback models" - assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400"' "opencode model pool exits before the job timeout so the approval gate can publish a reason" - assert_file_contains "$workflow_file" 'OPENCODE_POOL_MAX_CYCLES: "1"' "opencode model pool stops after one full candidate pass instead of looping to the job timeout" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode primary review has enough per-model time for deep tool-using review 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" + assert_file_not_contains "$workflow_file" "OPENCODE_POOL_MAX_CYCLES" "opencode model pool retries invalid control output until the bounded retry or step timeout fires" + assert_file_contains "$workflow_file" 'OPENCODE_POOL_CYCLE_SLEEP_SECONDS: "15"' "opencode model pool uses a bounded sleep between invalid-output retry cycles" assert_file_contains "$workflow_file" "needs.coverage-evidence.result == 'success'" "opencode model pool only runs after coverage evidence passed" assert_file_contains "$workflow_file" "id: opencode_review_model_pool" "opencode DeepSeek V3 fallback still runs after a primary model timeout or step failure when coverage evidence passed" assert_file_contains "$workflow_file" "always()" "opencode fallback chain uses always() so failed model steps cannot skip every fallback" @@ -657,10 +658,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_not_contains "$workflow_file" "no model produced a valid review control block" "opencode model-failure path no longer documents a final exhausted state" assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode primary and fallback paths avoid multi-attempt stalls on one model" assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode catalog fallback tries each model once before moving on" - assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode catalog fallback has a bounded per-model review timeout before step timeout" + assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode catalog fallback has enough per-model time for deep tool-using review before step timeout" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode %s attempt %s/%s failed" "opencode catalog fallback records per-model retry failures" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "exponential backoff" "opencode model retry paths use exponential backoff instead of fixed sleeps" assert_file_contains "$workflow_file" "openai/gpt-5 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3" "opencode review tries native OpenAI before GitHub Models fallbacks" + assert_file_contains "$workflow_file" "github-models/openai/gpt-5-chat" "opencode review keeps GPT-5 chat fallback coverage after compact reasoning attempts" + assert_file_contains "$workflow_file" "github-models/openai/o3" "opencode review keeps o3 fallback coverage after compact reasoning attempts" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1 github-models/deepseek/deepseek-v3-0324" "opencode review keeps DeepSeek fallback coverage after OpenAI candidates" assert_file_contains "$workflow_file" "coverage-source-tree:" "opencode workflow materializes coverage source before running PR-head tests" assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" @@ -1089,6 +1092,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 3ddd86856..4cd16efc5 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -447,9 +447,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'github-models/deepseek/deepseek-r1-0528"' ) in workflow assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow - assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "180"' in workflow - assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "60"' in workflow - assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "540"' in workflow + assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' in workflow + assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow + assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "18000"' in workflow assert "OPENCODE_POOL_MAX_CYCLES" not in workflow assert 'OPENCODE_POOL_CYCLE_SLEEP_SECONDS: "15"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "5"' in workflow @@ -482,8 +482,10 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert ( 'OPENCODE_MODEL_CANDIDATES: "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: