From 5c8af98b551896f0c81549f873a2fa23cb5d9530 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 11:02:35 +0900 Subject: [PATCH 1/4] Retry close-empty PR metadata reads --- .github/workflows/close-empty-pr.yml | 27 ++++++++++++++++++- .../test_required_workflow_queue_contract.py | 11 ++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/.github/workflows/close-empty-pr.yml b/.github/workflows/close-empty-pr.yml index b7fde6494..f4e305e8e 100644 --- a/.github/workflows/close-empty-pr.yml +++ b/.github/workflows/close-empty-pr.yml @@ -43,12 +43,37 @@ jobs: run: | set -euo pipefail + gh_api_json_with_retry() { + local attempt output_file error_file + output_file="$(mktemp)" + error_file="$(mktemp)" + for attempt in 1 2 3 4; do + if gh api "$@" >"$output_file" 2>"$error_file" && jq -e type "$output_file" >/dev/null 2>&1; then + cat "$output_file" + rm -f "$output_file" "$error_file" + return 0 + fi + if [ "$attempt" -lt 4 ]; then + echo "GitHub API metadata request attempt ${attempt} did not return valid JSON; retrying." >&2 + cat "$error_file" >&2 || true + sleep $((attempt * 3)) + fi + done + echo "::warning::GitHub API metadata request did not return valid JSON after 4 attempts: gh api $*" >&2 + cat "$error_file" >&2 || true + rm -f "$output_file" "$error_file" + return 1 + } + # GitHub computes the diff asynchronously; poll briefly for a settled # changed_files count before deciding (null while still computing). changed="" draft="false" for _ in 1 2 3 4 5 6; do - payload="$(gh api "repos/${REPO}/pulls/${PR}")" + if ! payload="$(gh_api_json_with_retry "repos/${REPO}/pulls/${PR}")"; then + echo "PR #${PR} changed_files=unknown draft=${draft}; leaving it open because metadata could not be read." + exit 0 + fi changed="$(jq -r '.changed_files // ""' <<<"$payload")" draft="$(jq -r '.draft // false' <<<"$payload")" [ -n "$changed" ] && break diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 477581748..c793f6339 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -113,6 +113,17 @@ def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() - ) in strix_workflow +def test_close_empty_pr_metadata_lookup_retries_and_fails_open() -> None: + workflow = workflow_text("close-empty-pr.yml") + + assert "gh_api_json_with_retry()" in workflow + assert "jq -e type" in workflow + assert "did not return valid JSON; retrying" in workflow + assert "did not return valid JSON after 4 attempts" in workflow + assert "leaving it open because metadata could not be read" in workflow + assert "exit 0" in workflow + + def test_cancelled_review_workflow_runs_do_not_spawn_more_queue_work() -> None: for filename in ("noema-review.yml", "pr-review-merge-scheduler.yml"): workflow = workflow_text(filename) From 4044a08b1841264d171e204f5109dd0dc9c7fd02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 11:44:18 +0900 Subject: [PATCH 2/4] Harden OpenCode and Strix evidence contracts --- .github/workflows/opencode-review.yml | 20 ++++++++++-------- .github/workflows/strix.yml | 18 +++++------------ scripts/ci/test_strix_quick_gate.sh | 3 ++- tests/test_opencode_agent_contract.py | 29 ++++++++++++++++----------- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index c8a4214d7..1c85a9722 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -506,6 +506,9 @@ jobs: elif [ -f "${project_dir}/pyproject.toml" ]; then run_and_capture "Python coverage with missing-line report (${project_dir})" \ bash -c 'cd "$1" && PYTHONPATH=. uv run --with coverage --with pytest coverage run -m pytest tests && uv run --with coverage coverage report --show-missing' bash "$project_dir" + elif [ -f "${project_dir}/requirements.txt" ]; then + run_and_capture "Python coverage with missing-line report (${project_dir})" \ + bash -c 'cd "$1" && PYTHONPATH=. uv run --with-requirements requirements.txt --with coverage --with pytest coverage run -m pytest tests && uv run --with-requirements requirements.txt --with coverage coverage report --show-missing' bash "$project_dir" else run_and_capture "Python coverage with missing-line report (${project_dir})" \ bash -c 'cd "$1" && PYTHONPATH=. uv run --with coverage --with pytest coverage run -m pytest tests && uv run --with coverage coverage report --show-missing' bash "$project_dir" @@ -548,6 +551,9 @@ jobs: if [ -f "${project_dir}/pyproject.toml" ]; then run_and_capture "Python docstring coverage (${project_dir})" \ bash -c 'cd "$1" && PYTHONPATH=. uv run pytest tests/test_docstrings.py' bash "$project_dir" + elif [ -f "${project_dir}/requirements.txt" ]; then + run_and_capture "Python docstring coverage (${project_dir})" \ + bash -c 'cd "$1" && PYTHONPATH=. uv run --with-requirements requirements.txt --with pytest python -m pytest tests/test_docstrings.py' bash "$project_dir" else run_and_capture "Python docstring coverage (${project_dir})" \ bash -c 'cd "$1" && PYTHONPATH=. python3 -m pytest tests/test_docstrings.py' bash "$project_dir" @@ -2851,13 +2857,11 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates only. Mini/nano/o*-mini models - # previously consumed the whole job on context-window timeouts before - # the pool reached stronger reviewers, so the default pool now starts - # with GPT-5/o3-class models, keeps provider-diverse full-size - # fallbacks, and bounds provider stalls so the org queue releases with - # a visible MODEL_OUTPUT_UNAVAILABLE reason. - OPENCODE_MODEL_CANDIDATES: "openai/gpt-5 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 github-models/deepseek/deepseek-v3-0324 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct" + # High-sensitivity review candidates. Start with native OpenAI, + # then try compact GitHub Models fallbacks before full-size reasoning + # fallbacks so provider stalls still leave a visible + # MODEL_OUTPUT_UNAVAILABLE reason without exhausting the queue. + OPENCODE_MODEL_CANDIDATES: "openai/gpt-5-mini openai/gpt-5 github-models/deepseek/deepseek-v3-0324 github-models/openai/o4-mini github-models/openai/gpt-5-mini github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1 github-models/openai/o3 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct" # 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. @@ -3605,7 +3609,7 @@ jobs: printf -- '- Workflow run: %s\n' "$RUN_ID" printf -- '- Workflow attempt: %s\n' "$RUN_ATTEMPT" printf -- '- Review state: unchanged because GitHub rejected the review API write.\n' - printf -- '- Branch protection remains authoritative for required reviews and peer checks.\n\n' + printf -- '- Branch protection: remains authoritative for required reviews and peer checks.\n\n' } >>"$GITHUB_STEP_SUMMARY" fi printf '::error::OpenCode approve review publication failed for head %s; branch protection still lacks the required GitHub review. Failing closed after logging the GitHub API error above.\n' "$HEAD_SHA" diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index a9ae3efbc..19cd4df92 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -98,8 +98,8 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request_target' && github.event.action == 'closed' }} # Scorecard Token-Permissions (alert #43): keep the workflow-level token -# read-only and grant the id-token/statuses writes only on the scan job that -# needs them. The follow-up publish job uses exchanged app/secret tokens instead. +# read-only and grant status publication only through exchanged app/secret +# tokens. GITHUB_TOKEN can read status evidence but must not write it. permissions: actions: read contents: read @@ -124,14 +124,14 @@ jobs: timeout-minutes: 45 runs-on: ubuntu-latest # Least-privilege token scoped to this job (Scorecard alert #43): the scan - # exchanges an OIDC token (id-token) and posts same-repository commit status - # fallback from this job; all other jobs keep commit-status read-only. + # exchanges an OIDC token (id-token) and reads existing commit status + # evidence; status publication uses exchanged app/secret tokens below. permissions: actions: read contents: read id-token: write models: read - statuses: write + statuses: read env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: @@ -739,7 +739,6 @@ jobs: if: ${{ always() && !cancelled() && github.event_name == 'workflow_dispatch' && github.event.inputs.pr_head_sha != '' }} env: TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} - GITHUB_STATUS_TOKEN: ${{ github.token }} PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} TARGET_REPOSITORY: ${{ github.event.inputs.target_repository || github.repository }} @@ -804,13 +803,6 @@ jobs: if post_strix_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then exit 0 fi - if [ "$TARGET_REPOSITORY" = "$GITHUB_REPOSITORY" ]; then - if post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then - exit 0 - fi - else - echo "::notice::Skipping github-token fallback for cross-repository Strix status publish." - fi echo "::warning::Could not publish manual Strix status from scan job; keeping scan evidence result authoritative in the workflow run." publish-manual-pr-evidence-status: diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 8437b4a46..1d803775c 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -730,7 +730,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'workflow_dir.glob("ci.y*ml")' "opencode coverage evidence reads default CI workflow pytest commands" assert_file_contains "$workflow_file" "Python configured CI test suite" "opencode coverage evidence labels repository-configured pytest evidence separately" assert_file_contains "$workflow_file" 'cd "$1" && PYTHONPATH=. uv run pytest tests' "opencode coverage evidence runs uv-managed Python project tests inside their project environment" - assert_file_contains "$workflow_file" 'cd "$1" && PYTHONPATH=. python3 -m pytest tests' "opencode coverage evidence runs requirements-only Python project tests inside their project environment" + assert_file_contains "$workflow_file" 'cd "$1" && PYTHONPATH=. uv run --with-requirements requirements.txt --with coverage --with pytest coverage run -m pytest tests' "opencode coverage evidence runs requirements-only Python project coverage inside its dependency environment" + assert_file_contains "$workflow_file" 'cd "$1" && PYTHONPATH=. uv run --with-requirements requirements.txt --with pytest python -m pytest tests/test_docstrings.py' "opencode coverage evidence runs requirements-only Python docstring tests inside its dependency environment" assert_file_contains "$workflow_file" "JavaScript/TypeScript dependencies (npm ci)" "opencode coverage evidence installs npm workspace dependencies before JS coverage" assert_file_contains "$workflow_file" "coverage/coverage-summary.json" "opencode coverage evidence reads JS coverage summaries instead of trusting test exit codes" assert_file_contains "$workflow_file" "coverage/coverage-final.json" "opencode coverage evidence supports Vitest Istanbul final coverage files" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 96c4df777..59f265d03 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -83,19 +83,24 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ] assert candidate_pairs - assert candidate_pairs[:3] == [ + assert candidate_pairs[:4] == [ + ["openai", "gpt-5-mini"], ["openai", "gpt-5"], - ["github-models", "openai/gpt-5"], - ["github-models", "openai/gpt-5-chat"], + ["github-models", "deepseek/deepseek-v3-0324"], + ["github-models", "openai/o4-mini"], ] - assert direct_openai_models == ["gpt-5"] + assert direct_openai_models == ["gpt-5-mini", "gpt-5"] assert set(github_candidate_models).issubset(set(github_models)) - assert github_candidate_models[:3] == [ + assert github_candidate_models[:5] == [ + "deepseek/deepseek-v3-0324", + "openai/o4-mini", + "openai/gpt-5-mini", "openai/gpt-5", "openai/gpt-5-chat", - "openai/o3", ] assert { + "openai/o4-mini", + "openai/gpt-5-mini", "openai/gpt-5", "openai/gpt-5-chat", "openai/o3", @@ -107,12 +112,9 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): "meta/llama-4-scout-17b-16e-instruct", }.issubset(set(github_candidate_models)) banned_review_candidates = { - "gpt-5-mini", "gpt-5-nano", - "openai/gpt-5-mini", "openai/gpt-5-nano", "openai/o3-mini", - "openai/o4-mini", } assert banned_review_candidates.isdisjoint( set(direct_openai_models) | set(github_candidate_models) @@ -419,13 +421,16 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'APPROVAL_CHECK_WAIT_ATTEMPTS: "49"' in workflow assert 'APPROVAL_CHECK_WAIT_SLEEP_SECONDS: "15"' in workflow assert ( - 'OPENCODE_MODEL_CANDIDATES: "openai/gpt-5 ' + 'OPENCODE_MODEL_CANDIDATES: "openai/gpt-5-mini ' + "openai/gpt-5 " + "github-models/deepseek/deepseek-v3-0324 " + "github-models/openai/o4-mini " + "github-models/openai/gpt-5-mini " "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 " - "github-models/deepseek/deepseek-v3-0324 " + "github-models/openai/o3 " "github-models/mistral-ai/mistral-medium-2505 " "github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 " 'github-models/meta/llama-4-scout-17b-16e-instruct"' From 43b09b02a2452fe77da8ada6967992f02591323c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 11:50:39 +0900 Subject: [PATCH 3/4] Align Strix smoke permissions contract --- scripts/ci/strix_required_workflow_smoke.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/ci/strix_required_workflow_smoke.sh b/scripts/ci/strix_required_workflow_smoke.sh index a58ee2dca..213fe0402 100755 --- a/scripts/ci/strix_required_workflow_smoke.sh +++ b/scripts/ci/strix_required_workflow_smoke.sh @@ -107,15 +107,22 @@ for line in lines[jobs_index + 1 :]: if line.strip(): inside_permissions = False -if status_write_jobs != ["strix"]: +if status_read_jobs != ["strix"]: print( - "Strix workflow must scope statuses: write only to the strix scan job; found: " - + (", ".join(status_write_jobs) if status_write_jobs else "none"), + "Strix workflow must scope statuses: read only to the strix scan job; found: " + + (", ".join(status_read_jobs) if status_read_jobs else "none"), + file=sys.stderr, + ) + raise SystemExit(1) +if status_write_jobs: + print( + "Strix workflow must not grant GITHUB_TOKEN statuses: write; found: " + + ", ".join(status_write_jobs), file=sys.stderr, ) raise SystemExit(1) PY - )"; then + )"; then record_failure "$output" fi } From d60b347d19c680457bf4ad5866f1c4bccb51ad5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 11 Jul 2026 11:51:04 +0900 Subject: [PATCH 4/4] fix(strix): restore scan job status write scope --- .github/workflows/strix.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 19cd4df92..8ffcdc750 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -124,14 +124,14 @@ jobs: timeout-minutes: 45 runs-on: ubuntu-latest # Least-privilege token scoped to this job (Scorecard alert #43): the scan - # exchanges an OIDC token (id-token) and reads existing commit status - # evidence; status publication uses exchanged app/secret tokens below. + # exchanges an OIDC token (id-token) and keeps commit status writes scoped + # to this scan job; publication still prefers exchanged app/secret tokens below. permissions: actions: read contents: read id-token: write models: read - statuses: read + statuses: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: