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
60 changes: 48 additions & 12 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3258,11 +3258,13 @@ jobs:
# the SAME model 5x let a rate-limited/hung leader consume the whole
# step, so the pool never reached a healthy fallback model.
OPENCODE_MODEL_ATTEMPTS: "1"
# Keep stale providers from pinning required review jobs for hours;
# deterministic current-head fallback remains the escape hatch.
# Keep stale providers from pinning required review jobs for hours.
# Adversarial validation needs enough room to read the evidence, but
# dynamic cadence and the outer watchdog still bound each current-head run.
OPENCODE_RUN_TIMEOUT_SECONDS: "300"
OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"
OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "600"
OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "1080"
# Visit the high-sensitivity candidate catalog once. Per-provider
# failures and timeouts are logged before the publish gate evaluates
# the current-head result and fails closed when no verdict exists.
Expand All @@ -3281,8 +3283,8 @@ jobs:
OPENCODE_DYNAMIC_MAX_CYCLES: "1"
CENTRAL_REVIEW_PROCESS_FALLBACK_ELIGIBLE: ${{ steps.central_review_process_fallback_scope.outputs.eligible || 'false' }}
CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }}
OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_RUN_TIMEOUT_SECONDS: "300"
OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_TOTAL_BUDGET_SECONDS: "420"
OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_RUN_TIMEOUT_SECONDS: "120"
OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_TOTAL_BUDGET_SECONDS: "180"
OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_MAX_CYCLES: "1"
OPENCODE_BACKOFF_INITIAL_SECONDS: "30"
OPENCODE_BACKOFF_MAX_SECONDS: "30"
Expand All @@ -3302,7 +3304,20 @@ jobs:
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh"
set +e
timeout --kill-after=30s "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-1080}s" \
bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh"
pool_status=$?
set -e
if [ "$pool_status" -eq 124 ] || [ "$pool_status" -eq 137 ] || [ "$pool_status" -eq 143 ]; then
printf 'OpenCode model pool exceeded the outer %ss step budget; marking the pool exhausted so current-head evidence fallback can publish a bounded reason instead of blocking the org queue.\n' \
"${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-1080}"
{
printf 'review_model=\n'
printf 'review_status=exhausted\n'
} >>"$GITHUB_OUTPUT"
fi
exit "$pool_status"

- name: Exchange OpenCode app token for review writes
id: opencode_app_token
Expand Down Expand Up @@ -3880,7 +3895,9 @@ jobs:
env:
GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
CHECK_LOOKUP_GH_TOKEN: ${{ github.token }}
CODE_SCANNING_GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token || github.token }}
CONFIGURED_REVIEW_WRITE_TOKEN_SOURCE: ${{ steps.opencode_app_token.outputs.available == 'true' && 'opencode-app' || secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || 'github-token' }}
CODE_SCANNING_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.opencode_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }}
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 }}
# Exposed so the "openai" provider in opencode.jsonc resolves during the
Expand Down Expand Up @@ -3981,6 +3998,7 @@ jobs:
fi
overview_comment_token="$review_write_token"
echo "check lookup token source=${check_lookup_token_source}"
echo "code-scanning lookup token source=${CODE_SCANNING_TOKEN_SOURCE:-configured}"
echo "review write token source=${review_write_token_source}"
echo "review write fallback token source=${review_write_fallback_token_source:-none}"

Expand Down Expand Up @@ -6068,12 +6086,24 @@ jobs:

