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
27 changes: 26 additions & 1 deletion .github/workflows/close-empty-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down
16 changes: 4 additions & 12 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -124,8 +124,8 @@ 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 keeps commit status writes scoped
# to this scan job; publication still prefers exchanged app/secret tokens below.
permissions:
actions: read
contents: read
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 11 additions & 4 deletions scripts/ci/strix_required_workflow_smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
29 changes: 17 additions & 12 deletions tests/test_opencode_agent_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
Expand Down Expand Up @@ -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"'
Expand Down
11 changes: 11 additions & 0 deletions tests/test_required_workflow_queue_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading