Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 105 additions & 4 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
permissions:
contents: read
pull-requests: read
id-token: write
outputs:
target_repository: ${{ steps.validate.outputs.target_repository }}
pr_number: ${{ steps.validate.outputs.pr_number }}
Expand All @@ -46,10 +47,81 @@ jobs:
head_sha: ${{ steps.validate.outputs.head_sha }}
is_private: ${{ steps.validate.outputs.is_private }}
steps:
- name: Exchange OpenCode app token for target repository metadata reads
id: metadata_read_app_token
if: >-
github.event_name == 'repository_dispatch'
&& github.event.client_payload.target_repository != ''
&& github.event.client_payload.target_repository != github.repository
env:
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
run: |
set -euo pipefail

mark_unavailable() {
echo "available=false" >>"$GITHUB_OUTPUT"
}

if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] ||
[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "OpenCode app token exchange unavailable: OIDC request environment is missing."
mark_unavailable
exit 0
fi

request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}"
separator="&"
case "$request_url" in
*\?*) ;;
*) separator="?" ;;
esac

if ! oidc_response="$(
curl -fsS \
-H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${request_url}${separator}audience=${OIDC_AUDIENCE}"
)"; then
echo "OpenCode app token exchange unavailable: OIDC token request did not complete."
mark_unavailable
exit 0
fi

oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")"
if [ -z "$oidc_token" ]; then
echo "OpenCode app token exchange unavailable: OIDC token response was empty."
mark_unavailable
exit 0
fi

if ! token_response="$(
curl -fsS \
-X POST \
-H "Authorization: Bearer ${oidc_token}" \
"${OPENCODE_API_BASE_URL}/exchange_github_app_token"
)"; then
echo "OpenCode app token exchange unavailable: app token request did not complete."
mark_unavailable
exit 0
fi

app_token="$(jq -r '.token // empty' <<<"$token_response")"
if [ -z "$app_token" ]; then
echo "OpenCode app token exchange unavailable: app token response was empty."
mark_unavailable
exit 0
fi

echo "::add-mask::$app_token"
{
echo "available=true"
echo "token=$app_token"
} >>"$GITHUB_OUTPUT"