collect_open_code_scanning_alerts() {
local output_file="$1"
local pr_json head_ref
pr_json="$(timeout "$(check_lookup_api_timeout_seconds)s" \
gh pr view "$PR_NUMBER" --repo "$GH_REPOSITORY" --json headRefName 2>/dev/null)" || return 1
local pr_json head_ref scan_token lookup_error_file
scan_token="${CODE_SCANNING_GH_TOKEN:-${GH_TOKEN:-}}"
if [ -z "$scan_token" ]; then
printf '::warning::Open code-scanning alert lookup skipped because no target-repository read token was configured.\n' >&2
return 1
fi
lookup_error_file="$(mktemp)"
if ! pr_json="$(GH_TOKEN="$scan_token" timeout "$(check_lookup_api_timeout_seconds)s" \
gh pr view "$PR_NUMBER" --repo "$GH_REPOSITORY" --json headRefName 2>"$lookup_error_file")"; then
sed 's/^/gh: /' "$lookup_error_file" >&2 || true
rm -f "$lookup_error_file"
return 1
fi
rm -f "$lookup_error_file"
head_ref="$(printf '%s\n' "$pr_json" | jq -r '.headRefName // empty')"
[ -n "$head_ref" ] || return 1
timeout "$(check_lookup_api_timeout_seconds)s" \
lookup_error_file="$(mktemp)"
if ! GH_TOKEN="$scan_token" timeout "$(check_lookup_api_timeout_seconds)s" \
gh api -X GET "repos/${GH_REPOSITORY}/code-scanning/alerts" \
-f "ref=refs/heads/${head_ref}" \
-f state=open \
Expand All @@ -6092,7 +6122,12 @@ jobs:
]
| .[]
| "- " + .tool + "/" + .rule + ": " + .severity + " alert #" + (.number | tostring) + (if .url != "" then " (" + .url + ")" else "" end)
' >"$output_file"
' >"$output_file" 2>"$lookup_error_file"; then
sed 's/^/gh: /' "$lookup_error_file" >&2 || true
rm -f "$lookup_error_file"
return 1
fi
rm -f "$lookup_error_file"
}

publish_blockers_after_model_unavailable() {
Expand Down Expand Up @@ -6644,11 +6679,12 @@ jobs:
&& steps.opencode_review_model_pool.outputs.review_status != ''
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token || github.token }}
GH_REPOSITORY: ${{ github.event.inputs.target_repository }}
PR_HEAD_SHA: ${{ github.event.inputs.pr_head_sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
OPENCODE_MODEL_POOL_OUTCOME: ${{ steps.opencode_review_model_pool.outputs.review_status }}
OPENCODE_STATUS_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.opencode_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }}
run: |
set -euo pipefail
if [ -z "${PR_HEAD_SHA:-}" ]; then
Expand All @@ -6664,7 +6700,7 @@ jobs:
description="OpenCode workflow_dispatch evidence did not produce approval evidence."
fi

printf 'Publishing OpenCode workflow_dispatch status context opencode-review for %s at %s with state=%s.\n' "$GH_REPOSITORY" "$PR_HEAD_SHA" "$state"
printf 'Publishing OpenCode workflow_dispatch status context opencode-review for %s at %s with state=%s using %s token.\n' "$GH_REPOSITORY" "$PR_HEAD_SHA" "$state" "${OPENCODE_STATUS_TOKEN_SOURCE:-configured}"
gh api -X POST "repos/${GH_REPOSITORY}/statuses/${PR_HEAD_SHA}" \
-f state="$state" \
-f context="opencode-review" \
Expand Down
28 changes: 26 additions & 2 deletions scripts/ci/pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,10 +972,14 @@ def review_matches_current_head(review: dict[str, Any], pr: dict[str, Any]) -> b
"""Return whether a review is valid evidence for the current head commit."""
head = pr.get("headRefOid")
commit = (review.get("commit") or {}).get("oid")
if not head or commit != head:
if not head:
return False
body_head = review_body_head_sha(review)
return body_head is None or body_head.lower() == head.lower()
if commit == head:
return body_head is None or body_head.lower() == head.lower()
if not commit and body_head is not None:
return body_head.lower() == head.lower()
return False


def review_body_head_sha(review: dict[str, Any]) -> str | None:
Expand Down Expand Up @@ -1110,6 +1114,17 @@ def is_deterministic_fallback_approval(review: dict[str, Any]) -> bool:
return any(marker in body for marker in DETERMINISTIC_APPROVAL_MARKERS)


def has_current_head_deterministic_fallback_approval(pr: dict[str, Any]) -> bool:
"""Return whether OpenCode's latest current-head review is fallback-only."""
for review in reversed((pr.get("reviews") or {}).get("nodes") or []):
if not is_opencode_review(review):
continue
if not review_matches_current_head(review, pr):
continue
return is_deterministic_fallback_approval(review)
return False


