From 51d2625597f57e594f12a2b095d2fe25f522386f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 29 Jun 2026 17:21:47 +0900 Subject: [PATCH] fix: allow manual opencode reviews for target repos --- .github/workflows/opencode-review.yml | 20 +++++++++++++------- .github/workflows/strix.yml | 18 ++++++++++++------ docs/org-required-workflow-rollout.md | 1 + scripts/ci/test_strix_quick_gate.sh | 14 ++++++++++++-- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index cf459655c..bb2825f38 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -9,6 +9,11 @@ on: description: Pull request number to review required: true type: string + target_repository: + description: Repository that owns the pull request, in owner/name form + required: false + default: "" + type: string pr_base_ref: description: Pull request base branch required: true @@ -77,9 +82,10 @@ jobs: - name: Checkout pull request head for coverage measurement uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.event.inputs.target_repository || github.repository }} 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 }} path: pr-head @@ -553,8 +559,8 @@ jobs: - name: Materialize pull request head for OpenCode review data env: - GH_TOKEN: ${{ github.token }} - GH_REPOSITORY: ${{ github.repository }} + 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 }} @@ -630,8 +636,8 @@ jobs: - name: Prepare bounded OpenCode review evidence timeout-minutes: 40 env: - GH_TOKEN: ${{ github.token }} - GH_REPOSITORY: ${{ github.repository }} + 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 }} @@ -1978,7 +1984,7 @@ jobs: || steps.opencode_review_catalog_fallback.outputs.review_status == 'success') env: GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} - GH_REPOSITORY: ${{ github.repository }} + 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 }} RUN_ID: ${{ github.run_id }} @@ -2247,7 +2253,7 @@ jobs: timeout-minutes: 45 env: GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }} - GH_REPOSITORY: ${{ github.repository }} + GH_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }} STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} OPENCODE_APP_TOKEN: ${{ steps.opencode_app_token.outputs.token }} OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index a63e164fe..6b1ba6528 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -13,6 +13,11 @@ on: description: Optional pull request number for trusted PR-scope evidence required: false type: string + target_repository: + description: Optional repository that owns the pull request, in owner/name form + required: false + default: "" + type: string pr_base_sha: description: Optional pull request base SHA for trusted PR-scope evidence required: false @@ -29,7 +34,7 @@ on: concurrency: group: >- - strix-${{ github.repository }}-${{ github.event_name == 'pull_request_target' && + 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) || @@ -134,8 +139,8 @@ jobs: - name: Materialize target workspace env: - GH_TOKEN: ${{ github.token }} - REPOSITORY: ${{ github.repository }} + GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }} + REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }} TARGET_WORKSPACE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }} run: | set -euo pipefail @@ -154,7 +159,7 @@ jobs: - name: Fetch pull request head for trusted scan if: github.event_name == 'pull_request_target' || github.event.inputs.pr_number != '' env: - GH_TOKEN: ${{ github.token }} + 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 }} @@ -519,7 +524,8 @@ jobs: steps: - name: Publish same-head manual Strix status env: - GH_TOKEN: ${{ github.token }} + 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 }} STRIX_RESULT: ${{ needs.strix.result }} run: | @@ -544,7 +550,7 @@ jobs: ;; esac - gh api -X POST "repos/${GITHUB_REPOSITORY}/statuses/${PR_HEAD_SHA}" \ + gh api -X POST "repos/${TARGET_REPOSITORY}/statuses/${PR_HEAD_SHA}" \ -f state="$state" \ -f context="strix" \ -f description="$description" \ diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 5ed50fc22..4c067d20a 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -35,6 +35,7 @@ The central `.github/workflows/opencode-review.yml` is now part of the active or - Stable required check job name: `opencode-review` - Trusted source: `ContextualWisdomLab/.github` - PR-head handling: checkout or fetch PR head as review data only; trusted scripts come from the central `.github` ref +- Manual target support: OpenCode and Strix `workflow_dispatch` runs can pass `target_repository` for repos such as private `aFIPC` whose PRs do not yet inherit the org required-workflow rule; org ruleset coverage is still the required steady state before draining that queue - Model token posture: use the organization `STRIX_GITHUB_MODELS_TOKEN` secret for GitHub Models calls, with `github.token` as the fallback; live workflow evidence showed `github.token` alone can return 403 from `models.github.ai/inference` - Write posture: OpenCode may create review/comment side effects through the OpenCode app token when available; `github.token` remains the last fallback and publication failures are soft-failed - Coverage execution posture: privileged `pull_request_target` coverage runs only for same-repository PR heads; fork PR heads must be covered by an unprivileged PR-side check or manually trusted dispatch before approval diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b3a700f07..0305d4bf7 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -97,6 +97,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "branches: [main, develop, master]" "strix workflow scans GitHub Flow and Git Flow protected branches" 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" "|| github.ref" "strix workflow scopes non-PR concurrency to the current ref" @@ -117,6 +118,9 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" 'TRUSTED_STRIX_SOURCE=$trusted_strix_source' "strix workflow exports the central Strix source path" assert_file_contains "$workflow_file" 'TRUSTED_STRIX_GATE=$trusted_strix_source/scripts/ci/strix_quick_gate.sh' "strix workflow executes the central Strix gate script" assert_file_contains "$workflow_file" "Materialize target workspace" "strix workflow materializes target repository data separately from trusted scripts" + assert_file_contains "$workflow_file" "target_repository:" "strix workflow_dispatch can target a repository whose PR does not inherit required workflows" + assert_file_contains "$workflow_file" 'REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }}' "strix manual dispatch fetches target repository data instead of the central .github repo" + assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }}' "strix manual dispatch can use the cross-repo approval token to read private target repositories" assert_file_contains "$workflow_file" "TARGET_WORKSPACE_SHA" "strix workflow pins target workspace SHA" assert_file_contains "$workflow_file" "TRUSTED_WORKSPACE=\$trusted_workspace" "strix workflow exports a trusted workspace path" assert_file_contains "$workflow_file" "git -C \"\$TRUSTED_WORKSPACE\"" "strix workflow runs git only inside trusted workspace" @@ -154,7 +158,7 @@ assert_strix_workflow_pr_trigger_hardened() { in_block { print } ' "$workflow_file" )" - if [[ "$pr_head_fetch_block" != *'GH_TOKEN: ${{ github.token }}'* ]]; then + if [[ "$pr_head_fetch_block" != *'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }}'* ]]; then record_failure "strix workflow passes GH_TOKEN to PR head fetch step" fi if [[ "$pr_head_fetch_block" != *"gh auth setup-git"* ]]; then @@ -396,7 +400,9 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "Checkout trusted OpenCode coverage contract" "opencode coverage job uses central trusted coverage tooling instead of target-repo copies" assert_file_contains "$workflow_file" "repository: ContextualWisdomLab/.github" "opencode required workflow checks out the central source repository" assert_file_contains "$workflow_file" 'ref: ${{ steps.trusted_source.outputs.ref }}' "opencode required workflow checks out the resolved central ref" - assert_file_contains "$workflow_file" 'repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}' "opencode coverage checks out the PR head repository separately from trusted scripts" + assert_file_contains "$workflow_file" "target_repository:" "opencode workflow_dispatch can target a repository whose PR does not inherit required workflows" + assert_file_contains "$workflow_file" 'repository: ${{ github.event.pull_request.head.repo.full_name || github.event.inputs.target_repository || github.repository }}' "opencode coverage checks out the PR head repository separately from trusted scripts" + assert_file_contains "$workflow_file" 'token: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }}' "opencode manual dispatch can use the cross-repo approval token to read private target repositories" assert_file_contains "$workflow_file" "path: pr-head" "opencode coverage keeps PR-head data outside the trusted workflow root" assert_file_contains "$workflow_file" 'COVERAGE_SOURCE_WORKDIR: ${{ github.workspace }}/pr-head' "opencode coverage measures the PR-head checkout explicitly" assert_file_not_contains "$workflow_file" "pr_head_ref:" "opencode workflow_dispatch no longer accepts an unused PR head branch input" @@ -617,7 +623,9 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { 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" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'statuses: write' "strix manual evidence status job has commit-status write permission" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'TARGET_REPOSITORY: ${{ github.event.inputs.target_repository || github.repository }}' "strix manual evidence status publishes to the requested target repository" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'context="strix"' "strix manual evidence status uses the status context consumed by OpenCode" + assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'repos/${TARGET_REPOSITORY}/statuses/${PR_HEAD_SHA}' "strix manual evidence status does not post private-target evidence to .github by mistake" assert_file_contains "$REPO_ROOT/.github/workflows/strix.yml" 'Manual workflow_dispatch Strix evidence failed' "strix manual evidence status records failed reruns so older success cannot mask newer failure" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '"workflow_run"' "failed-check evidence includes failed same-head workflow runs outside statusCheckRollup" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" "--json databaseId,workflowName,status,conclusion,url,event,headSha" "failed-check evidence scopes supplemental workflow runs with event and head SHA metadata" @@ -664,6 +672,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_not_contains "$workflow_file" "opencode github run" "opencode review workflow must not use the oversized GitHub agent prompt path" assert_file_not_contains "$workflow_file" 'repos/${{ github.repository }}' "opencode review workflow must pass repository expressions through env before shell use" assert_file_contains "$workflow_file" "GH_REPOSITORY:" "opencode review workflow exports repository context through env" + assert_file_contains "$workflow_file" 'GH_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }}' "opencode manual dispatch routes API calls and review publication to the requested target repository" + assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || github.token }}' "opencode manual dispatch uses the cross-repo approval token for target PR evidence lookups" assert_file_contains "$workflow_file" 'repos/${GH_REPOSITORY}' "opencode review workflow uses env-backed repository context in shell commands" assert_file_contains "$workflow_file" "Run OpenCode PR Review (DeepSeek R1)" "opencode review starts with DeepSeek R1" assert_file_contains "$workflow_file" "MODEL: github-models/deepseek/deepseek-r1-0528" "opencode review starts with a reachable DeepSeek R1 reasoning model"