- name: Bind workflow inputs to live organization pull request metadata
id: validate
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
GH_TOKEN: ${{ steps.metadata_read_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
EVENT_NAME: ${{ github.event_name }}
# A rerun retains github.actor from the original dispatch; authorize
# the identity that initiated the current run or rerun instead.
Expand Down Expand Up @@ -3341,7 +3413,7 @@ jobs:
"$schema": "https://opencode.ai/config.json",
"model": "github-models/deepseek/deepseek-r1-0528",
"small_model": "github-models/deepseek/deepseek-v3-0324",
"enabled_providers": ["opencode-free", "openai", "openrouter", "github-models"],
"enabled_providers": ["opencode-free", "opencode", "openai", "openrouter", "github-models"],
"lsp": false,
"mcp": {},
"permission": {
Expand Down Expand Up @@ -3528,6 +3600,33 @@ jobs:
}
}
},
"opencode": {
"npm": "@ai-sdk/openai",
"name": "OpenCode Zen",
"options": {
"baseURL": "https://opencode.ai/zen/v1",
"apiKey": "{env:OPENCODE_API_KEY}"
},
"models": {
"gpt-5.6-terra": {
"name": "OpenCode Zen GPT-5.6 Terra",
"tool_call": true,
"reasoning": true,
"options": {
"reasoningEffort": "high"
},
"variants": {
"high": {
"reasoningEffort": "high"
}
},
"limit": {
"context": 1000000,
"output": 128000
}
}
}
},
"openai": {
"npm": "@ai-sdk/openai",
"name": "OpenAI (direct)",
Expand Down Expand Up @@ -3836,6 +3935,7 @@ jobs:
# api.openai.com directly with the org OPENAI_API_KEY gives the lead
# model a working, un-throttled backend. Resolves {env:OPENAI_API_KEY}
# in the opencode.jsonc "openai" provider block.
OPENCODE_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
SHARE: "false"
Expand All @@ -3847,7 +3947,8 @@ jobs:
# may be logged, retained, or used for product/model improvement, so
# private repositories never include them. The pool then falls
# through to
# DeepSeek V3, the direct GPT-5.6 Luna slot, pinned PAID
# OpenCode Zen GPT-5.6 Terra, DeepSeek V3, the direct GPT-5.6 Luna
# slot, pinned PAID
# OpenRouter coder models (free-tier candidates hit the shared
# free-models-per-day cap and hung for the full candidate timeout,
# so the OpenRouter slots use cheap paid models billed against the
Expand All @@ -3857,7 +3958,7 @@ jobs:
# cost-efficient tier, cheaper than the legacy gpt-5 it replaced
# ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget
# stretches further between top-ups.
OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free ' || '' }}github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1"
OPENCODE_MODEL_CANDIDATES: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free ' || '' }}opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1"
# One attempt per model, then fall through to the next model. Retrying
# the SAME model 5x let a rate-limited/hung leader consume the whole
# step, so the pool never reached a healthy fallback model.
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ concurrency:
github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number) ||
github.event_name == 'workflow_call' && inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) ||
github.event_name == 'workflow_call' && inputs.base_branch != '' && format('call-{0}', inputs.base_branch) ||
github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule) ||
github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.pr_number != '' && format('target-{0}-pr-{1}', github.event.client_payload.target_repository, github.event.client_payload.pr_number) ||
github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && format('pr-{0}', github.event.client_payload.pr_number) ||
github.event_name == 'repository_dispatch' && github.run_id ||
Expand Down Expand Up @@ -563,7 +564,11 @@ jobs:
(github.event_name == 'repository_dispatch' && github.event.client_payload.org_sweep == true)
)
runs-on: ubuntu-latest
timeout-minutes: 30
# The complete organization walk exceeded the legacy 30-minute boundary in
# production. Keep one running and one latest pending */15 sweep through the
# schedule-specific concurrency key above, while allowing the current walk
# enough time to finish instead of cancelling before later repositories.
timeout-minutes: 60
permissions:
actions: write
checks: read
Expand All @@ -583,10 +588,10 @@ jobs:
ORG_SWEEP_MAX_PRS: ${{ github.event.client_payload.max_prs || inputs.max_prs || vars.ORG_SWEEP_MAX_PRS || '1000' }}
ORG_SWEEP_REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_dispatch_limit || inputs.review_dispatch_limit || vars.ORG_SWEEP_REVIEW_DISPATCH_LIMIT || '1' }}
ORG_SWEEP_BRANCH_UPDATE_LIMIT: ${{ github.event.client_payload.branch_update_limit || inputs.branch_update_limit || vars.ORG_SWEEP_BRANCH_UPDATE_LIMIT || '1' }}
ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false || inputs.trigger_reviews == true }}
ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false || inputs.enable_auto_merge == true }}
ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false || inputs.trigger_reviews == true }}
ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false || inputs.enable_auto_merge == true }}
ORG_SWEEP_MERGE_MODE: ${{ github.event.client_payload.merge_mode || inputs.merge_mode || 'direct_or_auto' }}
ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false || inputs.update_branches == true }}
ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false || inputs.update_branches == true }}
ORG_SWEEP_STALE_QUEUE_HOURS: ${{ vars.ORG_SWEEP_STALE_QUEUE_HOURS || '24' }}
# A repository the sweep credential structurally cannot read (the OpenCode
# app is not installed there / the PR_REVIEW_MERGE_TOKEN lacks it) returns
Expand Down
11 changes: 8 additions & 3 deletions scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review includes a broad catalog fallback pool"
assert_file_not_contains "$workflow_file" "steps.opencode_review_model_pool.outcome == 'success'" "opencode approval gate still runs after model pool failure to publish a reason"
assert_file_contains "$workflow_file" "opencode-free/north-mini-code-free" "opencode review starts public repository reviews with a free coding model"
assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review retains DeepSeek V3 before full-size GPT fallbacks"
assert_file_contains "$workflow_file" "opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review retains paid Zen and DeepSeek V3 before full-size GPT fallbacks"
assert_file_contains "$workflow_file" "The publish gate re-runs source-backed validation against PR-head data" "opencode review publish gate validates model output against the PR-head worktree"
assert_file_contains "$workflow_file" '"openai/o3"' "opencode config declares OpenAI o3 fallback"
assert_file_contains "$workflow_file" '"openai/o4-mini"' "opencode config declares OpenAI o4-mini fallback"
Expand Down Expand Up @@ -868,7 +868,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode catalog fallback preserves legitimate full-hour provider sessions"
assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode %s attempt %s/%s failed" "opencode catalog fallback records per-model retry failures"
assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "exponential backoff" "opencode model retry paths use exponential backoff instead of fixed sleeps"
assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review tries DeepSeek V3 before OpenAI fallbacks"
assert_file_contains "$workflow_file" "opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review tries paid Zen and DeepSeek V3 before OpenAI fallbacks"
assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" "opencode review keeps DeepSeek reasoning fallback coverage after OpenAI candidates"
assert_file_contains "$workflow_file" "coverage-source-tree:" "opencode workflow materializes coverage source before running PR-head tests"
assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review"
Expand Down Expand Up @@ -1197,7 +1197,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || steps.review_read_app_token.outputs.token || github.token }}' "opencode manual dispatch uses the cross-repo approval token for target PR evidence lookups with app-token fallback"
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 model pool" "opencode review starts the central model pool"
assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review starts with DeepSeek V3 before full-size GPT fallbacks"
assert_file_contains "$workflow_file" "opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review starts with paid Zen before DeepSeek V3 and full-size GPT fallbacks"
assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528" "opencode review keeps a reachable DeepSeek R1 reasoning fallback model"
assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324" "opencode review has a reachable DeepSeek V3 fallback model"
assert_file_contains "$workflow_file" "github-models/openai/gpt-5" "opencode review still has a bounded GPT-5 fallback model"
Expand Down Expand Up @@ -1445,8 +1445,13 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() {
assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "scheduler must not hard-code repository-specific PR bypasses"
assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR"
assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR"
assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates the 15-minute organization sweep from the separate 30-minute scheduled scan"
assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.run_id" "scheduler keeps manual queue scans isolated per run"
assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts"
assert_file_contains "$workflow_file" "timeout-minutes: 60" "organization sweep has enough headroom to finish the complete repository walk"
assert_file_contains "$workflow_file" "ORG_SWEEP_TRIGGER_REVIEWS: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps retry missing current-head OpenCode reviews"
assert_file_contains "$workflow_file" "ORG_SWEEP_ENABLE_AUTO_MERGE: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps merge approved current heads"
assert_file_contains "$workflow_file" "ORG_SWEEP_UPDATE_BRANCHES: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps refresh eligible stale branches"
assert_file_contains "$workflow_file" 'github.event.workflow_run.pull_requests[0].number' "scheduler scopes OpenCode workflow_run events to the completed review PR"
assert_file_contains "$workflow_file" "github.event.client_payload.trigger_reviews != false" "scheduler enables review dispatch by default for default-branch dispatch events"
assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' || github.event_name == 'push'" "scheduler can dispatch a bounded follow-up OpenCode review after review workflow completion"
Expand Down
Loading
Loading