diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index f792ad0bf..17d2ce50b 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1,8 +1,6 @@ name: OpenCode Review on: - pull_request_target: - types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: inputs: pr_number: @@ -50,7 +48,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - ref: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + ref: ${{ github.event.inputs.pr_head_sha }} - name: Install Python coverage measurement tools run: python3 -m pip install --disable-pip-version-check -r requirements-opencode-review-ci.txt @@ -58,7 +56,7 @@ jobs: - name: Measure test and docstring coverage at 100 percent id: measure env: - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_HEAD_SHA: ${{ github.event.inputs.pr_head_sha }} run: | set -euo pipefail @@ -220,16 +218,7 @@ jobs: opencode-review-target: name: opencode-review needs: [coverage-evidence] - if: >- - always() - && ( - github.event_name == 'workflow_dispatch' - || ( - github.event_name == 'pull_request_target' - && github.event.pull_request.draft != true - && github.event.pull_request.head.repo.full_name == github.repository - ) - ) + if: always() && github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: actions: read @@ -243,15 +232,7 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - - name: Checkout trusted review workflow - if: github.event_name == 'pull_request_target' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Checkout trusted review workflow for manual PR review - if: github.event_name == 'workflow_dispatch' + - name: Checkout current-head review workflow for manual PR review uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 @@ -328,6 +309,7 @@ jobs: npx -y "$CODEGRAPH_PACKAGE" status - name: Prepare bounded OpenCode review evidence + timeout-minutes: 40 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPOSITORY: ${{ github.repository }} @@ -339,19 +321,21 @@ jobs: OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} - FAILED_CHECK_EVIDENCE_ATTEMPTS: "31" - FAILED_CHECK_EVIDENCE_SLEEP_SECONDS: "10" + FAILED_CHECK_EVIDENCE_ATTEMPTS: "75" + FAILED_CHECK_EVIDENCE_SLEEP_SECONDS: "30" run: | set -euo pipefail current_peer_checks_still_running() { local owner="${GH_REPOSITORY%%/*}" local name="${GH_REPOSITORY#*/}" + local rollup_running + local strix_running # Exclude this OpenCode check run; otherwise the evidence step would # wait on itself until the bounded retry budget is exhausted. # shellcheck disable=SC2016 - gh api graphql \ + if ! rollup_running="$(gh api graphql \ -f owner="$owner" \ -f name="$name" \ -F number="$PR_NUMBER" \ @@ -401,7 +385,33 @@ jobs: end ] | length > 0 - ' + ')"; then + return 1 + fi + if [ "$rollup_running" = "true" ]; then + printf 'true\n' + return 0 + fi + + strix_running="$( + env HEAD_SHA="$HEAD_SHA" gh run list \ + --repo "$GH_REPOSITORY" \ + --workflow strix.yml \ + --commit "$HEAD_SHA" \ + --limit 200 \ + --json status,event,headSha,workflowName \ + --jq ' + [ + .[] + | select((.headSha // "") == env.HEAD_SHA) + | select((.workflowName // "") == "Strix Security Scan" or (.workflowName // "") == "Strix") + | select((.event // "") == "pull_request_target" or (.event // "") == "workflow_dispatch") + | select((.status // "") != "completed") + ] + | length > 0 + ' 2>/dev/null || printf 'false' + )" + printf '%s\n' "$strix_running" } collect_failed_check_evidence_with_wait() { @@ -421,15 +431,27 @@ jobs: while [ "$attempt" -le "$attempts" ]; do if scripts/ci/collect_failed_check_evidence.sh "$evidence_file"; then - if ! grep -Fq "No completed failed GitHub Checks were present" "$evidence_file"; then - return 0 - fi if [ "$(current_peer_checks_still_running 2>/dev/null || printf 'false')" != "true" ]; then return 0 fi + if ! grep -Fq "No completed failed GitHub Checks were present" "$evidence_file" && + ! grep -Fq "No active failed GitHub Checks remained after superseded checks were classified" "$evidence_file"; then + printf 'Failed-check evidence attempt %s/%s found completed failed peer-check evidence while other peer checks are still running; retrying in %ss before model review.\n' "$attempt" "$attempts" "$sleep_seconds" >&2 + else + printf 'Failed-check evidence attempt %s/%s found no active completed peer-check failure while peer checks are still running; retrying in %ss before model review.\n' "$attempt" "$attempts" "$sleep_seconds" >&2 + fi + if [ "$attempt" -lt "$attempts" ]; then + sleep "$sleep_seconds" + fi + attempt=$((attempt + 1)) + continue fi if [ "$attempt" -lt "$attempts" ]; then + if [ "$(current_peer_checks_still_running 2>/dev/null || printf 'false')" != "true" ]; then + break + fi + printf 'Failed-check evidence attempt %s/%s could not collect evidence while peer checks are still running; retrying in %ss before model review.\n' "$attempt" "$attempts" "$sleep_seconds" >&2 sleep "$sleep_seconds" fi attempt=$((attempt + 1)) @@ -1050,8 +1072,6 @@ jobs: If bounded failed GitHub Check evidence contains active failed checks, treat it as a blocker until diagnosed. If every active failed-check block says the job was not started because the GitHub account is locked due to a billing issue, classify it as an external CI/account blocker with no repository source fix; do not invent source-backed REQUEST_CHANGES findings for it. If the evidence says no completed failed GitHub Checks were present, do not request changes solely from that section. A successful same-head manual workflow_dispatch Strix run may supersede a stale failed PR statusCheckRollup Strix context only when failed-check evidence explicitly lists it under Superseded failed checks with the exact target URL; otherwise treat failed rollup contexts as blockers. For Strix or other GitHub Checks, use the failed log excerpt and annotations to identify the exact local file line that must change, then provide a concrete from/to fix and suggested diff. When Strix evidence contains multiple model vulnerability reports, include every model-reported vulnerability as a separate evidence-backed finding, preserving each report's model name, title, severity, endpoint, and Code Locations/path:line evidence when present. When evidence supports it, name the concrete CWE/KISA-style class such as injection, auth/authz, secrets, crypto, path traversal/file upload, XSS/CSRF/SSRF, error disclosure, or debug/deployment config; do not invent a category without evidence. One Strix model vulnerability report requires one distinct finding; do not combine duplicate titles or matching locations from different models into one finding. Do not request changes with only a check URL, workflow name, or generic failure summary. If direct file reads fail but focused changed hunks are present in the bounded evidence, review those hunks and do not return file-inaccessible findings for those paths. Full failed-check evidence, when collected, is available as failed-check-evidence.md in the isolated review workspace; inspect it before emitting any failed-check or Strix finding. - Current-head bounded evidence excerpt, inlined to prevent false no-change or no-coverage approvals when tool/file reads are skipped: - $(cat "$OPENCODE_REVIEW_WORKDIR/bounded-review-evidence-excerpt.md" 2>/dev/null || true) Do not request rollback of Node 24 or Python 3.14 solely from model memory. If all current-head GitHub Checks for those runtime changes passed, version support is not a blocker unless you cite a concrete current source inconsistency or failed registry/check evidence. Use tools only through the OpenCode runtime. Never return raw tool-call markup, tool-call JSON, or MCP call syntax in the review body; if a tool cannot execute, fall back to local git diff/source inspection and still return the final control block. Do not spend the session listing every changed path before reviewing; inspect the highest-risk evidence first. When a claim can be tested, create temporary proof or repro code only under the runner temporary directory or another ignored scratch path, execute it, and cite the command and result in PoC/execution; do not commit or request committing scratch PoC files. Always return a final control block instead of a progress summary. @@ -1182,8 +1202,6 @@ jobs: If bounded failed GitHub Check evidence contains active failed checks, treat it as a blocker until diagnosed. If every active failed-check block says the job was not started because the GitHub account is locked due to a billing issue, classify it as an external CI/account blocker with no repository source fix; do not invent source-backed REQUEST_CHANGES findings for it. If the evidence says no completed failed GitHub Checks were present, do not request changes solely from that section. A successful same-head manual workflow_dispatch Strix run may supersede a stale failed PR statusCheckRollup Strix context only when failed-check evidence explicitly lists it under Superseded failed checks with the exact target URL; otherwise treat failed rollup contexts as blockers. For Strix or other GitHub Checks, use the failed log excerpt and annotations to identify the exact local file line that must change, then provide a concrete from/to fix and suggested diff. When Strix evidence contains multiple model vulnerability reports, include every model-reported vulnerability as a separate evidence-backed finding, preserving each report's model name, title, severity, endpoint, and Code Locations/path:line evidence when present. When evidence supports it, name the concrete CWE/KISA-style class such as injection, auth/authz, secrets, crypto, path traversal/file upload, XSS/CSRF/SSRF, error disclosure, or debug/deployment config; do not invent a category without evidence. One Strix model vulnerability report requires one distinct finding; do not combine duplicate titles or matching locations from different models into one finding. Do not request changes with only a check URL, workflow name, or generic failure summary. If direct file reads fail but focused changed hunks are present in the bounded evidence, review those hunks and do not return file-inaccessible findings for those paths. Full failed-check evidence, when collected, is available as failed-check-evidence.md in the isolated review workspace; inspect it before emitting any failed-check or Strix finding. - Current-head bounded evidence excerpt, inlined to prevent false no-change or no-coverage approvals when tool/file reads are skipped: - $(cat "$OPENCODE_REVIEW_WORKDIR/bounded-review-evidence-excerpt.md" 2>/dev/null || true) Do not request rollback of Node 24 or Python 3.14 solely from model memory. If all current-head GitHub Checks for those runtime changes passed, version support is not a blocker unless you cite a concrete current source inconsistency or failed registry/check evidence. Use tools only through the OpenCode runtime. Never return raw tool-call markup, tool-call JSON, or MCP call syntax in the review body; if a tool cannot execute, fall back to local git diff/source inspection and still return the final control block. Do not spend the session listing every changed path before reviewing; inspect the highest-risk evidence first. When a claim can be tested, create temporary proof or repro code only under the runner temporary directory or another ignored scratch path, execute it, and cite the command and result in PoC/execution; do not commit or request committing scratch PoC files. Always return a final control block instead of a progress summary. @@ -1315,8 +1333,6 @@ jobs: If bounded failed GitHub Check evidence contains active failed checks, treat it as a blocker until diagnosed. If every active failed-check block says the job was not started because the GitHub account is locked due to a billing issue, classify it as an external CI/account blocker with no repository source fix; do not invent source-backed REQUEST_CHANGES findings for it. If the evidence says no completed failed GitHub Checks were present, do not request changes solely from that section. A successful same-head manual workflow_dispatch Strix run may supersede a stale failed PR statusCheckRollup Strix context only when failed-check evidence explicitly lists it under Superseded failed checks with the exact target URL; otherwise treat failed rollup contexts as blockers. For Strix or other GitHub Checks, use the failed log excerpt and annotations to identify the exact local file line that must change, then provide a concrete from/to fix and suggested diff. When Strix evidence contains multiple model vulnerability reports, include every model-reported vulnerability as a separate evidence-backed finding, preserving each report's model name, title, severity, endpoint, and Code Locations/path:line evidence when present. When evidence supports it, name the concrete CWE/KISA-style class such as injection, auth/authz, secrets, crypto, path traversal/file upload, XSS/CSRF/SSRF, error disclosure, or debug/deployment config; do not invent a category without evidence. One Strix model vulnerability report requires one distinct finding; do not combine duplicate titles or matching locations from different models into one finding. Do not request changes with only a check URL, workflow name, or generic failure summary. If direct file reads fail but focused changed hunks are present in the bounded evidence, review those hunks and do not return file-inaccessible findings for those paths. Full failed-check evidence, when collected, is available as failed-check-evidence.md in the isolated review workspace; inspect it before emitting any failed-check or Strix finding. - Current-head bounded evidence excerpt, inlined to prevent false no-change or no-coverage approvals when tool/file reads are skipped: - $(cat "$OPENCODE_REVIEW_WORKDIR/bounded-review-evidence-excerpt.md" 2>/dev/null || true) Do not request rollback of Node 24 or Python 3.14 solely from model memory. If all current-head GitHub Checks for those runtime changes passed, version support is not a blocker unless you cite a concrete current source inconsistency or failed registry/check evidence. Use tools only through the OpenCode runtime. Never return raw tool-call markup, tool-call JSON, or MCP call syntax in the review body; if a tool cannot execute, fall back to local git diff/source inspection and still return the final control block. Do not spend the session listing every changed path before reviewing; inspect the highest-risk evidence first. When a claim can be tested, create temporary proof or repro code only under the runner temporary directory or another ignored scratch path, execute it, and cite the command and result in PoC/execution; do not commit or request committing scratch PoC files. Always return a final control block instead of a progress summary. @@ -1469,8 +1485,6 @@ jobs: GPT-5, DeepSeek R1, and DeepSeek V3 did not produce a usable review. Review PR #${PR_NUMBER} in ${OPENCODE_SOURCE_WORKDIR} with ${model_candidate}. The trusted workflow checkout is ${GITHUB_WORKSPACE}; inspect the pull request head source only from ${OPENCODE_SOURCE_WORKDIR}. CodeGraph MCP is mandatory for structural checks. Also use DeepWiki for repo docs, Context7 for current library/API docs, and web_search for bounded external lookups such as user-claimed concepts, industry standards, international standards, official platform specifications, and comparable issue or PR precedents when applicable. Do not rely on model memory for concepts, standards, runtime support, or domain terminology when a search source is available. Read ./bounded-review-evidence.md first, follow its Review language evidence for the final review language, then inspect changed files and focused hunks under the PR head worktree. Cover security/privacy boundaries, tenant isolation, workflow contracts, user-facing behavior, tests, cross-file compatibility, repository conventions, deployment evidence, git history, breaking-change/backcompat impact, and regression risk. For schema, migration, database, API, workflow, security, or compliance changes, compare against nearby implementation, code conventions, reserved words, naming rules, applicable standards, and production deployment evidence before approving. - Current-head bounded evidence excerpt, inlined to prevent false no-change or no-coverage approvals when tool/file reads are skipped: - $(cat "$OPENCODE_REVIEW_WORKDIR/bounded-review-evidence-excerpt.md" 2>/dev/null || true) Before APPROVE, the summary must name at least one exact changed file path and include a Verification posture section with these exact labels: Linter/static:, TDD/regression:, Coverage:, Docstring coverage:, DAG:, PoC/execution:, DDD/domain:, CDD/context:, Similar issues:, Claim/concept check:, Standards search:, Compatibility/convention:, Breaking-change/backcompat:, Performance:, Design/UX:, Security/privacy:. The CDD/context label must explicitly mention CodeGraph or structural MCP evidence. Coverage and Docstring coverage labels must cite Coverage execution evidence proving 100%; missing, partial, skipped, unavailable, or not-applicable measurement is a blocker, not an approval condition. DAG: must name the rendered Change Flow DAG or an equivalent Mermaid DAG that maps changed files to affected execution path, main risk, and verification path. PoC/execution: must cite the scratch proof, repro, focused test, lint, security, performance, or UI verification command that was actually run and its result; if no meaningful PoC can be run, state the exact repository limitation and request changes when the claim cannot otherwise be proven. If a surface is not applicable or unavailable, say why using that label. First line exactly: @@ -1847,6 +1861,7 @@ jobs: - name: Approve PR if OpenCode review passed if: always() + timeout-minutes: 45 env: GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || secrets.GITHUB_TOKEN }} GH_REPOSITORY: ${{ github.repository }} @@ -1877,7 +1892,7 @@ jobs: OPENCODE_O_SERIES_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-o-series-fallback.md PR_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.inputs.pr_base_sha }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} - APPROVAL_CHECK_WAIT_ATTEMPTS: "241" + APPROVAL_CHECK_WAIT_ATTEMPTS: "81" APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30" CHECK_LOOKUP_RETRY_ATTEMPTS: "5" CHECK_LOOKUP_RETRY_SLEEP_SECONDS: "5" @@ -3030,10 +3045,25 @@ jobs: local input_file="$1" local output_file="$2" local manual_strix_success_target + local manual_strix_success_run_id + local failed_strix_run_id manual_strix_success_target="$(current_head_manual_strix_success_status || true)" if [ -n "$manual_strix_success_target" ]; then - awk '$0 !~ /^- (Strix Security Scan\/strix|strix):/' "$input_file" >"$output_file" + manual_strix_success_run_id="$(printf '%s' "$manual_strix_success_target" | sed -n 's#.*/actions/runs/\([0-9][0-9]*\).*#\1#p')" + while IFS= read -r rollup_line; do + case "$rollup_line" in + "- Strix Security Scan/"*|"- strix:"*) + failed_strix_run_id="$(printf '%s' "$rollup_line" | sed -n 's#.*/actions/runs/\([0-9][0-9]*\).*#\1#p')" + if [ -z "$failed_strix_run_id" ] || + [ -z "$manual_strix_success_run_id" ] || + [ "$failed_strix_run_id" -lt "$manual_strix_success_run_id" ]; then + continue + fi + ;; + esac + printf '%s\n' "$rollup_line" + done <"$input_file" >"$output_file" else cat "$input_file" >"$output_file" fi diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index b76c81f28..508ee6d3b 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -135,8 +135,12 @@ jobs: exit 1 - name: Self-test Strix gate script + timeout-minutes: 10 working-directory: ${{ runner.temp }}/trusted-workspace - run: bash "$TRUSTED_STRIX_GATE_TEST" + run: | + set -euo pipefail + printf 'Running Strix gate self-test with a 10-minute step timeout.\n' + bash "$TRUSTED_STRIX_GATE_TEST" - name: Gate Strix secrets id: gate @@ -341,6 +345,7 @@ jobs: - name: Run Strix (quick) if: steps.gate.outputs.enabled == 'true' + timeout-minutes: 30 # Security invariant for pull_request_target: execute only from the # trusted base checkout. The gate copies PR-head blobs into an isolated # temporary scope with execute bits stripped, then scans that scope as @@ -384,11 +389,11 @@ jobs: IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && 'true' || 'false' }} run: | budget_suffix="TIME""OUT" - process_budget_seconds="3600" + process_budget_seconds="1500" export "LLM_${budget_suffix}=120" export "STRIX_MEMORY_COMPRESSOR_${budget_suffix}=10" export "STRIX_PROCESS_${budget_suffix}_SECONDS=$process_budget_seconds" - export "STRIX_TOTAL_${budget_suffix}_SECONDS=7200" + export "STRIX_TOTAL_${budget_suffix}_SECONDS=1800" bash "$TRUSTED_STRIX_GATE" - name: Collect Strix reports for artifact upload diff --git a/PR_GOVERNANCE_AUDIT.md b/PR_GOVERNANCE_AUDIT.md index 28582e47f..151f2a0ec 100644 --- a/PR_GOVERNANCE_AUDIT.md +++ b/PR_GOVERNANCE_AUDIT.md @@ -13,15 +13,16 @@ OpenCode decides; GitHub Actions mutates. - Prefer `gh pr merge --auto --merge --match-head-commit ` when native auto-merge is enabled. - Use direct `gh pr merge --merge --match-head-commit ` only when the repo policy already allows immediate merge. - OpenCode app-token merges are deprecated; keep app tokens for review publication, not mechanical branch mutation. +- OpenCode approval publication must be bounded. Peer GitHub Checks can be awaited, but the approval step itself must time out instead of running for hours; the current central limit is a 45 minute approval step with 81 peer-check probes at 30 seconds. - Tool failures are not source findings. Model failure, API transient, update-branch `422/403`, fork/write-permission failure, conflict, failed checks, and stale review state must be reported as distinct scheduler outcomes. ## Live Repository Inventory -Live generated: 2026-06-23 04:18 KST. PR #28 rechecked: 2026-06-23 11:43 KST. +Live generated: 2026-06-23 04:18 KST. PR #28 post-merge refresh: 2026-06-23 16:05 KST. | Repo | Flow | Default | Auto | Rulesets | Required checks | Stale dismissal | Merge queue | Workflows | Recent merged actor | |---|---:|---:|---:|---|---|---:|---:|---|---| -| `ContextualWisdomLab/.github` | GitHub Flow | `main` | on | `Lock default branch` | none | true | no | OpenCode Review; PR Review Merge Scheduler; Strix Security Scan | #18 `seonghobae`; #17 `seonghobae`; #2 `seonghobae` | +| `ContextualWisdomLab/.github` | GitHub Flow | `main` | on | `Lock default branch` | none | true | no | OpenCode Review; PR Review Merge Scheduler; Strix Security Scan | #28 `seonghobae` merge `a025be1`; #18 `seonghobae`; #17 `seonghobae` | | `ContextualWisdomLab/bandscope` | Git Flow | `develop` | on | `Lock default branch` | `ci / build-and-test`, `dependency-review`, `security-audit`, `CodeQL`, `sbom`, `release-preflight`, `gate / build / windows`, `gate / build / macos`, `trivy-fs-scan` | false | no | OpenCode Review; PR Review Merge Scheduler; Strix Security Scan | #427 `github-actions`; #408 `seonghobae`; #405 `seonghobae` | | `ContextualWisdomLab/clearfolio` | GitHub Flow | `main` | off | `PR` | none | false | no | OpenCode Review; Strix Security Scan | #9 `seonghobae`; #8 `seonghobae`; #7 `seonghobae` | | `ContextualWisdomLab/codec-carver` | GitHub Flow | `main` | on | `Lock default branch` | none | true | no | OpenCode Review; Scheduled PR Review Merge; Strix Security Scan | #94 `opencode-agent`; #93 `seonghobae`; #90 `seonghobae` | @@ -37,7 +38,7 @@ Live generated: 2026-06-23 04:18 KST. PR #28 rechecked: 2026-06-23 11:43 KST. | Repo | Gap | |---|---| -| `.github` | PR #28 head `60c821e` is blocked by current-head OpenCode `CHANGES_REQUESTED` and `strix` status failure. Same-head manual Strix run `27996904501` passed self-test but failed `Run Strix (quick)`, so it is not merge evidence. | +| `.github` | PR #28 is merged at `a025be1` after same-head manual Strix run `28007326148`, same-head OpenCode run `28008174977`, unresolved review threads `0`, and guarded merge against head `811446d`. Remaining open PRs #19-#27 and #29-#36 are still blocked by `CHANGES_REQUESTED` and/or `DIRTY`; the scheduler dry run inspected 17 PRs and made no mutation. | | `bandscope` | Required checks are repo-specific and broad; keep GitHub native auto-merge as the check interpreter. | | `clearfolio` | Auto-merge is off and the PR Review Merge Scheduler is missing. | | `codec-carver` | Latest merged sample #94 still used `opencode-agent`; replace the legacy scheduler with the central GitHub Actions path. | @@ -53,7 +54,7 @@ Live generated: 2026-06-23 04:18 KST. PR #28 rechecked: 2026-06-23 11:43 KST. | Repo | Live evidence | Adopt | Reject | |---|---|---|---| | `naruon` | `develop`, strict required checks `opencode-review` and `strix`, stale review dismissal enabled. Open PRs show `BEHIND`, `DIRTY`, and `CHANGES_REQUESTED` cases. | Strict current-head evidence and stale-dismissal awareness. | Treating `BEHIND` as merge-ready. | -| `.github` | PR #28 is `MERGEABLE` but `BLOCKED`; required PR-target Strix failed on trusted-base self-test, and same-head manual Strix run `27996904501` also failed at `Run Strix (quick)` after publishing `strix` status failure. The latest OpenCode review still cited the stale PR-target Strix URL instead of the same-head manual Strix failure. | Same-head manual evidence for self-modifying trusted workflow changes, plus explicit handling for failed manual evidence. | Treating stale PR-target failure logs as the only current-head diagnosis after a same-head manual Strix rerun exists. | +| `.github` | PR #28 head `811446d` reached current-head approval after manual Strix run `28007326148` published a successful `strix` status and manual OpenCode run `28008174977` approved the same head; it was merged by `seonghobae` with merge commit `a025be1`. The earlier PR-target Strix failure remains useful only as the reason same-head manual evidence was required. | Same-head manual evidence for self-modifying trusted workflow changes, current-head OpenCode approval, unresolved thread check, and `--match-head-commit` guarded merge. | Treating stale PR-target failure logs as merge blockers after newer same-head evidence exists. | | `pg-erd-cloud` | Recent PRs #236, #237, #239 were merged by `app/github-actions`. | GitHub Actions as mechanical merge actor with head guard. | Human-only queue draining. | | `codec-carver` | Recent PR #94 was merged by `app/opencode-agent`, and the repo still has legacy `Scheduled PR Review Merge`. | Native auto-merge path for current-head approved PRs. | OpenCode app as merge actor. | | `VibeSec` | PR #108 had native auto-merge enabled; #106 merged by `app/github-actions`; #109 merged by human. | Keep native auto-merge as preferred waiting path. | Repo-by-repo actor inconsistency. | @@ -69,7 +70,9 @@ The checked-in scheduler already does the minimal central path: - blocks current-head failed check runs or status contexts before enabling auto-merge; - updates `BEHIND` only when OpenCode approved the exact current head, using `expected_head_sha`; - enables native auto-merge only for current-head OpenCode approval; -- dispatches OpenCode when the current head has no OpenCode decision. +- dispatches same-head Strix evidence first when the current head has no completed Strix evidence; +- waits while same-head Strix evidence is still running, so OpenCode is not started just to poll a peer check; +- dispatches OpenCode only after same-head Strix evidence is complete, including failed Strix evidence that OpenCode must explain from logs. Small proof run: @@ -78,16 +81,15 @@ $ python3 scripts/ci/pr_review_merge_scheduler.py --self-test self-test passed $ python3 scripts/ci/pr_review_merge_scheduler.py --repo ContextualWisdomLab/.github --base-branch main --project-flow github-flow --dry-run --max-prs 40 --no-trigger-reviews -PR #19: block: current-head OpenCode review requested changes -PR #20: block: current-head OpenCode review requested changes +PR #19: block: merge conflict: DIRTY +PR #20: block: merge conflict: DIRTY PR #21: block: current-head OpenCode review requested changes -PR #22: block: current-head OpenCode review requested changes +PR #22: block: merge conflict: DIRTY PR #23: block: merge conflict: DIRTY PR #24: block: current-head OpenCode review requested changes PR #25: block: current-head OpenCode review requested changes PR #26: block: current-head OpenCode review requested changes PR #27: block: current-head OpenCode review requested changes -PR #28: block: current-head OpenCode review requested changes PR #29: block: current-head OpenCode review requested changes PR #30: block: current-head OpenCode review requested changes PR #31: block: current-head OpenCode review requested changes @@ -95,8 +97,8 @@ PR #32: block: current-head OpenCode review requested changes PR #33: block: current-head OpenCode review requested changes PR #34: block: current-head OpenCode review requested changes PR #35: block: current-head OpenCode review requested changes -PR #36: block: current-head OpenCode review requested changes -{"base_branch": "main", "counts": {"block": 18}, "dry_run": true, "inspected": 18, "project_flow": "github-flow"} +PR #36: block: merge conflict: DIRTY +{"base_branch": "main", "counts": {"block": 17}, "dry_run": true, "inspected": 17, "project_flow": "github-flow"} ``` ## Rollout List @@ -109,7 +111,20 @@ PR #36: block: current-head OpenCode review requested changes ## Remaining Proof Gaps +- A live current-head review -> same-head manual Strix status bridge -> OpenCode approval -> guarded merge trace has been completed on `.github` PR #28. - No live outdated -> update-branch -> new-head review -> merge/auto-merge trace has been completed yet. - `update-branch` `422/403` behavior still needs a safe fixture or a real blocked case before claiming standardized handling. - Required-check interpretation should stay delegated to GitHub native auto-merge until a repo needs immediate merge. -- PR #28 itself cannot prove adoption until the same-head manual Strix failure is diagnosed and OpenCode stops reusing stale PR-target Strix self-test logs as the only failed-check evidence. +- PR #28 proves the self-modifying trusted workflow bootstrap path after newer same-head evidence exists, but it does not prove update-branch behavior, stale approval dismissal after a head change, or cross-repository rollout. +- PR #37 adds a bounded OpenCode approval publication timeout after manual current-head OpenCode run `28011338113` reached the approval step and was observed waiting on peer checks instead of finishing promptly. +- PR #37 current-head run `28012303665` proved 100% coverage/docstring evidence and OpenCode completion for head `184be63`; Strix run `28012303876` proved same-head manual Strix success. The run also exposed that cancelled PR-target helper check `Strix Security Scan/publish-manual-pr-evidence-status` must be superseded by the newer same-head manual `strix` status, not treated as a source finding. +- PR #37 head `9bbf641` exposed a remaining race: OpenCode can finish before same-head manual Strix publishes the superseding `strix` status, causing stale cancelled PR-target Strix checks to become REQUEST_CHANGES. The evidence preparation step now waits, within a 40 minute bound, whenever peer checks are still running, even if completed failed check evidence is already visible. +- The same race also showed that PR `statusCheckRollup` does not see a manual Strix `workflow_dispatch` run until it publishes a commit status. OpenCode evidence preparation now queries current-head `strix.yml` workflow runs directly and treats in-progress same-head Strix runs as peer checks. +- Strix run `28014156427` also reported sensitive log disclosure risk in failed-check evidence handling. The collector now redacts common token, API key, password, secret, authorization, Slack token, and AWS access-key patterns before any failed logs are summarized or embedded in review evidence. +- Strix run `28015621232` reported `GitHub Actions pull_request_target with PR Code Execution` against `.github/workflows/opencode-review.yml`. OpenCode Review is now `workflow_dispatch`-only, and the scheduler dispatches same-head Strix before same-head OpenCode. This follows GitHub's secure-use guidance to avoid `pull_request_target` with untrusted PR checkout/execution: https://docs.github.com/en/actions/reference/security/secure-use and GitHub Security Lab's "Preventing pwn requests": https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/. +- OpenCode run `28017920517` failed without posting a PR review because every model attempt failed to produce a valid control block; the primary `github-models/openai/gpt-5` error was `Request body too large for gpt-5 model. Max size: 4000 tokens.` The prompt now requires reading `bounded-review-evidence.md` instead of inlining `bounded-review-evidence-excerpt.md`. +- PR #37 head `ce5591e` reproduced the self-modifying workflow hazard: the base-branch `pull_request_target` OpenCode run `28019367683` posted `REQUEST_CHANGES` from skipped coverage evidence, while the same-head manual `coverage-evidence` job in run `28019384032` proved 100% test and docstring coverage. The central policy removes `pull_request_target` from OpenCode review and relies on scheduler-dispatched `workflow_dispatch` evidence for PR-head review. +- OpenCode run `28019384032` also showed a model-output repair gap: DeepSeek V3 returned an `APPROVE` control block but wrote `Coverage: Not applicable` and `Docstring coverage: Not applicable` even though bounded current-head evidence proved both at 100%. The normalizer now reads the last concrete verification label after evidence-based repair, so an appended repair summary can replace earlier invalid model labels without accepting missing coverage. +- Strix run `28022323798` caught that the first label repair changed normalizer parsing too narrowly: inline approval summaries in `test_strix_quick_gate.sh` no longer normalized. Label parsing now accepts inline verification labels while excluding the `Coverage:` suffix inside `Docstring coverage:`, preserving both inline transcript controls and appended evidence repair. +- PR #37 same-head manual Strix run `28023392848` succeeded for head `07a6b76`, but the concurrently dispatched same-head manual OpenCode run `28023401894` spent its early lifetime waiting in `Prepare bounded OpenCode review evidence`. That exposed a scheduler-level resource issue: dispatching Strix and OpenCode together can turn OpenCode into a long poller whenever Strix is queued or slow. The scheduler now serializes the process: first dispatch Strix, then wait for a later scheduler pass to dispatch OpenCode after Strix evidence is complete. +- The base-branch automatic OpenCode run `28025023007` still posted a current-head `CHANGES_REQUESTED` review before cancellation on head `1d05f52`, even though that automatic trigger is removed by this PR. The scheduler previously treated any current-head OpenCode `CHANGES_REQUESTED` as permanent. It now reads the latest OpenCode review on the current head, so a later same-head OpenCode approval can supersede an earlier false negative from the same reviewer. diff --git a/README.md b/README.md index 82e982168..8cc9d343b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ Branch updates and merges run through the workflow `GITHUB_TOKEN`, so GitHub records those mechanical mutations as `github-actions[bot]` rather than an OpenCode app token or a personal token. +OpenCode review execution is `workflow_dispatch`-only. The scheduler dispatches +same-head Strix evidence first, then dispatches OpenCode for the same PR head. +This avoids running PR-head review, CodeGraph, coverage, or PoC code from a +privileged `pull_request_target` OpenCode workflow. + OpenCode approval is evidence-gated. Before approval, the review summary must name changed files, CodeGraph or structural MCP evidence, a Change Flow DAG, 100% test coverage evidence, 100% docstring coverage evidence, and a concrete diff --git a/scripts/ci/collect_failed_check_evidence.sh b/scripts/ci/collect_failed_check_evidence.sh index 76c8a0968..5fba5b3bd 100755 --- a/scripts/ci/collect_failed_check_evidence.sh +++ b/scripts/ci/collect_failed_check_evidence.sh @@ -17,6 +17,17 @@ strip_ansi() { perl -pe 's/\x1b\[[0-9;?]*[A-Za-z]//g' } +redact_sensitive_log() { + perl -pe ' + s/\b(gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})/[REDACTED_GITHUB_TOKEN]/g; + s/\b(sk-[A-Za-z0-9_-]{20,})/[REDACTED_API_KEY]/g; + s/\b(xox[baprs]-[A-Za-z0-9-]{20,})/[REDACTED_SLACK_TOKEN]/g; + s/\b(AKIA[0-9A-Z]{16})/[REDACTED_AWS_ACCESS_KEY]/g; + s/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|password|passwd|secret)\s*[:=]\s*)["'\'']?[^"'\''\s]+["'\'']?/${1}[REDACTED]/ig; + s/((?:authorization|proxy-authorization)\s*:\s*(?:bearer|basic)\s+)[A-Za-z0-9._~+\/=-]+/${1}[REDACTED]/ig; + ' +} + emit_bounded_file() { local file_path="$1" local max_lines="$2" @@ -201,20 +212,38 @@ trap cleanup EXIT manual_success_for_label() { local label="$1" + local failed_run_id="${2:-}" local key + local lower_label + local success_context + local success_url + local success_description + local success_run_id key="${label##*/}" key="$(printf '%s' "$key" | tr '[:upper:]' '[:lower:]')" - awk -F '\t' -v key="$key" ' - tolower($1) == key { - print - found = 1 - exit - } - END { - exit found ? 0 : 1 - } - ' "$manual_success_contexts" + lower_label="$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]')" + case "$lower_label" in + "strix security scan/"*) + key="strix" + ;; + esac + + while IFS=$'\t' read -r success_context success_url success_description; do + if [ "$(printf '%s' "$success_context" | tr '[:upper:]' '[:lower:]')" != "$key" ]; then + continue + fi + success_run_id="$(printf '%s' "$success_url" | sed -n 's#.*/actions/runs/\([0-9][0-9]*\).*#\1#p')" + if [ -n "$failed_run_id" ] && + [ -n "$success_run_id" ] && + [ "$failed_run_id" -ge "$success_run_id" ]; then + continue + fi + printf '%s\t%s\t%s\n' "$success_context" "$success_url" "$success_description" + return 0 + done <"$manual_success_contexts" + + return 1 } # shellcheck disable=SC2016 @@ -350,7 +379,7 @@ while IFS=$'\t' read -r kind label conclusion details_url run_id check_run_id; d done <"$workflow_run_contexts" while IFS=$'\t' read -r kind label conclusion details_url run_id check_run_id; do - if success_line="$(manual_success_for_label "$label")"; then + if success_line="$(manual_success_for_label "$label" "$run_id")"; then IFS=$'\t' read -r success_context success_url success_description <<<"$success_line" printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ "$kind" \ @@ -424,7 +453,7 @@ done <"$failed_contexts" stripped_log_file="$(mktemp)" tmp_files+=("$log_file" "$stripped_log_file") if gh run view "$run_id" --repo "$GH_REPOSITORY" --log-failed >"$log_file" 2>&1; then - strip_ansi <"$log_file" >"$stripped_log_file" + strip_ansi <"$log_file" | redact_sensitive_log >"$stripped_log_file" if [ -s "$stripped_log_file" ]; then emit_failure_signal_summary "$stripped_log_file" || true printf '### Failed workflow run log excerpt\n\n' @@ -441,7 +470,7 @@ done <"$failed_contexts" fi fi else - strip_ansi <"$log_file" >"$stripped_log_file" + strip_ansi <"$log_file" | redact_sensitive_log >"$stripped_log_file" printf 'No GitHub Actions job log is available for this failed workflow run.\n\n' printf '```text\n' emit_bounded_file "$stripped_log_file" 60 @@ -493,7 +522,7 @@ done <"$failed_contexts" --repo "$GH_REPOSITORY" \ --job "$check_run_id" \ --log-failed >"$log_raw" 2>&1; then - strip_ansi <"$log_raw" >"$log_clean" + strip_ansi <"$log_raw" | redact_sensitive_log >"$log_clean" if [ -s "$log_clean" ]; then emit_failure_signal_summary "$log_clean" || true if emit_strix_vulnerability_evidence "$log_clean"; then @@ -509,7 +538,7 @@ done <"$failed_contexts" printf 'The failed job log could not be collected with `gh run view --log-failed`.\n\n' if [ -s "$log_raw" ]; then printf '```text\n' - strip_ansi <"$log_raw" | sed -n '1,40p' + strip_ansi <"$log_raw" | redact_sensitive_log | sed -n '1,40p' printf '\n```\n\n' fi fi diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index e467862c9..a624030c7 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -145,14 +145,25 @@ def mentions_verification_posture(reason: str, summary: str) -> bool: def label_section(text: str, label: str) -> str: """Return text after a verification label until the next known label.""" - start = text.find(label) - if start == -1: + def label_matches(candidate: str) -> list[re.Match[str]]: + """Return exact verification-label matches without suffix collisions.""" + matches = [] + for match in re.finditer(re.escape(candidate), text): + if candidate == "coverage:" and text[max(0, match.start() - 10) : match.start()] == "docstring ": + continue + matches.append(match) + return matches + + matches = label_matches(label) + if not matches: return "" - start += len(label) + start = matches[-1].end() next_starts = [ - text.find(candidate, start) + match.start() for candidate in APPROVAL_VERIFICATION_LABELS - if candidate != label and text.find(candidate, start) != -1 + if candidate != label + for match in label_matches(candidate) + if match.start() >= start ] end = min(next_starts) if next_starts else len(text) return text[start:end] diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index e5628ae06..609fd51c8 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -152,6 +152,20 @@ def is_opencode_context(node: dict[str, Any]) -> bool: return node.get("context") == "opencode-review" +def is_strix_context(node: dict[str, Any]) -> bool: + """Return whether a check or status context belongs to Strix evidence.""" + if node.get("__typename") == "CheckRun": + workflow = ( + ((node.get("checkSuite") or {}).get("workflowRun") or {}).get("workflow") + or {} + ) + workflow_name = workflow.get("name") + return workflow_name in {"Strix Security Scan", "Strix"} or ( + node.get("name") == "strix" and workflow_name is None + ) + return (node.get("context") or "") in {"strix", "Strix Security Scan"} + + def opencode_in_progress(pr: dict[str, Any]) -> bool: """Return whether any OpenCode review status for the PR is still running.""" for node in context_nodes(pr): @@ -163,6 +177,21 @@ def opencode_in_progress(pr: dict[str, Any]) -> bool: return False +def strix_evidence_state(pr: dict[str, Any]) -> str: + """Return missing, running, or complete for current-head Strix evidence.""" + found = False + for node in context_nodes(pr): + if not is_strix_context(node): + continue + found = True + status = (node.get("status") or node.get("state") or "").upper() + if status in {"PENDING", "EXPECTED", "QUEUED", "IN_PROGRESS", "WAITING", "REQUESTED"}: + return "running" + if node.get("__typename") == "CheckRun" and status != "COMPLETED": + return "running" + return "complete" if found else "missing" + + def unresolved_thread_count(pr: dict[str, Any]) -> int: """Count active, non-outdated unresolved review threads on a PR.""" threads = ((pr.get("reviewThreads") or {}).get("nodes") or []) @@ -180,16 +209,15 @@ def is_opencode_review(review: dict[str, Any]) -> bool: def current_head_review_state(pr: dict[str, Any], state: str) -> bool: - """Return whether OpenCode left a target review state on the current head.""" + """Return whether OpenCode's latest current-head review has the target state.""" head = pr.get("headRefOid") for review in reversed((pr.get("reviews") or {}).get("nodes") or []): if not is_opencode_review(review): continue - if (review.get("state") or "").upper() != state: - continue commit = (review.get("commit") or {}).get("oid") - if commit == head: - return True + if commit != head: + continue + return (review.get("state") or "").upper() == state return False @@ -288,6 +316,30 @@ def dispatch_opencode_review(repo: str, workflow: str, pr: dict[str, Any], *, dr ) +def dispatch_strix_evidence(repo: str, workflow: str, pr: dict[str, Any], *, dry_run: bool) -> None: + """Dispatch same-head Strix workflow evidence before OpenCode reviews.""" + if dry_run: + return + run( + [ + "gh", + "workflow", + "run", + workflow, + "--repo", + repo, + "--ref", + pr["baseRefName"], + "-f", + f"pr_number={pr['number']}", + "-f", + f"pr_base_sha={pr['baseRefOid']}", + "-f", + f"pr_head_sha={pr['headRefOid']}", + ] + ) + + def inspect_pr( repo: str, pr: dict[str, Any], @@ -297,6 +349,7 @@ def inspect_pr( enable_auto_merge_flag: bool, update_branches: bool, workflow: str, + security_workflow: str, base_branch: str, ) -> Decision: """Decide and optionally act on one pull request's merge-readiness state.""" @@ -343,8 +396,22 @@ def inspect_pr( return Decision(number, "wait", "OpenCode review is already in progress") if trigger_reviews: + strix_state = strix_evidence_state(pr) + if strix_state == "missing": + dispatch_strix_evidence(repo, security_workflow, pr, dry_run=dry_run) + return Decision( + number, + "security_dispatch", + "current head has no completed Strix evidence; same-head Strix dispatched", + ) + if strix_state == "running": + return Decision(number, "wait", "same-head Strix evidence is still running") dispatch_opencode_review(repo, workflow, pr, dry_run=dry_run) - return Decision(number, "review_dispatch", "current head has no OpenCode approval") + return Decision( + number, + "review_dispatch", + "current head has completed Strix evidence; same-head OpenCode dispatched", + ) return Decision(number, "block", "current head has no OpenCode approval") @@ -410,6 +477,7 @@ def self_test() -> None: enable_auto_merge_flag=True, update_branches=True, workflow="OpenCode Review", + security_workflow="Strix Security Scan", base_branch="main", ) assert decision.action == "auto_merge" @@ -424,6 +492,7 @@ def self_test() -> None: enable_auto_merge_flag=True, update_branches=True, workflow="OpenCode Review", + security_workflow="Strix Security Scan", base_branch="main", ) assert decision.action == "block" @@ -469,6 +538,28 @@ def self_test() -> None: enable_auto_merge_flag=True, update_branches=True, workflow="OpenCode Review", + security_workflow="Strix Security Scan", + base_branch="main", + ) + assert decision.action == "security_dispatch" + sample["statusCheckRollup"]["contexts"]["nodes"] = [ + { + "__typename": "CheckRun", + "name": "strix", + "status": "COMPLETED", + "conclusion": "SUCCESS", + "checkSuite": {"workflowRun": {"workflow": {"name": "Strix Security Scan"}}}, + } + ] + decision = inspect_pr( + "owner/repo", + sample, + dry_run=True, + trigger_reviews=True, + enable_auto_merge_flag=True, + update_branches=True, + workflow="OpenCode Review", + security_workflow="Strix Security Scan", base_branch="main", ) assert decision.action == "review_dispatch" @@ -481,6 +572,7 @@ def self_test() -> None: enable_auto_merge_flag=True, update_branches=True, workflow="OpenCode Review", + security_workflow="Strix Security Scan", base_branch="main", ) assert decision.action == "update_branch" @@ -499,6 +591,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--enable-auto-merge", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--update-branches", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--review-workflow", default="OpenCode Review") + parser.add_argument("--security-workflow", default="Strix Security Scan") parser.add_argument("--self-test", action="store_true") return parser.parse_args(argv) @@ -525,6 +618,7 @@ def main(argv: list[str]) -> int: enable_auto_merge_flag=args.enable_auto_merge, update_branches=args.update_branches, workflow=args.review_workflow, + security_workflow=args.security_workflow, base_branch=args.base_branch, ) for pr in prs diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b517b1b48..8ea3cb11d 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -156,8 +156,8 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "VERTEXAI_LOCATION" "strix workflow exports LiteLLM Vertex location env" assert_file_contains "$workflow_file" "timeout-minutes: 120" "strix workflow job budget covers PR-scoped Strix scans" assert_file_contains "$workflow_file" 'budget_suffix="TIME""OUT"' "strix workflow builds budget env keys without visible timeout signal text" - assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=7200"' "strix workflow total Strix budget covers PR-scoped scans" - assert_file_contains "$workflow_file" 'process_budget_seconds="3600"' "strix workflow keeps PR-scoped process budget large enough for report finalization" + assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=1800"' "strix workflow caps total Strix budget for PR-scoped quick scans" + assert_file_contains "$workflow_file" 'process_budget_seconds="1500"' "strix workflow keeps process budget within the PR quick-scan step timeout" assert_file_contains "$workflow_file" 'IS_PR_EVIDENCE_RUN: ${{ (github.event_name == '"'"'pull_request_target'"'"' || github.event.inputs.pr_number != '"'"''"'"') && '"'"'true'"'"' || '"'"'false'"'"' }}' "strix workflow passes PR evidence mode through env" assert_file_not_contains "$workflow_file" 'if [ "${{ (github.event_name == '"'"'pull_request_target'"'"' || github.event.inputs.pr_number != '"'"''"'"') && '"'"'true'"'"' || '"'"'false'"'"' }}" = "true" ]; then' "strix workflow does not interpolate GitHub context inside shell condition" assert_file_not_contains "$workflow_file" "LLM_TIMEOUT:" "strix workflow must not expose LLM timeout env names in GitHub logs" @@ -339,15 +339,18 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { local workflow_file="$REPO_ROOT/.github/workflows/opencode-review.yml" local opencode_config="$REPO_ROOT/opencode.jsonc" - assert_file_contains "$workflow_file" "pull_request_target:" "opencode review workflow runs on the trusted PR trigger so merge-conflict PRs still get the standard review surface" + if grep -Eq '^[[:space:]]+pull_request_target:[[:space:]]*$' "$workflow_file"; then + record_failure "opencode review workflow must not run PR-head review code from pull_request_target" + fi + assert_file_contains "$workflow_file" "workflow_dispatch:" "opencode review workflow runs only through scheduler or manual current-head dispatch" if grep -Eq '^[[:space:]]+pull_request:[[:space:]]*$' "$workflow_file"; then record_failure "opencode review workflow must not double-run on pull_request and pull_request_target" fi assert_file_not_contains "$workflow_file" "Wait for trusted OpenCode approval review" "opencode pull_request bridge was removed to avoid duplicate required-check resource use" assert_file_not_contains "$workflow_file" "Trusted OpenCode requested changes for head" "opencode pull_request bridge no longer reconsumes stale trusted review state" - assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target'" "opencode review side effects are limited to pull_request_target or manual workflow dispatch" + assert_file_contains "$workflow_file" "if: always() && github.event_name == 'workflow_dispatch'" "opencode review side effects are limited to manual workflow dispatch" assert_file_contains "$workflow_file" "opencode-review-target:" "opencode trusted review job owns the required check surface" - assert_file_contains "$workflow_file" "github.event.pull_request.head.repo.full_name == github.repository" "opencode review workflow limits pull_request_target review execution to same-repository PRs" + assert_file_not_contains "$workflow_file" "github.event.pull_request.head.repo.full_name == github.repository" "opencode review no longer executes same-repository PR heads from pull_request_target" assert_file_contains "$workflow_file" "Initialize CodeGraph index for OpenCode" "opencode review workflow initializes CodeGraph before review" assert_file_contains "$workflow_file" "actions: read" "opencode review workflow can read failed Actions logs for GitHub Check diagnosis" assert_file_contains "$workflow_file" "checks: read" "opencode review workflow can read failed check-run annotations for line-specific findings" @@ -361,11 +364,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "Prepare bounded OpenCode review evidence" "opencode review workflow prepares bounded local evidence instead of oversized GitHub prompt data" assert_file_contains "$workflow_file" "emit_file_prefix" "opencode review prompt evidence is byte-capped before GitHub Models requests" assert_file_contains "$workflow_file" "bounded-review-evidence.md" "opencode review prompt reads bounded evidence from the isolated workspace instead of inlining it" + assert_file_not_contains "$workflow_file" '$(cat "$OPENCODE_REVIEW_WORKDIR/bounded-review-evidence-excerpt.md"' "opencode review prompt must not inline evidence excerpts into small-context models" assert_file_contains "$workflow_file" "Prepare isolated OpenCode review workspace" "opencode review workflow isolates from the large project AGENTS.md" assert_file_contains "$workflow_file" 'cd "$OPENCODE_REVIEW_WORKDIR"' "opencode review runs from the isolated OpenCode workspace" assert_file_contains "$workflow_file" "failed-check-evidence.md" "opencode review copies full failed-check evidence into the isolated workspace" - assert_file_contains "$workflow_file" "Checkout trusted review workflow" "opencode review executes trusted workflow scripts from the base checkout" - assert_file_contains "$workflow_file" "Checkout trusted review workflow for manual PR review" "opencode review checks out explicit base SHA for manual PR review reruns" + assert_file_not_contains "$workflow_file" "Checkout trusted review workflow" "opencode review no longer has a pull_request_target trusted-workflow execution path" + assert_file_contains "$workflow_file" "Checkout current-head review workflow for manual PR review" "opencode review checks out explicit PR head SHA for manual current-head validation" assert_file_contains "$workflow_file" "pr_head_ref:" "opencode workflow_dispatch accepts scheduler-provided PR head branch" assert_file_contains "$workflow_file" 'github.event.inputs.pr_head_ref' "opencode review uses scheduler-provided PR head branch before falling back to PR lookup" assert_file_contains "$workflow_file" 'ref: ${{ github.event.inputs.pr_head_sha }}' "opencode manual review checks out the PR head workflow scripts for same-head gate validation" @@ -489,7 +493,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'load_selected_review_output()' "opencode approval step has a direct selected-output fallback when the overview comment is stale or invalid" assert_file_contains "$workflow_file" "gate result from Review Overview comment" "opencode approval step distinguishes overview-comment gate results" assert_file_contains "$workflow_file" "gate result from selected OpenCode output" "opencode approval step can recover from an invalid overview by validating the selected successful output" - assert_file_contains "$workflow_file" 'APPROVAL_CHECK_WAIT_ATTEMPTS: "241"' "opencode approval waits for long-running peer checks before approving" + assert_file_contains "$workflow_file" 'timeout-minutes: 45' "opencode approval step has a bounded wall-clock timeout" + assert_file_contains "$workflow_file" 'APPROVAL_CHECK_WAIT_ATTEMPTS: "81"' "opencode approval waits for bounded long-running peer checks before approving" assert_file_contains "$workflow_file" 'CHECK_LOOKUP_RETRY_ATTEMPTS: "5"' "opencode approval retries transient GitHub check lookup failures before changing review state" assert_file_contains "$workflow_file" 'GitHub Checks lookup failed; retrying' "opencode approval logs transient check lookup retries" assert_file_contains "$workflow_file" 'collect_github_checks_with_retry collect_pending_github_checks "$output_file"' "opencode approval retry-wraps pending check lookup" @@ -507,7 +512,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" assert_file_contains "$workflow_file" "github.event_name == 'workflow_dispatch'" "manual current-head OpenCode reviews measure coverage instead of approving skipped coverage evidence" assert_file_contains "$workflow_file" "if: github.event_name == 'workflow_dispatch'" "pull_request_target must not execute PR-head coverage scripts" - assert_file_contains "$workflow_file" 'ref: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }}' "manual coverage evidence checks out the requested PR head SHA" + assert_file_contains "$workflow_file" 'ref: ${{ github.event.inputs.pr_head_sha }}' "manual coverage evidence checks out the requested PR head SHA" assert_file_contains "$workflow_file" 'ref: ${{ github.event.inputs.pr_head_sha }}' "manual OpenCode review checks out the PR head gate scripts for same-head validation" assert_file_contains "$workflow_file" 'COVERAGE_EVIDENCE_RESULT: ${{ needs.coverage-evidence.result || '\''skipped'\'' }}' "opencode approval receives the coverage-evidence job conclusion" assert_file_contains "$workflow_file" 'build_coverage_evidence_failure_body()' "opencode approval can publish a coverage-evidence blocker" @@ -523,6 +528,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "PoC/execution:" "opencode approval requires concrete PoC or execution evidence" assert_file_contains "$workflow_file" "create temporary proof or repro code only under the runner temporary directory" "opencode review may create scratch PoC code without committing it" assert_file_contains "$workflow_file" 'current_peer_checks_still_running()' "opencode evidence waits for PR statusCheckRollup peer checks before reviewing" + assert_file_contains "$workflow_file" '--workflow strix.yml' "opencode evidence also waits for current-head manual Strix workflow runs before reviewing" + assert_file_contains "$workflow_file" 'select((.status // "") != "completed")' "opencode evidence treats in-progress current-head Strix workflow runs as peer checks" assert_file_contains "$workflow_file" 'collect_pending_github_checks()' "opencode approval collects pending peer GitHub Checks" assert_file_contains "$workflow_file" 'collect_current_head_strix_workflow_runs()' "opencode approval separately accounts for jobless current-head Strix workflow runs" assert_file_contains "$workflow_file" 'actions/workflows/strix.yml' "opencode approval probes whether Strix is installed before listing Strix runs" @@ -539,6 +546,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'grep -Fq -- "Strix Security Scan/strix:" "$rollup_file"' "opencode approval avoids duplicate supplemental Strix workflow-run blockers when statusCheckRollup already has the Strix check" assert_file_contains "$workflow_file" 'current_head_manual_strix_success_status()' "opencode approval can identify same-head manual Strix success status evidence" assert_file_contains "$workflow_file" 'filter_superseded_strix_failures()' "opencode approval filters only explicitly superseded stale Strix failures" + assert_file_contains "$workflow_file" '"- Strix Security Scan/"*|"- strix:"*' "opencode approval filters stale Strix workflow helper checks after newer manual evidence" assert_file_contains "$workflow_file" 'Manual workflow_dispatch Strix evidence passed' "opencode approval requires an explicit manual Strix evidence status description" assert_file_contains "$workflow_file" 'last // empty' "opencode approval checks the latest strix status before accepting manual success evidence" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'publish-manual-pr-evidence-status:' "strix workflow publishes same-head manual PR evidence as a commit status" @@ -552,6 +560,11 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select((.workflowName // "") == "Strix Security Scan" or (.workflowName // "") == "Strix")' "failed-check evidence only appends Strix workflow runs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'group_by(.__context_key)' "failed-check evidence groups manual Strix statuses by context before accepting superseding success" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'map(last)' "failed-check evidence accepts only the latest status per context" + assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '"strix security scan/"*' "failed-check evidence maps stale Strix workflow helper checks to the manual strix evidence status" + assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '[ "$failed_run_id" -ge "$success_run_id" ]' "failed-check evidence only supersedes Strix helper checks older than the manual success run" + assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'redact_sensitive_log()' "failed-check evidence redacts sensitive values before emitting logs" + assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '[REDACTED_GITHUB_TOKEN]' "failed-check evidence redacts GitHub token patterns" + assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'redact_sensitive_log >"$log_clean"' "failed-check evidence redacts collected job logs before summaries" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'awk -F '"'"'\t'"'"' -v run_id="$run_id"' "failed-check evidence avoids duplicate workflow-run evidence when statusCheckRollup already includes the run" assert_file_not_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '[[ ! "$run_id" =~ ^[0-9]+$ ]]' "failed-check evidence no longer suppresses failed contexts as superseded" assert_file_contains "$workflow_file" 'wait_for_peer_github_checks "$pending_checks_file"' "opencode approval gates approval on pending peer GitHub Checks" @@ -593,7 +606,9 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "collect_failed_check_evidence.sh" "opencode review workflow collects failed check logs and annotations" 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" 'FAILED_CHECK_EVIDENCE_ATTEMPTS: "31"' "opencode review workflow waits long enough for slow Strix self-test failures" + 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: "75"' "opencode review workflow waits long enough for bounded Strix evidence before model review" + 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" assert_file_contains "$workflow_file" "collect_failed_check_evidence_with_wait" "opencode review workflow waits briefly for failed checks before building model evidence" assert_file_contains "$workflow_file" "Failed-check evidence collector is not installed in this repository." "opencode review evidence handles repos without the failed-check helper instead of retrying a missing script" assert_file_contains "$workflow_file" "collect_failed_check_evidence_or_note()" "opencode approval handles repos without the failed-check helper before publishing fallback reviews" @@ -773,6 +788,10 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "pull-requests: write" "scheduler has pull-request write permission for update-branch and auto-merge" assert_file_contains "$scheduler_file" "update-branch" "scheduler calls the GitHub update-branch API for outdated approved PRs" assert_file_contains "$scheduler_file" "expected_head_sha={head}" "scheduler guards branch updates with the current PR head SHA" + assert_file_contains "$scheduler_file" "dispatch_strix_evidence" "scheduler dispatches same-head Strix evidence before OpenCode review" + assert_file_contains "$scheduler_file" "--security-workflow" "scheduler allows the canonical Strix workflow name to be configured" + assert_file_contains "$scheduler_file" "same-head Strix and OpenCode dispatched" "scheduler records review dispatch as a coupled security and review evidence action" + assert_file_contains "$workflow_file" "--review-workflow \"OpenCode Review\"" "scheduler dispatches the canonical OpenCode Review workflow" assert_file_contains "$readme_file" "github-actions[bot]" "README documents that mechanical branch updates and merges are attributed to GitHub Actions bot" assert_file_contains "$readme_file" "Scratch PoC files are not committed." "README documents PoC proof artifacts are scratch evidence, not committed changes" assert_file_contains "$readme_file" "Failed GitHub Checks are not reviewed as URL lists." "README documents failed-check reviews require explanations, not URL-only bullets" diff --git a/tests/test_opencode_review_normalize_output.py b/tests/test_opencode_review_normalize_output.py index 30ed3b116..b85bb8d79 100644 --- a/tests/test_opencode_review_normalize_output.py +++ b/tests/test_opencode_review_normalize_output.py @@ -190,6 +190,65 @@ def test_valid_control_repairs_approval_summary_from_bounded_evidence(tmp_path, assert norm.mentions_full_coverage(repaired["reason"], repaired["summary"]) +def test_valid_control_repair_overrides_earlier_invalid_coverage_labels(tmp_path, monkeypatch): + evidence = tmp_path / "bounded-review-evidence.md" + evidence.write_text( + """\ +# OpenCode bounded PR review evidence + +## Coverage execution evidence + +# Coverage Evidence + +## Coverage Decision + +- Result: PASS +- Test coverage: 100% +- Docstring coverage: 100% + +## Changed files + +M\tscripts/ci/opencode_review_normalize_output.py +M\ttests/test_opencode_review_normalize_output.py +""", + encoding="utf-8", + ) + monkeypatch.setenv("OPENCODE_APPROVAL_REPAIR_EVIDENCE_FILE", str(evidence)) + + repaired = norm.valid_control( + control( + reason="No blockers found in the PR changes.", + summary="""\ +Inspected the PR changes and found no actionable blockers. +Verification posture: CodeGraph was available, but the model summarized too broadly. +Linter/static: Not applicable. +TDD/regression: Not applicable. +Coverage: Not applicable. +Docstring coverage: Not applicable. +DAG: Not applicable. +PoC/execution: Not applicable. +DDD/domain: Not applicable. +CDD/context: Not applicable. +Similar issues: Not applicable. +Claim/concept check: Not applicable. +Standards search: Not applicable. +Compatibility/convention: Not applicable. +Breaking-change/backcompat: Not applicable. +Performance: Not applicable. +Design/UX: Not applicable. +Security/privacy: Not applicable. +""", + ), + expected_head_sha="head", + expected_run_id="run", + expected_run_attempt="attempt", + ) + + assert repaired is not None + assert "scripts/ci/opencode_review_normalize_output.py" in repaired["summary"] + assert norm.mentions_full_coverage(repaired["reason"], repaired["summary"]) + + def test_valid_control_does_not_repair_unsafe_or_unproven_approval(tmp_path, monkeypatch): evidence = tmp_path / "bounded-review-evidence.md" evidence.write_text( diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index a57c2e72c..e89af16c9 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -32,6 +32,16 @@ def opencode_review(state="APPROVED", commit="head", login="opencode-agent"): return {"state": state, "author": {"login": login}, "commit": {"oid": commit}} +def strix_check(status="COMPLETED", conclusion="SUCCESS", workflow="Strix Security Scan"): + return { + "__typename": "CheckRun", + "name": "strix", + "status": status, + "conclusion": conclusion, + "checkSuite": {"workflowRun": {"workflow": {"name": workflow}}}, + } + + def inspect(pr, **overrides): kwargs = { "dry_run": True, @@ -39,6 +49,7 @@ def inspect(pr, **overrides): "enable_auto_merge_flag": True, "update_branches": True, "workflow": "OpenCode Review", + "security_workflow": "Strix Security Scan", "base_branch": "main", } kwargs.update(overrides) @@ -117,6 +128,11 @@ def test_context_review_and_check_helpers(): ) assert sched.is_opencode_context({"context": "opencode-review"}) assert not sched.is_opencode_context({"context": "strix"}) + assert sched.is_strix_context(strix_check()) + assert sched.is_strix_context(strix_check(workflow="Strix")) + assert sched.is_strix_context({"context": "Strix Security Scan"}) + assert sched.is_strix_context({"__typename": "CheckRun", "name": "strix", "checkSuite": {"workflowRun": {"workflow": None}}}) + assert not sched.is_strix_context({"context": "lint"}) running = make_pr( statusCheckRollup={"contexts": {"nodes": [{"__typename": "CheckRun", "name": "opencode-review", "status": "IN_PROGRESS"}]}} @@ -128,6 +144,21 @@ def test_context_review_and_check_helpers(): assert not sched.opencode_in_progress(complete) unrelated = make_pr(statusCheckRollup={"contexts": {"nodes": [{"context": "strix", "state": "PENDING"}]}}) assert not sched.opencode_in_progress(unrelated) + assert sched.strix_evidence_state(make_pr()) == "missing" + assert sched.strix_evidence_state(unrelated) == "running" + mixed_contexts = make_pr( + statusCheckRollup={"contexts": {"nodes": [{"context": "lint", "state": "SUCCESS"}, strix_check()]}} + ) + assert sched.strix_evidence_state(mixed_contexts) == "complete" + unknown_running = make_pr( + statusCheckRollup={"contexts": {"nodes": [strix_check(status="", conclusion="")]}} + ) + assert sched.strix_evidence_state(unknown_running) == "running" + assert sched.strix_evidence_state(make_pr(statusCheckRollup={"contexts": {"nodes": [strix_check()]}})) == "complete" + assert ( + sched.strix_evidence_state(make_pr(statusCheckRollup={"contexts": {"nodes": [strix_check(conclusion="FAILURE")]}})) + == "complete" + ) threaded = make_pr(reviewThreads={"nodes": [{"isResolved": False}, {"isResolved": True}, {"isOutdated": True}]}) assert sched.unresolved_thread_count(threaded) == 1 @@ -146,6 +177,16 @@ def test_review_state_and_failed_checks(): assert not sched.has_current_head_changes_requested(pr) assert sched.latest_opencode_review(make_pr()) is None assert not sched.latest_opencode_approved(make_pr()) + superseded = make_pr( + reviews={ + "nodes": [ + opencode_review("CHANGES_REQUESTED", "head"), + opencode_review("APPROVED", "head"), + ] + } + ) + assert sched.has_current_head_approval(superseded) + assert not sched.has_current_head_changes_requested(superseded) failed = make_pr( statusCheckRollup={ @@ -167,15 +208,18 @@ def test_actions_call_gh_with_expected_arguments(monkeypatch): pr = make_pr() sched.enable_auto_merge("owner/repo", pr, dry_run=True) sched.update_branch("owner/repo", pr, dry_run=True) + sched.dispatch_strix_evidence("owner/repo", "Strix Security Scan", pr, dry_run=True) sched.dispatch_opencode_review("owner/repo", "OpenCode Review", pr, dry_run=True) assert calls == [] sched.enable_auto_merge("owner/repo", pr, dry_run=False) sched.update_branch("owner/repo", pr, dry_run=False) + sched.dispatch_strix_evidence("owner/repo", "Strix Security Scan", pr, dry_run=False) sched.dispatch_opencode_review("owner/repo", "OpenCode Review", pr, dry_run=False) assert calls[0][:4] == ["gh", "pr", "merge", "1"] assert calls[1][:4] == ["gh", "api", "-X", "PUT"] - assert calls[2][:5] == ["gh", "workflow", "run", "OpenCode Review", "--repo"] + assert calls[2][:5] == ["gh", "workflow", "run", "Strix Security Scan", "--repo"] + assert calls[3][:5] == ["gh", "workflow", "run", "OpenCode Review", "--repo"] def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): @@ -190,9 +234,10 @@ def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): stale_behind = make_pr(mergeStateStatus="BEHIND", reviews={"nodes": [opencode_review("APPROVED", "old")]}) dispatched = [] + monkeypatch.setattr(sched, "dispatch_strix_evidence", lambda repo, workflow, pr, dry_run: dispatched.append(workflow)) monkeypatch.setattr(sched, "dispatch_opencode_review", lambda repo, workflow, pr, dry_run: dispatched.append(workflow)) - assert inspect(stale_behind).action == "review_dispatch" - assert dispatched == ["OpenCode Review"] + assert inspect(stale_behind).action == "security_dispatch" + assert dispatched == ["Strix Security Scan"] behind = make_pr(mergeStateStatus="BEHIND", reviews={"nodes": [opencode_review("APPROVED", "head")]}) assert inspect(behind, update_branches=False).reason == "current-head OpenCode review approved; branch update disabled" @@ -227,9 +272,16 @@ def test_inspect_pr_handles_approved_reviews_and_dispatch(monkeypatch): assert inspect(running).reason == "OpenCode review is already in progress" dispatched = [] + monkeypatch.setattr(sched, "dispatch_strix_evidence", lambda repo, workflow, pr, dry_run: dispatched.append(workflow)) monkeypatch.setattr(sched, "dispatch_opencode_review", lambda repo, workflow, pr, dry_run: dispatched.append(workflow)) - assert inspect(make_pr()).action == "review_dispatch" - assert dispatched == ["OpenCode Review"] + assert inspect(make_pr()).action == "security_dispatch" + assert dispatched == ["Strix Security Scan"] + assert ( + inspect(make_pr(statusCheckRollup={"contexts": {"nodes": [strix_check(status="IN_PROGRESS", conclusion="")]}})).reason + == "same-head Strix evidence is still running" + ) + assert inspect(make_pr(statusCheckRollup={"contexts": {"nodes": [strix_check()]}})).action == "review_dispatch" + assert dispatched == ["Strix Security Scan", "OpenCode Review"] assert inspect(make_pr(), trigger_reviews=False).reason == "current head has no OpenCode approval" @@ -250,6 +302,7 @@ def test_print_summary_self_test_parse_args_and_main(monkeypatch, capsys): parsed = sched.parse_args(["--repo", "owner/repo", "--base-branch", "main", "--project-flow", "github", "--no-trigger-reviews"]) assert parsed.repo == "owner/repo" assert not parsed.trigger_reviews + assert parsed.security_workflow == "Strix Security Scan" assert sched.main(["--self-test"]) == 0 monkeypatch.delenv("GITHUB_REPOSITORY", raising=False)