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
53 changes: 45 additions & 8 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
base_sha: ${{ steps.validate.outputs.base_sha }}
head_ref: ${{ steps.validate.outputs.head_ref }}
head_sha: ${{ steps.validate.outputs.head_sha }}
is_private: ${{ steps.validate.outputs.is_private }}
steps:
- name: Bind workflow inputs to live organization pull request metadata
id: validate
Expand Down Expand Up @@ -102,12 +103,14 @@ jobs:
live_head_ref="$(jq -r '.head.ref // empty' <<<"$pull_request_json")"
live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")"
live_state="$(jq -r '.state // empty' <<<"$pull_request_json")"
live_is_private="$(jq -r '.base.repo.private | tostring' <<<"$pull_request_json")"

if [ "$live_state" != "open" ] ||
[ "$live_base_repository" != "$TARGET_REPOSITORY" ] ||
[ "$live_head_repository" != "$TARGET_REPOSITORY" ] ||
! [[ "$live_base_sha" =~ ^[0-9a-fA-F]{40}$ ]] ||
! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]] ||
! [[ "$live_is_private" =~ ^(true|false)$ ]] ||
Comment thread
coderabbitai[bot] marked this conversation as resolved.
[ -z "$live_base_ref" ] ||
[ -z "$live_head_ref" ]; then
printf '::error::PR metadata validation rejected closed, missing, cross-repository, or malformed live metadata. target=%s#%s state=%s base_repo=%s head_repo=%s base=%s head=%s\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${live_state:-<missing>}" "${live_base_repository:-<missing>}" "${live_head_repository:-<missing>}" "${live_base_sha:-<missing>}" "${live_head_sha:-<missing>}"
Expand All @@ -133,6 +136,7 @@ jobs:
printf 'base_sha=%s\n' "$live_base_sha"
printf 'head_ref=%s\n' "$live_head_ref"
printf 'head_sha=%s\n' "$live_head_sha"
printf 'is_private=%s\n' "$live_is_private"
} >>"$GITHUB_OUTPUT"
printf 'Validated current live metadata for %s#%s: base=%s/%s head=%s/%s.\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "$live_base_ref" "$live_base_sha" "$live_head_ref" "$live_head_sha"

