diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index b0a6bf12d..613ee5c56 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3660,10 +3660,9 @@ jobs: && steps.opencode_review_model_pool.outputs.review_status == 'success' && steps.central_review_process_fallback_scope.outputs.eligible == 'true' continue-on-error: true - # 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 + # Keep the normal peer-check hold short, but leave bounded room for the + # dynamic image-validation extension and review publication overhead. + timeout-minutes: 14 env: GH_TOKEN: ${{ steps.opencode_app_token.outputs.token }} CHECK_LOOKUP_GH_TOKEN: ${{ github.token }} @@ -3683,6 +3682,7 @@ jobs: 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: "36" + APPROVAL_SLOW_IMAGE_CHECK_WAIT_ATTEMPTS: "60" APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "10" REVIEW_PUBLISH_GH_API_TIMEOUT_SECONDS: "15" run: | @@ -3770,7 +3770,10 @@ jobs: check_runs_file="$(mktemp)" pending_checks_file="$(mktemp)" failed_checks_file="$(mktemp)" - for attempt in $(seq 1 "${APPROVAL_CHECK_WAIT_ATTEMPTS:-36}"); do + attempts="${APPROVAL_CHECK_WAIT_ATTEMPTS:-36}" + slow_image_attempts="${APPROVAL_SLOW_IMAGE_CHECK_WAIT_ATTEMPTS:-60}" + attempt=1 + while [ "$attempt" -le "$attempts" ]; 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 @@ -3780,11 +3783,17 @@ jobs: if [ ! -s "$pending_checks_file" ]; then break fi - 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}" + if [ "$attempts" -lt "$slow_image_attempts" ] && + grep -Eiq -- '^- validate [^:/]+ image:' "$pending_checks_file"; then + printf '::notice::Extending central fast approval peer-check wait from %s to %s attempts because current-head image validation is still running.\n' "$attempts" "$slow_image_attempts" + attempts="$slow_image_attempts" + fi + if [ "$attempt" -lt "$attempts" ]; then + printf 'Central fast approval waiting for peer checks (%s/%s):\n' "$attempt" "$attempts" cat "$pending_checks_file" sleep "${APPROVAL_CHECK_WAIT_SLEEP_SECONDS:-10}" fi + attempt=$((attempt + 1)) done if [ -s "$pending_checks_file" ]; then echo "::error::CENTRAL_FAST_APPROVAL_WAITING_FOR_CHECKS: peer GitHub Checks remained pending for current head ${HEAD_SHA}." @@ -3943,9 +3952,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. - # 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 + # The approval gate normally waits about six minutes, with a bounded + # extension for image validation plus API and publication overhead. + timeout-minutes: 16 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 }} @@ -3989,6 +3998,7 @@ jobs: 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: "36" + APPROVAL_SLOW_IMAGE_CHECK_WAIT_ATTEMPTS: "60" APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "10" CHECK_LOOKUP_RETRY_ATTEMPTS: "1" CHECK_LOOKUP_RETRY_SLEEP_SECONDS: "2" @@ -6263,6 +6273,7 @@ jobs: wait_for_peer_github_checks() { local output_file="$1" local attempts="${APPROVAL_CHECK_WAIT_ATTEMPTS:-36}" + local slow_image_attempts="${APPROVAL_SLOW_IMAGE_CHECK_WAIT_ATTEMPTS:-60}" local sleep_seconds="${APPROVAL_CHECK_WAIT_SLEEP_SECONDS:-10}" local attempt=1 @@ -6273,6 +6284,11 @@ jobs: if [ ! -s "$output_file" ]; then return 0 fi + if [ "$attempts" -lt "$slow_image_attempts" ] && + grep -Eiq -- '^- (Build and Publish Docker Images/)?validate [^:/]+ image:' "$output_file"; then + printf '::notice::Extending OpenCode peer-check wait from %s to %s attempts because current-head image validation is still running.\n' "$attempts" "$slow_image_attempts" + attempts="$slow_image_attempts" + fi if [ "$attempt" -lt "$attempts" ]; then printf 'Waiting for peer GitHub Checks before OpenCode approval (%s/%s):\n' "$attempt" "$attempts" cat "$output_file" diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 47626335e..7e2dac46f 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -43,7 +43,7 @@ The central `.github/workflows/opencode-review.yml` is now part of the active or - 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 - Fork posture: PR heads are fetched through `refs/pull//head` when direct head-SHA fetch is not available, so review can inspect fork PR source as data without executing it in the trusted workflow context -- Runtime posture: pre-model failed-check evidence waits are capped at about five minutes; the later approval gate still rechecks current-head peer checks before approving +- Runtime posture: pre-model failed-check evidence waits are capped at about five minutes; the later approval gate rechecks current-head peer checks and extends its bounded wait only while image-validation checks remain pending, logging the reason before approval Keep the OpenCode required workflow active only while the central workflow keeps proving current-head coverage, CodeGraph initialization, bounded evidence, model review output, and approval-gate publication on the current head. diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 5bb5fc7c1..4acb8edfd 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -582,7 +582,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'timeout-minutes: 90' "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: 65' "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" 'timeout-minutes: 14' "opencode fast approval publication is bounded around the dynamic image-check wait" 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 bounded stalled provider attempt" assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600"' "opencode model pool exits before the step timeout so the approval gate can publish a reason" @@ -658,13 +658,15 @@ 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: 10' "opencode approval step has a bounded wall-clock timeout that covers slow peer checks" + assert_file_contains "$workflow_file" 'timeout-minutes: 16' "opencode approval step has a bounded wall-clock timeout that covers dynamically extended image 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: "36"' "opencode approval gives slow peer checks a bounded six-minute hold window before scheduler retry" + assert_file_contains "$workflow_file" 'APPROVAL_SLOW_IMAGE_CHECK_WAIT_ATTEMPTS: "60"' "opencode approval dynamically extends its bounded hold only for current-head image validation" assert_file_contains "$workflow_file" 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "10"' "opencode approval poll cadence keeps peer-check API volume bounded" + assert_file_contains "$workflow_file" "current-head image validation is still running" "opencode approval logs why the peer-check wait budget was dynamically extended" 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" assert_file_not_contains "$workflow_file" "OPENCODE_PUBLISH_TIMEOUT_WRAPPED" "opencode review publication does not re-exec the runner shell script" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 92bf9f4f5..d3bf9b546 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -727,10 +727,12 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "OpenCode model pool exceeded the outer" in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "0"' 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 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 re.search(r"Publish central OpenCode fast approval[\s\S]{0,900}timeout-minutes: 14", workflow) + assert re.search(r"Publish OpenCode review outcome[\s\S]{0,900}timeout-minutes: 16", workflow) assert workflow.count('APPROVAL_CHECK_WAIT_ATTEMPTS: "36"') == 2 + assert workflow.count('APPROVAL_SLOW_IMAGE_CHECK_WAIT_ATTEMPTS: "60"') == 2 assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "10"' in workflow + assert workflow.count("current-head image validation is still running") == 2 assert 'CHECK_LOOKUP_GH_API_TIMEOUT_SECONDS: "15"' in workflow assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "120"' in workflow assert "Skipping publish-step failed-check OpenCode diagnosis for central review-process self-repair" in workflow @@ -1375,10 +1377,16 @@ def test_peer_check_wait_budget_fits_publication_step_timeouts(): encoding="utf-8" ) - attempts = [ + normal_attempts = [ int(value) for value in re.findall(r'APPROVAL_CHECK_WAIT_ATTEMPTS: "(\d+)"', workflow) ] + slow_image_attempts = [ + int(value) + for value in re.findall( + r'APPROVAL_SLOW_IMAGE_CHECK_WAIT_ATTEMPTS: "(\d+)"', workflow + ) + ] sleeps = [ int(value) for value in re.findall( @@ -1394,10 +1402,50 @@ def test_peer_check_wait_budget_fits_publication_step_timeouts(): workflow, ) - assert attempts == [36, 36] + assert normal_attempts == [36, 36] + assert slow_image_attempts == [60, 60] assert sleeps == [10, 10] assert fast_timeout is not None assert publish_timeout is not None - wait_seconds = (attempts[0] - 1) * sleeps[0] + wait_seconds = (slow_image_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 + + +def test_slow_peer_wait_matches_only_image_validation_checks(): + """Reject lookalike labels when selecting the extended peer-check budget.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text( + encoding="utf-8" + ) + fast_pattern = r"^- validate [^:/]+ image:" + general_pattern = r"^- (Build and Publish Docker Images/)?validate [^:/]+ image:" + + assert workflow.count(f"grep -Eiq -- '{fast_pattern}'") == 1 + assert workflow.count(f"grep -Eiq -- '{general_pattern}'") == 1 + + probes = ( + ("- validate naruon image: in_progress\n", True, True), + ( + "- Build and Publish Docker Images/validate frontend image: IN_PROGRESS\n", + False, + True, + ), + ("- invalidate naruon image: in_progress\n", False, False), + ("- validate security/image: in_progress\n", False, False), + ("- docs image validation: in_progress\n", False, False), + ) + for candidate, fast_expected, general_expected in probes: + fast_match = subprocess.run( + ["grep", "-Eiq", "--", fast_pattern], + input=candidate, + text=True, + check=False, + ).returncode == 0 + general_match = subprocess.run( + ["grep", "-Eiq", "--", general_pattern], + input=candidate, + text=True, + check=False, + ).returncode == 0 + assert fast_match is fast_expected, candidate + assert general_match is general_expected, candidate