def current_head_review_state(pr: dict[str, Any], state: str) -> bool:
"""Return whether OpenCode's latest current-head review has the target state."""
target_state = state.upper()
Expand Down Expand Up @@ -2085,6 +2100,15 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio
if opencode_state == "running":
return decide("wait", "OpenCode review is already in progress")

if (
os.environ.get("GITHUB_EVENT_NAME") == "workflow_run"
and has_current_head_deterministic_fallback_approval(pr)
):
return decide(
"wait",
"current-head deterministic fallback is not merge evidence; defer real-model retry to the next scheduler heartbeat",
)

if behind_by and trigger_reviews:
if not update_branches:
return decide("wait", "current head has no OpenCode approval; branch update disabled before review dispatch")
Expand Down
10 changes: 5 additions & 5 deletions scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -571,11 +571,11 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "skipping remaining attempts for this model" "opencode review skips same-model retries after context-window overflow"
assert_file_contains "$workflow_file" 'timeout-minutes: 45' "opencode review target releases stalled review runners within the bounded queue budget"
assert_file_contains "$workflow_file" 'timeout-minutes: 12' "opencode evidence preparation fails closed before it ties up the review queue"
assert_file_contains "$workflow_file" 'timeout-minutes: 30' "opencode model pool gives multiple candidates a bounded review window while capping stalled model attempts"
assert_file_contains "$workflow_file" 'timeout-minutes: 12' "opencode model pool gives multiple candidates a bounded review window while capping stalled model attempts"
assert_file_contains "$workflow_file" 'timeout-minutes: 8' "opencode approval publication is bounded so a stalled review releases queue capacity promptly"
assert_file_contains "$workflow_file" 'continue-on-error: true' "opencode approval gate still runs after model-pool failure to publish a reason"
assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode primary review advances after a ten-minute stalled provider attempt"
assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "1500"' "opencode model pool exits before the step timeout so the approval gate can publish a reason"
assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "180"' "opencode primary review advances after a bounded stalled provider attempt"
assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "540"' "opencode model pool exits before the step timeout so the approval gate can publish a reason"
assert_file_contains "$workflow_file" 'OPENCODE_POOL_MAX_CYCLES: "1"' "opencode model pool visits the candidate catalog once without reoccupying the queue"
assert_file_contains "$workflow_file" "needs.coverage-evidence.result == 'success'" "opencode model pool only runs after coverage evidence passed"
assert_file_contains "$workflow_file" "id: opencode_review_model_pool" "opencode DeepSeek V3 fallback still runs after a primary model timeout or step failure when coverage evidence passed"
Expand Down Expand Up @@ -707,7 +707,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() {
assert_file_not_contains "$workflow_file" "no model produced a valid review control block" "opencode model-failure path no longer documents a final exhausted state"
assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode primary and fallback paths avoid multi-attempt stalls on one model"
assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode catalog fallback tries each model once before moving on"
assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode catalog fallback advances after a ten-minute stalled provider attempt"
assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "180"' "opencode catalog fallback advances after a bounded stalled provider attempt"
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 github-models/openai/gpt-5" "opencode review tries DeepSeek V3 before OpenAI fallbacks"
Expand Down Expand Up @@ -994,7 +994,7 @@ 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" 'timeout-minutes: 30' "opencode model stage has a bounded multi-provider timeout"
assert_file_contains "$workflow_file" 'timeout-minutes: 12' "opencode model stage has a bounded multi-provider timeout"
assert_file_contains "$workflow_file" 'timeout-minutes: 12' "opencode evidence preparation has a bounded peer-check wait timeout"
assert_file_contains "$workflow_file" 'FAILED_CHECK_EVIDENCE_ATTEMPTS: "6"' "opencode review workflow keeps pre-model peer-check waiting bounded for required workflow DX"
assert_file_contains "$workflow_file" 'FAILED_CHECK_EVIDENCE_SLEEP_SECONDS: "5"' "opencode review workflow retries peer-check evidence without stalling the model stage for Strix-scale durations"
Expand Down
Loading
Loading