Expand Down Expand Up @@ -1834,6 +1838,7 @@ jobs:
EXPECTED_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }}
EXPECTED_HEAD_REF: ${{ needs.validate-pr-metadata.outputs.head_ref }}
EXPECTED_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }}
EXPECTED_IS_PRIVATE: ${{ needs.validate-pr-metadata.outputs.is_private }}
run: |
set -euo pipefail
if ! [[ "$GH_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] ||
Expand All @@ -1849,15 +1854,19 @@ jobs:
live_base_sha="$(jq -r '.base.sha // empty' <<<"$pull_request_json")"
live_head_ref="$(jq -r '.head.ref // empty' <<<"$pull_request_json")"
live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")"
live_is_private="$(jq -r '.base.repo.private | tostring' <<<"$pull_request_json")"
if [ "$live_state" != "open" ] ||
[ "$base_repository" != "$GH_REPOSITORY" ] ||
[ "$head_repository" != "$GH_REPOSITORY" ] ||
[ "$live_base_ref" != "$EXPECTED_BASE_REF" ] ||
[ "$live_base_sha" != "$EXPECTED_BASE_SHA" ] ||
[ "$live_head_ref" != "$EXPECTED_HEAD_REF" ] ||
[ "$live_head_sha" != "$EXPECTED_HEAD_SHA" ]; then
printf '::error::OpenCode privileged review metadata changed before OIDC, review-token, CodeGraph, or model execution. target=%s#%s state=%s base_repo=%s base=%s/%s expected_base=%s/%s head_repo=%s head=%s/%s expected_head=%s/%s\n' \
"$GH_REPOSITORY" "$PR_NUMBER" "${live_state:-<missing>}" "${base_repository:-<missing>}" "${live_base_ref:-<missing>}" "${live_base_sha:-<missing>}" "$EXPECTED_BASE_REF" "$EXPECTED_BASE_SHA" "${head_repository:-<missing>}" "${live_head_ref:-<missing>}" "${live_head_sha:-<missing>}" "$EXPECTED_HEAD_REF" "$EXPECTED_HEAD_SHA"
[ "$live_head_sha" != "$EXPECTED_HEAD_SHA" ] ||
! [[ "$EXPECTED_IS_PRIVATE" =~ ^(true|false)$ ]] ||
! [[ "$live_is_private" =~ ^(true|false)$ ]] ||
[ "$live_is_private" != "$EXPECTED_IS_PRIVATE" ]; then
printf '::error::OpenCode privileged review metadata changed before OIDC, review-token, CodeGraph, or model execution. target=%s#%s state=%s base_repo=%s base=%s/%s expected_base=%s/%s head_repo=%s head=%s/%s expected_head=%s/%s private=%s expected_private=%s\n' \
"$GH_REPOSITORY" "$PR_NUMBER" "${live_state:-<missing>}" "${base_repository:-<missing>}" "${live_base_ref:-<missing>}" "${live_base_sha:-<missing>}" "$EXPECTED_BASE_REF" "$EXPECTED_BASE_SHA" "${head_repository:-<missing>}" "${live_head_ref:-<missing>}" "${live_head_sha:-<missing>}" "$EXPECTED_HEAD_REF" "$EXPECTED_HEAD_SHA" "${live_is_private:-<missing>}" "${EXPECTED_IS_PRIVATE:-<missing>}"
exit 1
fi
printf 'Validated same-repository OpenCode review source for %s#%s (%s).\n' \
Expand Down Expand Up @@ -3267,7 +3276,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": ["openai", "openrouter", "github-models"],
"enabled_providers": ["opencode-free", "openai", "openrouter", "github-models"],
"lsp": false,
"mcp": {},
"permission": {
Expand Down Expand Up @@ -3344,6 +3353,32 @@ jobs:
}
},
"provider": {
"opencode-free": {
"npm": "@ai-sdk/openai-compatible",
"name": "OpenCode Zen Free",
"options": {
"baseURL": "https://opencode.ai/zen/v1"
},
"models": {
"north-mini-code-free": {
"name": "North Mini Code Free",
"tool_call": true,
"reasoning": true,
"options": {
"reasoningEffort": "high"
},
"variants": {
"high": {
"reasoningEffort": "high"
}
},
"limit": {
"context": 256000,
"output": 64000
}
}
}
},
"openai": {
"npm": "@ai-sdk/openai",
"name": "OpenAI (direct)",
Expand Down Expand Up @@ -3657,9 +3692,11 @@ jobs:
SHARE: "false"
NPM_CONFIG_IGNORE_SCRIPTS: "true"
NO_COLOR: "1"
# High-sensitivity review candidates only. DeepSeek V3 has been the
# most reliable first-pass reviewer in the org queue, then the pool
# falls through to the direct GPT-5.6 Luna slot, pinned PAID
# High-sensitivity review candidates only. Public repositories first
# use OpenCode Zen's anonymous North Mini Code free endpoint. Zen's
# free-period privacy terms permit retention, so private repositories
# never include this candidate. The pool then falls through to
# 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 @@ -3669,7 +3706,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: "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/north-mini-code-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"
# 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
10 changes: 9 additions & 1 deletion scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$workflow_file" "Initialize CodeGraph index for OpenCode" "opencode review workflow initializes CodeGraph before review"
assert_file_contains "$workflow_file" "Validate pull request head repository trust" "opencode privileged review validates the live head repository before token exchange and PR-head tooling"
assert_file_contains "$workflow_file" "metadata changed before OIDC" "opencode privileged review fails closed for repository-dispatched fork or stale heads with a visible reason"
assert_file_contains "$workflow_file" 'EXPECTED_IS_PRIVATE: ${{ needs.validate-pr-metadata.outputs.is_private }}' "opencode privileged review carries the validated privacy state into its final trust check"
assert_file_contains "$workflow_file" '[ "$live_is_private" != "$EXPECTED_IS_PRIVATE" ]' "opencode privileged review fails closed when a public repository becomes private before model execution"
assert_file_contains "$workflow_file" "actions: read" "opencode review workflow can read failed Actions logs without Actions write scope"
assert_file_contains "$workflow_file" "checks: read" "opencode review workflow can read failed check-run annotations for line-specific findings"
assert_file_contains "$workflow_file" "contents: read" "opencode review workflow uses read-only repository contents permission"
Expand Down Expand Up @@ -590,6 +592,11 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_not_contains "$workflow_file" 'secrets.GITHUB_TOKEN' "opencode review uses github.token instead of a nonexistent GITHUB_TOKEN secret"
assert_file_contains "$workflow_file" 'STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}' "opencode review uses the organization GitHub Models token secret with GITHUB_TOKEN fallback"
assert_file_not_contains "$workflow_file" 'GITHUB_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}' "opencode review does not expose GitHub credentials through the generic model environment"
assert_file_contains "$workflow_file" 'is_private: ${{ steps.validate.outputs.is_private }}' "opencode review carries validated repository privacy into model routing"
assert_file_contains "$workflow_file" '"opencode-free"' "opencode review enables its anonymous Zen free provider"
assert_file_contains "$workflow_file" '"baseURL": "https://opencode.ai/zen/v1"' "opencode review routes the free provider through the official Zen endpoint"
assert_file_contains "$workflow_file" '"north-mini-code-free"' "opencode review declares the current Zen coding model"
assert_file_contains "$workflow_file" "needs.validate-pr-metadata.outputs.is_private == 'false'" "opencode review limits data-retaining free models to public repositories"
assert_file_matches "$workflow_file" 'uses:[[:space:]]+actions/checkout@[0-9a-fA-F]{40}([[:space:]]|$)' "opencode review workflow pins checkout to a full commit SHA"
assert_workflow_uses_are_sha_pinned "$workflow_file" "opencode review workflow"
assert_file_contains "$workflow_file" "scripts/ci/codegraph-package/package-lock.json" "opencode review workflow installs CodeGraph from the committed lockfile"
Expand Down Expand Up @@ -707,7 +714,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode fallback tries the catalog promptly instead of spending the entire review on one model"
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" "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-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" "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
34 changes: 30 additions & 4 deletions tests/test_opencode_agent_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,16 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates():
candidates_match = re.search(r'OPENCODE_MODEL_CANDIDATES: "([^"]+)"', workflow)

assert candidates_match is not None
candidates = candidates_match.group(1).split()
conditional_public_candidate = (
"${{ needs.validate-pr-metadata.outputs.is_private == 'false' "
"&& 'opencode-free/north-mini-code-free ' || '' }}"
)
candidates_text = candidates_match.group(1)
assert candidates_text.startswith(conditional_public_candidate)
candidates = [
"opencode-free/north-mini-code-free",
*candidates_text.removeprefix(conditional_public_candidate).split(),
]
candidate_pairs = [candidate.split("/", 1) for candidate in candidates]
direct_openai_models = [
model_name for provider, model_name in candidate_pairs if provider == "openai"
Expand All @@ -106,6 +115,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates():

assert candidate_pairs
assert candidate_pairs == [
["opencode-free", "north-mini-code-free"],
["github-models", "deepseek/deepseek-v3-0324"],
["openai", "gpt-5.6-luna"],
["openrouter", "deepseek/deepseek-v3.2"],
Expand All @@ -123,6 +133,8 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates():
"qwen/qwen3-coder",
]
assert set(github_candidate_models).issubset(set(github_models))
assert '"context": 256000' in workflow
assert '"output": 64000' in workflow
assert github_candidate_models == [
"deepseek/deepseek-v3-0324",
"openai/gpt-4.1",
Expand Down Expand Up @@ -1186,7 +1198,11 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent():
in workflow
)
assert (
'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 '
"needs.validate-pr-metadata.outputs.is_private == 'false' && "
"'opencode-free/north-mini-code-free ' || ''"
) in workflow
assert (
"github-models/deepseek/deepseek-v3-0324 "
"openai/gpt-5.6-luna "
"openrouter/deepseek/deepseek-v3.2 "
"openrouter/qwen/qwen3-coder "
Expand All @@ -1195,7 +1211,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent():
"github-models/openai/gpt-5-chat "
"github-models/openai/o3 "
"github-models/deepseek/deepseek-r1-0528 "
'github-models/deepseek/deepseek-r1"'
"github-models/deepseek/deepseek-r1"
) in workflow
assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow
assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' in workflow
Expand Down Expand Up @@ -1311,7 +1327,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent():
'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow
)
assert (
'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 '
"github-models/deepseek/deepseek-v3-0324 "
"openai/gpt-5.6-luna "
"openrouter/deepseek/deepseek-v3.2 "
"openrouter/qwen/qwen3-coder "
Expand Down Expand Up @@ -1748,6 +1764,16 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed():
assert "metadata changed before OIDC" in trust_step
assert 'live_head_sha="$(jq -r' in trust_step
assert '[ "$live_head_sha" != "$EXPECTED_HEAD_SHA" ]' in trust_step
assert (
"EXPECTED_IS_PRIVATE: "
"${{ needs.validate-pr-metadata.outputs.is_private }}"
) in trust_step
assert (
'live_is_private="$(jq -r \'.base.repo.private | tostring\''
) in trust_step
assert '! [[ "$EXPECTED_IS_PRIVATE" =~ ^(true|false)$ ]]' in trust_step
assert '! [[ "$live_is_private" =~ ^(true|false)$ ]]' in trust_step
assert '[ "$live_is_private" != "$EXPECTED_IS_PRIVATE" ]' in trust_step
assert target_job.index(
"Validate pull request head repository trust"
) < target_job.index(
Expand Down
Loading