From eb9889837e2ada2f2cf2740621d39a0bb3124d45 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 13 Jul 2026 17:37:19 +0900 Subject: [PATCH 1/2] fix(opencode): wait for slow current-head checks --- .github/workflows/opencode-review.yml | 23 +++++++++------- scripts/ci/test_strix_quick_gate.sh | 4 +-- tests/test_opencode_agent_contract.py | 39 +++++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 191c90451..a8c939411 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3650,7 +3650,10 @@ jobs: && steps.opencode_review_model_pool.outputs.review_status == 'success' && steps.central_review_process_fallback_scope.outputs.eligible == 'true' continue-on-error: true - timeout-minutes: 3 + # Some target repositories run multi-language CodeQL, Strix, or R CMD + # checks for several minutes. Keep this step alive long enough for the + # bounded six-minute peer-check window plus publication overhead. + timeout-minutes: 8 env: GH_TOKEN: ${{ steps.opencode_app_token.outputs.token }} CHECK_LOOKUP_GH_TOKEN: ${{ github.token }} @@ -3669,7 +3672,7 @@ jobs: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head_sha }} CENTRAL_REVIEW_PROCESS_FALLBACK_CHANGED_COUNT: ${{ steps.central_review_process_fallback_scope.outputs.changed_count || '0' }} CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }} - APPROVAL_CHECK_WAIT_ATTEMPTS: "12" + APPROVAL_CHECK_WAIT_ATTEMPTS: "36" APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "10" REVIEW_PUBLISH_GH_API_TIMEOUT_SECONDS: "15" run: | @@ -3757,7 +3760,7 @@ jobs: check_runs_file="$(mktemp)" pending_checks_file="$(mktemp)" failed_checks_file="$(mktemp)" - for attempt in $(seq 1 "${APPROVAL_CHECK_WAIT_ATTEMPTS:-12}"); do + for attempt in $(seq 1 "${APPROVAL_CHECK_WAIT_ATTEMPTS:-36}"); do curl_api_read "${api_url}/repos/${GH_REPOSITORY}/commits/${HEAD_SHA}/check-runs?per_page=100" >"$check_runs_file" jq -r "${self_check_filter} latest_peer_checks @@ -3767,8 +3770,8 @@ jobs: if [ ! -s "$pending_checks_file" ]; then break fi - if [ "$attempt" -lt "${APPROVAL_CHECK_WAIT_ATTEMPTS:-12}" ]; then - printf 'Central fast approval waiting for peer checks (%s/%s):\n' "$attempt" "${APPROVAL_CHECK_WAIT_ATTEMPTS:-12}" + if [ "$attempt" -lt "${APPROVAL_CHECK_WAIT_ATTEMPTS:-36}" ]; then + printf 'Central fast approval waiting for peer checks (%s/%s):\n' "$attempt" "${APPROVAL_CHECK_WAIT_ATTEMPTS:-36}" cat "$pending_checks_file" sleep "${APPROVAL_CHECK_WAIT_SLEEP_SECONDS:-10}" fi @@ -3930,7 +3933,9 @@ jobs: # keeps GitHub review publication bounded. Failed-check evidence is # collected from logs/SARIF before this point; central review-process # self-repair must not run a second model pass from the publish step. - timeout-minutes: 8 + # The approval gate may spend up to six minutes waiting for peer checks; + # retain bounded time for API retries and review publication afterwards. + timeout-minutes: 10 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 }} @@ -3973,7 +3978,7 @@ jobs: CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }} 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 }} - APPROVAL_CHECK_WAIT_ATTEMPTS: "12" + APPROVAL_CHECK_WAIT_ATTEMPTS: "36" APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "10" CHECK_LOOKUP_RETRY_ATTEMPTS: "1" CHECK_LOOKUP_RETRY_SLEEP_SECONDS: "2" @@ -6247,8 +6252,8 @@ jobs: wait_for_peer_github_checks() { local output_file="$1" - local attempts="${APPROVAL_CHECK_WAIT_ATTEMPTS:-10}" - local sleep_seconds="${APPROVAL_CHECK_WAIT_SLEEP_SECONDS:-15}" + local attempts="${APPROVAL_CHECK_WAIT_ATTEMPTS:-36}" + local sleep_seconds="${APPROVAL_CHECK_WAIT_SLEEP_SECONDS:-10}" local attempt=1 while [ "$attempt" -le "$attempts" ]; do diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 3e1180780..8646bb725 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -654,12 +654,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'load_selected_review_output()' "opencode approval step has a direct selected-output fallback when the overview comment is stale or invalid" assert_file_contains "$workflow_file" "gate result from Review Overview comment" "opencode approval step distinguishes overview-comment gate results" assert_file_contains "$workflow_file" "gate result from selected OpenCode output" "opencode approval step can recover from an invalid overview by validating the selected successful output" - assert_file_contains "$workflow_file" 'timeout-minutes: 8' "opencode approval step has a short bounded wall-clock timeout" + assert_file_contains "$workflow_file" 'timeout-minutes: 10' "opencode approval step has a bounded wall-clock timeout that covers slow peer checks" assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "120"' "opencode publish-stage diagnosis is a short best-effort augmentation" assert_file_not_contains "$workflow_file" "rekick_model_pool_on_exhaustion" "opencode publication must not rerun the exhausted model catalog after the model-pool step" assert_file_contains "$workflow_file" "publish stage performs no duplicate model-catalog pass" "opencode publication logs that exhausted model retries are delegated to the scheduler" assert_file_contains "$workflow_file" 'timeout --kill-after=15s "${OPENCODE_EXPORT_TIMEOUT_SECONDS:-120}s"' "opencode failed-check diagnosis bounds export so the publication gate cannot hang silently" - assert_file_contains "$workflow_file" 'APPROVAL_CHECK_WAIT_ATTEMPTS: "12"' "opencode approval gives slow peer checks a bounded hold window before scheduler retry" + assert_file_contains "$workflow_file" 'APPROVAL_CHECK_WAIT_ATTEMPTS: "36"' "opencode approval gives slow peer checks a bounded six-minute hold window before scheduler retry" assert_file_contains "$workflow_file" 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "10"' "opencode approval poll cadence keeps peer-check API volume bounded" assert_file_not_contains "$workflow_file" 'REVIEW_PUBLISH_STEP_TIMEOUT_SECONDS' "opencode review publication relies on the Actions step timeout instead of a background watchdog" assert_file_not_contains "$workflow_file" "PUBLISH_STEP_TIMEOUT" "opencode review publication does not leave orphaned watchdog processes" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 14e3e61f9..1907c8ebe 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -717,8 +717,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "OpenCode model pool exceeded the outer" in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,280}continue-on-error: true", workflow) - assert re.search(r"Publish OpenCode review outcome[\s\S]{0,900}timeout-minutes: 8", workflow) - assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "12"' in workflow + assert re.search(r"Publish central OpenCode fast approval[\s\S]{0,900}timeout-minutes: 8", workflow) + assert re.search(r"Publish OpenCode review outcome[\s\S]{0,900}timeout-minutes: 10", workflow) + assert workflow.count('APPROVAL_CHECK_WAIT_ATTEMPTS: "36"') == 2 assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "10"' in workflow assert 'CHECK_LOOKUP_GH_API_TIMEOUT_SECONDS: "15"' in workflow assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "120"' in workflow @@ -1285,3 +1286,37 @@ def test_opencode_review_thread_jq_filters_preserve_bash_single_quotes(): assert 'gsub("`"; "\'")' not in workflow assert workflow.count('gsub("`"; "'")') == 4 + + +def test_peer_check_wait_budget_fits_publication_step_timeouts(): + """Keep slow-check cadence bounded inside both publication step caps.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text( + encoding="utf-8" + ) + + attempts = [ + int(value) + for value in re.findall(r'APPROVAL_CHECK_WAIT_ATTEMPTS: "(\d+)"', workflow) + ] + sleeps = [ + int(value) + for value in re.findall( + r'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "(\d+)"', workflow + ) + ] + fast_timeout = re.search( + r"Publish central OpenCode fast approval[\s\S]{0,900}timeout-minutes: (\d+)", + workflow, + ) + publish_timeout = re.search( + r"Publish OpenCode review outcome[\s\S]{0,900}timeout-minutes: (\d+)", + workflow, + ) + + assert attempts == [36, 36] + assert sleeps == [10, 10] + assert fast_timeout is not None + assert publish_timeout is not None + wait_seconds = (attempts[0] - 1) * sleeps[0] + assert int(fast_timeout.group(1)) * 60 - wait_seconds >= 120 + assert int(publish_timeout.group(1)) * 60 - wait_seconds >= 240 From 2033838aef34a882ca1ee4fc99de0876bc4d3a59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 13 Jul 2026 17:44:43 +0900 Subject: [PATCH 2/2] fix(opencode): break metadata gate review cycle --- .github/workflows/opencode-review.yml | 4 ++-- scripts/ci/collect_failed_check_evidence.sh | 5 ++++- scripts/ci/test_strix_quick_gate.sh | 6 +++--- tests/test_opencode_agent_contract.py | 11 +++++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index a8c939411..384db4610 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -6016,7 +6016,7 @@ jobs: | select((.workflow // "") != "Required OpenCode Review") | select((.workflow // "") != "OpenCode PR Review") | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) - | select(((.name // "") == "metadata-only gate evaluation" and (.workflow // "") == "PR Governance") | not) + | select((.name // "") != "metadata-only gate evaluation") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.isRequired // false) | not) and (.workflow // "") == "CodeQL") | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) @@ -6143,7 +6143,7 @@ jobs: | select((.workflow // "") != "OpenCode Review") | select((.workflow // "") != "Required OpenCode Review") | select((.workflow // "") != "OpenCode PR Review") - | select(((.name // "") == "metadata-only gate evaluation" and (.workflow // "") == "PR Governance") | not) + | select((.name // "") != "metadata-only gate evaluation") | select((.status // "") != "COMPLETED") | "- " + (.label // "check") + ": " + (.status // "unknown") + (if (.detailsUrl // "") != "" then " (" + .detailsUrl + ")" else "" end) elif .kind == "status" then diff --git a/scripts/ci/collect_failed_check_evidence.sh b/scripts/ci/collect_failed_check_evidence.sh index 5a7e79ed4..d46ce9e11 100755 --- a/scripts/ci/collect_failed_check_evidence.sh +++ b/scripts/ci/collect_failed_check_evidence.sh @@ -520,7 +520,10 @@ gh api graphql \ if .__typename == "CheckRun" then select((.status // "") == "COMPLETED") | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) - | select(((.name // "") == "metadata-only gate evaluation" and (.checkSuite.workflowRun.workflow.name // "") == "PR Governance") | not) + # The metadata-only gate is derived from the OpenCode review state. + # GitHub can misattribute its workflow name (for example, as CodeQL), + # so its stable check name is the only safe cycle-breaking key. + | select((.name // "") != "metadata-only gate evaluation") | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.isRequired // false) | not) and (.checkSuite.workflowRun.workflow.name // "") == "CodeQL") | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue" and ((.checkSuite.workflowRun.workflow.name // "") == "PR Review Merge Scheduler" or (.checkSuite.workflowRun.workflow.name // "") == "Required PR Review Merge Scheduler")) | not) | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("${{"))) | not) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 3dd9c7489..7325c4fda 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -897,14 +897,14 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select((.workflowName // "") == "Strix Security Scan" or (.workflowName // "") == "Strix")' "failed-check evidence only appends Strix workflow runs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'group_by(.__context_key)' "failed-check evidence groups manual Strix statuses by context before accepting superseding success" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'map(last)' "failed-check evidence accepts only the latest status per context" - assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select(((.name // "") == "metadata-only gate evaluation" and (.checkSuite.workflowRun.workflow.name // "") == "PR Governance") | not)' "failed-check evidence ignores metadata-only PR Governance review-state gates" + assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'select((.name // "") != "metadata-only gate evaluation")' "failed-check evidence ignores metadata-only review-state gates even when GitHub misattributes their workflow" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" 'isRequired(pullRequestId: $prId)' "failed-check evidence reads PR-required status for check runs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '((.isRequired // false) | not) and (.checkSuite.workflowRun.workflow.name // "") == "CodeQL"' "failed-check evidence ignores non-required cancelled CodeQL checks without logs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '(.name // "") == "scan-pr-queue" and ((.checkSuite.workflowRun.workflow.name // "") == "PR Review Merge Scheduler" or (.checkSuite.workflowRun.workflow.name // "") == "Required PR Review Merge Scheduler")' "failed-check evidence ignores cancelled scheduler queue replacement checks" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '((.name // "") | contains("${{"))' "failed-check evidence ignores cancelled matrix-template helper checks without logs" assert_file_contains "$REPO_ROOT/scripts/ci/collect_failed_check_evidence.sh" '(.name // "") == "noema-review"' "failed-check evidence ignores cancelled Noema queue replacement checks without source logs" - assert_file_contains "$workflow_file" 'select(((.name // "") == "metadata-only gate evaluation" and (.workflow // "") == "PR Governance") | not)' "opencode failed-check collection ignores metadata-only PR Governance review-state gates" - metadata_gate_filter_count="$(grep -Fc 'select(((.name // "") == "metadata-only gate evaluation" and (.workflow // "") == "PR Governance") | not)' "$workflow_file")" + assert_file_contains "$workflow_file" 'select((.name // "") != "metadata-only gate evaluation")' "opencode failed-check collection ignores metadata-only review-state gates regardless of workflow attribution" + metadata_gate_filter_count="$(grep -Fc 'select((.name // "") != "metadata-only gate evaluation")' "$workflow_file")" if [ "$metadata_gate_filter_count" -lt 2 ]; then fail "opencode failed- and pending-check collection both ignore metadata-only PR Governance review-state gates (found ${metadata_gate_filter_count}, expected at least 2)" fi diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 1907c8ebe..ea264c601 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -833,6 +833,17 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert re.search(r'check-runs" \\\n\s+-f per_page=100 \\\n\s+--paginate \\\n\s+--slurp \|\n\s+jq -r "\$jq_filter"', workflow) assert not re.search(r"--slurp\s*\\\n\s*--jq", workflow) assert workflow.count('["opencode-review","coverage-evidence","metadata-only gate evaluation"]') >= 2 + metadata_gate_filter = 'select((.name // "") != "metadata-only gate evaluation")' + assert workflow.count(metadata_gate_filter) >= 2 + failed_check_collector = Path( + "scripts/ci/collect_failed_check_evidence.sh" + ).read_text(encoding="utf-8") + assert metadata_gate_filter in failed_check_collector + assert ( + '(.name // "") == "metadata-only gate evaluation" and ' + '(.checkSuite.workflowRun.workflow.name // "") == "PR Governance"' + not in failed_check_collector + ) assert "falling back to current-head REST check-runs" in workflow strix_workflow = Path(".github/workflows/strix.yml").read_text(encoding="utf-8")