diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 251f283eb..ffe05d1ae 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3,6 +3,29 @@ name: Required OpenCode Review on: pull_request_target: types: [opened, synchronize, reopened, ready_for_review] + workflow_call: + inputs: + pr_number: + description: Pull request number to review + required: true + type: string + pr_base_ref: + description: Pull request base branch + required: true + type: string + pr_base_sha: + description: Pull request base SHA + required: true + type: string + pr_head_sha: + description: Pull request head SHA + required: true + type: string + canonical_ref: + description: Ref of ContextualWisdomLab/.github to use for trusted review scripts + required: false + default: main + type: string workflow_dispatch: inputs: pr_number: @@ -33,7 +56,7 @@ on: type: string concurrency: - group: opencode-review-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.inputs.pr_number || github.run_id }} + group: opencode-review-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number || github.run_id }} cancel-in-progress: true permissions: @@ -59,7 +82,7 @@ jobs: - name: Resolve trusted OpenCode source ref id: trusted_source env: - INPUT_CANONICAL_REF: ${{ github.event.inputs.canonical_ref || '' }} + INPUT_CANONICAL_REF: ${{ inputs.canonical_ref || github.event.inputs.canonical_ref || '' }} WORKFLOW_REF: ${{ github.workflow_ref }} run: | set -euo pipefail @@ -86,7 +109,7 @@ jobs: fetch-depth: 0 persist-credentials: false token: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }} - ref: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + ref: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} path: pr-head - name: Install Python coverage measurement tools @@ -95,8 +118,8 @@ jobs: - name: Measure test and docstring evidence id: measure env: - 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 }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} COVERAGE_SOURCE_WORKDIR: ${{ github.workspace }}/pr-head run: | set -euo pipefail @@ -517,7 +540,7 @@ jobs: opencode-review-target: name: opencode-review needs: [coverage-evidence] - if: always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target') + if: always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_call') runs-on: ubuntu-latest permissions: actions: read @@ -535,7 +558,7 @@ jobs: - name: Resolve trusted OpenCode source ref id: trusted_source env: - INPUT_CANONICAL_REF: ${{ github.event.inputs.canonical_ref || '' }} + INPUT_CANONICAL_REF: ${{ inputs.canonical_ref || github.event.inputs.canonical_ref || '' }} WORKFLOW_REF: ${{ github.workflow_ref }} run: | set -euo pipefail @@ -559,10 +582,10 @@ jobs: env: GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }} GH_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - PR_BASE_REF: ${{ github.event.pull_request.base.ref || github.event.inputs.pr_base_ref }} - 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 }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref || inputs.pr_base_ref || github.event.inputs.pr_base_ref }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head run: | set -euo pipefail @@ -636,10 +659,10 @@ jobs: env: GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }} GH_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - 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 }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md @@ -1430,10 +1453,10 @@ jobs: OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-primary.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - 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 }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} run: | @@ -1556,10 +1579,10 @@ jobs: OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-fallback.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - 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 }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} run: | @@ -1683,10 +1706,10 @@ jobs: OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-second-fallback.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - 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 }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} run: | @@ -1811,10 +1834,10 @@ jobs: OPENCODE_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-catalog-fallback.md OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - 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 }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} run: | @@ -1983,8 +2006,8 @@ jobs: env: GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} GH_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} OPENCODE_PRIMARY_OUTCOME: ${{ steps.opencode_review_primary.outputs.review_status }} @@ -1997,8 +2020,8 @@ jobs: OPENCODE_CATALOG_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-catalog-fallback.md # The publish gate re-runs source-backed validation against PR-head data. OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - 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 }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} run: | set -euo pipefail @@ -2265,8 +2288,8 @@ jobs: USE_GITHUB_TOKEN: "true" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} OPENCODE_PRIMARY_OUTCOME: ${{ steps.opencode_review_primary.outputs.review_status }} @@ -2277,8 +2300,8 @@ jobs: OPENCODE_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-fallback.md OPENCODE_SECOND_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-second-fallback.md OPENCODE_CATALOG_FALLBACK_OUTPUT_FILE: ${{ runner.temp }}/opencode-review-catalog-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 }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} APPROVAL_CHECK_WAIT_ATTEMPTS: "81" APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "30" CHECK_LOOKUP_RETRY_ATTEMPTS: "5" @@ -3040,7 +3063,7 @@ jobs: } emit_known_missing_string_finding \ - "github.event.inputs.strix_llm || 'openai/gpt-5'" \ + "inputs.strix_llm || github.event.inputs.strix_llm || 'openai/gpt-5'" \ "Strix PR scans must default to GitHub Models GPT-5" \ ".github/workflows/strix.yml" \ "scripts/ci/test_strix_quick_gate.sh" diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 6b1ba6528..348c8549e 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -4,6 +4,25 @@ on: push: branches: [main, develop, master] pull_request_target: + workflow_call: + inputs: + pr_number: + description: Optional pull request number for trusted PR-scope evidence + required: false + type: string + pr_base_sha: + description: Optional pull request base SHA for trusted PR-scope evidence + required: false + type: string + pr_head_sha: + description: Optional pull request head SHA for trusted PR-scope evidence + required: false + type: string + strix_llm: + description: Optional Strix model override for caller evidence runs + required: false + default: openai/gpt-5 + type: string schedule: # Weekly scan on protected branches (Mondays at 03:00 UTC). - cron: '0 3 * * 1' @@ -36,9 +55,9 @@ concurrency: group: >- strix-${{ github.event.inputs.target_repository || github.repository }}-${{ github.event_name == 'pull_request_target' && format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) || - github.event.inputs.pr_number != '' && github.event.inputs.pr_head_sha != '' && - format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha) || - github.event.inputs.pr_number != '' && format('pr-{0}', github.event.inputs.pr_number) || github.ref }} + (inputs.pr_number || github.event.inputs.pr_number) != '' && (inputs.pr_head_sha || github.event.inputs.pr_head_sha) != '' && + format('pr-{0}-{1}', inputs.pr_number || github.event.inputs.pr_number, inputs.pr_head_sha || github.event.inputs.pr_head_sha) || + (inputs.pr_number || github.event.inputs.pr_number) != '' && format('pr-{0}', inputs.pr_number || github.event.inputs.pr_number) || github.ref }} # cancel-in-progress deliberately disabled: an attacker could force-push # a benign commit to cancel an in-progress scan of a malicious commit. The # head SHA in PR groups prevents stale scans from serializing newer evidence. @@ -157,12 +176,12 @@ jobs: } >> "$GITHUB_ENV" - name: Fetch pull request head for trusted scan - if: github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '' + if: github.event_name == 'pull_request_target' || (inputs.pr_number || github.event.inputs.pr_number) != '' env: GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }} - PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.inputs.pr_number }} - PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.inputs.pr_base_sha }} - PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} run: | set -euo pipefail if [ -z "$PR_NUMBER" ] || [ -z "$PR_HEAD_SHA" ]; then @@ -227,7 +246,7 @@ jobs: - name: Gate Strix secrets id: gate env: - STRIX_MODEL: ${{ github.event.inputs.strix_llm || 'openai/gpt-5' }} + STRIX_MODEL: ${{ inputs.strix_llm || github.event.inputs.strix_llm || 'openai/gpt-5' }} STRIX_OPENAI_API_KEY: ${{ secrets.STRIX_OPENAI_API_KEY }} STRIX_VERTEX_CREDENTIALS: ${{ secrets.GCP_SA_KEY }} STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} @@ -389,7 +408,7 @@ jobs: - name: Prepare Strix model input file if: steps.gate.outputs.enabled == 'true' env: - STRIX_MODEL: ${{ github.event.inputs.strix_llm || 'openai/gpt-5' }} + STRIX_MODEL: ${{ inputs.strix_llm || github.event.inputs.strix_llm || 'openai/gpt-5' }} run: | umask 077 strix_llm_file="$RUNNER_TEMP/strix_llm.txt" @@ -450,7 +469,7 @@ jobs: CLOUDSDK_PROJECT: ${{ env.CLOUDSDK_PROJECT }} VERTEXAI_LOCATION: ${{ secrets.VERTEX_LOCATION || 'us-central1' }} VERTEX_LOCATION: ${{ secrets.VERTEX_LOCATION || 'us-central1' }} - STRIX_TARGET_PATH: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && '__PR_SCOPE__' || './' }} + STRIX_TARGET_PATH: ${{ (github.event_name == 'pull_request_target' || (inputs.pr_number || github.event.inputs.pr_number) != '') && '__PR_SCOPE__' || './' }} STRIX_SOURCE_DIRS: ". backend frontend" STRIX_REASONING_EFFORT: low STRIX_LLM_MAX_RETRIES: 1 @@ -464,12 +483,12 @@ jobs: YARN_ENABLE_SCRIPTS: "false" BUN_CONFIG_IGNORE_SCRIPTS: "true" STRIX_FAIL_ON_MIN_SEVERITY: MEDIUM - STRIX_DISABLE_PR_SCOPING: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && '0' || '1' }} - GH_TOKEN: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && github.token || '' }} - PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.inputs.pr_number }} - PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.inputs.pr_base_sha }} - PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} - IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '') && 'true' || 'false' }} + STRIX_DISABLE_PR_SCOPING: ${{ (github.event_name == 'pull_request_target' || (inputs.pr_number || github.event.inputs.pr_number) != '') && '0' || '1' }} + GH_TOKEN: ${{ (github.event_name == 'pull_request_target' || (inputs.pr_number || github.event.inputs.pr_number) != '') && github.token || '' }} + PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number }} + PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || inputs.pr_base_sha || github.event.inputs.pr_base_sha }} + PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} + IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || (inputs.pr_number || github.event.inputs.pr_number) != '') && 'true' || 'false' }} run: | budget_suffix="TIME""OUT" process_budget_seconds="1500" @@ -482,7 +501,7 @@ jobs: - name: Collect Strix reports for artifact upload if: ${{ always() && steps.gate.outputs.enabled == 'true' }} env: - PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} + PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }} run: | set -euo pipefail mkdir -p "$GITHUB_WORKSPACE/strix_runs" @@ -517,7 +536,7 @@ jobs: publish-manual-pr-evidence-status: name: publish-manual-pr-evidence-status needs: strix - if: ${{ always() && !cancelled() && github.event_name == 'workflow_dispatch' && github.event.inputs.pr_head_sha != '' }} + if: ${{ always() && !cancelled() && github.event_name == 'workflow_dispatch' && (inputs.pr_head_sha || github.event.inputs.pr_head_sha) != '' }} runs-on: ubuntu-latest permissions: statuses: write @@ -526,7 +545,7 @@ jobs: env: GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }} TARGET_REPOSITORY: ${{ github.event.inputs.target_repository || github.repository }} - PR_HEAD_SHA: ${{ github.event.inputs.pr_head_sha }} + PR_HEAD_SHA: ${{ inputs.pr_head_sha || github.event.inputs.pr_head_sha }} STRIX_RESULT: ${{ needs.strix.result }} run: | set -euo pipefail diff --git a/PR_GOVERNANCE_AUDIT.md b/PR_GOVERNANCE_AUDIT.md index f3182eb84..f4065de25 100644 --- a/PR_GOVERNANCE_AUDIT.md +++ b/PR_GOVERNANCE_AUDIT.md @@ -302,16 +302,18 @@ PR #381: wait: OpenCode review is already in progress ## Remaining Proof Gaps -- 2026-06-29 KST `html4tree` onboarding gap: PR #3 is the lowest open PR and is - cleanly mergeable by GitHub, but current head - `d0c4cbc2bb267aed407e4bf6308f4f3cfd3b504c` has no check runs and no reviews. - The PR title claims an XSS/attribute-injection fix, while the diff only - changes indentation in `src/main/kotlin/html4tree/util.kt`. This proves that - `html4tree` cannot be merged by queue order until central Strix, OpenCode - Review, and scheduler evidence run on the same head and OpenCode either - requests changes or approves real code/test evidence. The required process - repair is to add `html4tree` to the organization required-workflow target set - or add a temporary thin caller that delegates to `.github`; do not bypass the review gate +- 2026-06-29 KST `html4tree` onboarding gap: after obsolete/no-op PRs #3-#19 + were closed with queue evidence, PR #20 is the lowest open PR. It is cleanly + mergeable by GitHub and contains real generated-HTML accessibility changes, + but current head `e5598f21cd6147b5fcc0e281d29866ae70bc77db` has no check runs + and no reviews from the central `.github` process. `html4tree` is a public + fork, and live repository ruleset lookup with inherited rules enabled returns + no applied organization rulesets. This proves that `html4tree` cannot be + merged by queue order until central Strix, OpenCode Review, and scheduler + evidence run on the same head and OpenCode either requests changes or + approves real code/test evidence. The required process repair is to add + `html4tree` to the organization required-workflow target set or add a + temporary thin caller that delegates to `.github`; do not bypass the review gate with a manual or forced merge. - 2026-06-26 17:53 KST continuation snapshot: `.github` PR #68 is merged at merge commit `590b4ecb2ac9eac700019a183081309e28d8f25b`; `bandscope` PR #459 is merged at merge commit `a7173e45304d8681f02fdf43e4de5a6b6540bb44`; `.github` PR #79 and #80 are merged, and organization ruleset `18156473` now requires central Strix, OpenCode, and scheduler workflows from `.github@807254a04efafd5f806e0f70cb067ecf050cfd11`. The live organization target inventory contains 12 public non-fork repositories and confirms `appguardrail` is present while `VibeSec` is not in that set. - PR #80 proved the no-copy required-workflow path after the ruleset update: `scan-pr-queue` ran as a required check in `ContextualWisdomLab/.github`, passed in 7s, used `PULL_REQUEST_NUMBER=80`, and reported `OpenCode review is already in progress` instead of scanning or mutating the entire queue. The same PR passed central Strix in 3m20s and OpenCode in 4m36s on current head `23ee41076b8f3cec21cff3afd3cd5b4380decf12`. diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 3cb072b64..5a1139f98 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -98,8 +98,8 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "pull_request_target:" "strix workflow uses trusted PR trigger" assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha)" "strix workflow scopes pull_request_target concurrency to the active pull request head" assert_file_contains "$workflow_file" 'strix-${{ github.event.inputs.target_repository || github.repository }}' "strix manual dispatch concurrency scopes to the target repository when provided" - assert_file_contains "$workflow_file" "format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha)" "strix workflow scopes manual PR evidence concurrency to the requested pull request head" - assert_file_contains "$workflow_file" "github.event.inputs.pr_number != '' && format('pr-{0}', github.event.inputs.pr_number)" "strix workflow retains a manual PR fallback group when no head SHA is provided" + assert_file_contains "$workflow_file" "format('pr-{0}-{1}', inputs.pr_number || github.event.inputs.pr_number, inputs.pr_head_sha || github.event.inputs.pr_head_sha)" "strix workflow scopes manual or caller PR evidence concurrency to the requested pull request head" + assert_file_contains "$workflow_file" "(inputs.pr_number || github.event.inputs.pr_number) != '' && format('pr-{0}', inputs.pr_number || github.event.inputs.pr_number)" "strix workflow retains a PR fallback group when no head SHA is provided" assert_file_contains "$workflow_file" "|| github.ref" "strix workflow scopes non-PR concurrency to the current ref" assert_file_contains "$workflow_file" "cancel-in-progress: false" "strix workflow never cancels in-progress security evidence" assert_file_contains "$workflow_file" "head SHA in PR groups prevents stale scans from serializing newer evidence" "strix workflow documents stale scan queue avoidance" @@ -182,7 +182,7 @@ assert_strix_workflow_pr_trigger_hardened() { 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=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_contains "$workflow_file" 'IS_PR_EVIDENCE_RUN: ${{ (github.event_name == '"'"'pull_request_target'"'"' || (inputs.pr_number || 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" assert_file_not_contains "$workflow_file" "STRIX_MEMORY_COMPRESSOR_TIMEOUT:" "strix workflow must not expose compressor timeout env names in GitHub logs" @@ -190,7 +190,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_not_contains "$workflow_file" "STRIX_TOTAL_TIMEOUT_SECONDS:" "strix workflow must not expose total timeout env names in GitHub logs" assert_file_not_contains "$workflow_file" "STRIX_PR_SCOPE_MAX_FILES_PER_BATCH" "strix workflow must not split Strix PR evidence into separate scanner runs" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM == 'vertex_ai/gemini-3.1-pro-preview-customtools' && 'vertex_ai/gemini-2.5-flash'" "strix workflow must not quarantine the approved Vertex preview model after organization secret visibility is fixed" - assert_file_contains "$workflow_file" "github.event.inputs.strix_llm || 'openai/gpt-5'" "strix workflow defaults PR Strix scans to GitHub Models GPT-5" + assert_file_contains "$workflow_file" "inputs.strix_llm || github.event.inputs.strix_llm || 'openai/gpt-5'" "strix workflow defaults PR Strix scans to GitHub Models GPT-5" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM ||" "strix workflow must not let the legacy STRIX_LLM secret override PR defaults" assert_file_contains "$workflow_file" "STRIX_LLM must select GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, or an approved organization Vertex AI model" "strix workflow rejects unsupported model inputs" assert_file_contains "$workflow_file" "vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash)" "strix workflow accepts only exact approved organization Vertex AI models" @@ -372,10 +372,10 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { 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_not_contains "$workflow_file" "github.event.pull_request.number == 240" "opencode review workflow must not hard-code repository-specific PR bypasses" - assert_file_contains "$workflow_file" 'group: opencode-review-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.inputs.pr_number || github.run_id }}' "opencode review cancels stale runs per PR instead of preserving older review heads" + assert_file_contains "$workflow_file" 'group: opencode-review-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr_number || github.event.inputs.pr_number || github.run_id }}' "opencode review cancels stale runs per PR instead of preserving older review heads" assert_file_contains "$workflow_file" 'cancel-in-progress: true' "opencode review cancels stale in-progress review attempts when a newer PR event arrives" assert_file_contains "$workflow_file" "github.event.pull_request.head.repo.full_name == github.repository" "opencode pull_request_target coverage execution is limited to same-repository PR heads" - assert_file_contains "$workflow_file" "if: always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target')" "opencode review side effects are limited to manual or required PR events" + assert_file_contains "$workflow_file" "if: always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_call')" "opencode review side effects are limited to manual, required PR, or reusable caller events" assert_file_contains "$workflow_file" "opencode-review-target:" "opencode trusted review job owns the required check surface" 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" @@ -565,8 +565,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "OpenCode %s fallback attempt %s/%s failed" "opencode catalog fallback records per-model retry failures" assert_file_contains "$workflow_file" "github-models/openai/o3 github-models/openai/o3-mini github-models/openai/o4-mini" "opencode review includes additional OpenAI reasoning model fallbacks" assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" - assert_file_contains "$workflow_file" "github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target'" "manual and required OpenCode reviews measure coverage instead of approving skipped coverage evidence" - assert_file_contains "$workflow_file" 'ref: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }}' "coverage evidence checks out the requested PR head SHA as data" + assert_file_contains "$workflow_file" "github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_call'" "manual, required, and reusable OpenCode reviews measure coverage instead of approving skipped coverage evidence" + assert_file_contains "$workflow_file" 'ref: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }}' "coverage evidence checks out the requested PR head SHA as data" assert_file_contains "$workflow_file" 'ref: ${{ steps.trusted_source.outputs.ref }}' "OpenCode review checks out central trusted 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" @@ -678,7 +678,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "statusCheckRollup" "opencode review workflow reads current-head GitHub Checks before approval" assert_file_contains "$workflow_file" "OPENCODE_FAILED_CHECK_EVIDENCE_FILE" "opencode review workflow persists failed-check evidence across review and approval steps" 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" 'HEAD_SHA: ${{ github.event.pull_request.head.sha || inputs.pr_head_sha || github.event.inputs.pr_head_sha }}' "opencode evidence step passes HEAD_SHA to failed-check evidence collection" assert_file_contains "$workflow_file" "FAILED_CHECK_EVIDENCE_ATTEMPTS" "opencode review workflow bounds waiting for peer check failures before model review" assert_file_contains "$workflow_file" 'timeout-minutes: 40' "opencode evidence preparation has a bounded peer-check wait timeout" assert_file_contains "$workflow_file" 'FAILED_CHECK_EVIDENCE_ATTEMPTS: "20"' "opencode review workflow keeps pre-model peer-check waiting bounded for required workflow DX" @@ -880,6 +880,9 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { local readme_file="$REPO_ROOT/README.md" assert_file_contains "$workflow_file" 'workflow_call:' "scheduler can run as the central reusable workflow contract" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'workflow_call:' "strix can run as the central reusable workflow contract" + assert_file_contains "$REPO_ROOT/.github/workflows/opencode-review.yml" 'workflow_call:' "opencode review can run as the central reusable workflow contract" + assert_file_contains "$REPO_ROOT/.github/workflows/opencode-review.yml" "github.event_name == 'workflow_call'" "opencode review job accepts reusable workflow callers" assert_file_contains "$workflow_file" 'pull_request_target:' "scheduler can run as an organization required workflow without repository-local copies" assert_file_contains "$workflow_file" 'workflows: ["Required OpenCode Review"]' "scheduler reruns after required OpenCode Review completion so approvals can trigger merge/update actions" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "scheduler must not hard-code repository-specific PR bypasses" diff --git a/tests/test_pr_governance_audit_contract.py b/tests/test_pr_governance_audit_contract.py index fb0f10fa6..2f94aeb11 100644 --- a/tests/test_pr_governance_audit_contract.py +++ b/tests/test_pr_governance_audit_contract.py @@ -11,11 +11,28 @@ def test_html4tree_public_fork_queue_requires_central_review_gate(): assert "temporary thin caller" in audit assert "same-head central review evidence" in audit assert "2026-06-29 KST `html4tree` onboarding gap" in audit - assert "PR #3 is the lowest open PR" in audit - assert "has no check runs and no reviews" in audit + assert "PR #20 is the lowest open PR" in audit + assert "real generated-HTML accessibility changes" in audit + assert "has no check runs" in audit + assert "no reviews from the central `.github` process" in audit assert "do not bypass the review gate" in audit +def test_central_review_workflows_are_reusable_for_thin_callers(): + """Temporary repo callers must delegate implementation to .github.""" + strix = Path(".github/workflows/strix.yml").read_text(encoding="utf-8") + opencode = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + scheduler = Path(".github/workflows/pr-review-merge-scheduler.yml").read_text(encoding="utf-8") + + for workflow in (strix, opencode, scheduler): + assert "workflow_call:" in workflow + + assert "inputs.pr_number || github.event.inputs.pr_number" in strix + assert "inputs.pr_head_sha || github.event.inputs.pr_head_sha" in strix + assert "github.event_name == 'workflow_call'" in opencode + assert "inputs.pr_number || github.event.inputs.pr_number" in opencode + + def test_afipc_queue_requires_central_required_workflow_evidence(): """Guard the aFIPC central required-workflow coverage gap.""" audit = Path("PR_GOVERNANCE_AUDIT.md").read_text(encoding="utf-8")