diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml deleted file mode 100644 index 2960430c..00000000 --- a/.github/workflows/gemini-review.yml +++ /dev/null @@ -1,211 +0,0 @@ -name: Gemini Review - -# Calls the multi-agent Code Review Council on every open PR to -# develop/main, posts a real PR review, and records the verdict on a -# `gemini-review` check_run so branch protection has something to gate -# on. The check name stays `gemini-review` for branch-protection -# compatibility, even though the council can route to either -# OpenRouter or Gemini under the hood. -# -# Provider routing (handled by .github/scripts/gemini_review.py): -# - `OPENROUTER_API_KEY` set → OpenRouter (multi-model gateway — -# deepseek/Claude/GPT/etc., switchable via `OPENROUTER_MODEL` var). -# - Otherwise → Gemini (free-tier fallback). -# -# Replaces the previous Copilot-based gate (see commit history / -# .github/workflows/copilot-review-required.yml). Picked because: -# - GitHub Copilot Code Review quota was exhausted. -# - OpenRouter unlocks models that catch bug classes Gemini misses -# (per Speecher's own gate experience) and lets us rotate models -# without redeploying the workflow. -# - Posting an *actual* review (not just a check_run) gives the -# dashboard inline comments instead of a black-box pass/fail. -# -# Setup required (one-time, see .github/scripts/gemini_review.py -# docstring for the full list): -# 1. Add `OPENROUTER_API_KEY` to repo secrets (preferred) and/or -# `GEMINI_API_KEY` (fallback). -# Settings → Secrets and variables → Actions → New repository secret. -# OpenRouter keys: https://openrouter.ai/keys -# Gemini keys: https://aistudio.google.com/apikey -# 2. Update branch protection on `develop` (and `main`): swap the -# required check `copilot-review` → `gemini-review`. The old -# copilot-review-required.yml workflow has been removed in the -# same PR that introduced this one. -# -# Notes: -# - We trigger only on `pull_request` events (no cron). Each push to -# a PR re-runs the workflow with the latest SHA — branch protection -# re-evaluates the check automatically. -# - We do NOT review PRs opened by bots (dependabot, renovate, etc.) -# — auto-pass them, same convention the prior workflow used. -# - Forks: PRs from forks get a read-only GITHUB_TOKEN, so posting -# reviews/checks fails with 403. The workflow handles that -# gracefully (the review step exits non-zero, the workflow's job -# conclusion fails, branch protection blocks the merge — which is -# the desired outcome for unmaintained-fork PRs anyway). - -on: - pull_request: - branches: [develop, main] - types: [opened, reopened, synchronize, ready_for_review] - workflow_dispatch: - inputs: - pr_number: - description: "PR number to (re)review" - required: true - -concurrency: - # Supersede in-flight runs when a new push lands on the same PR — no - # point reviewing an older SHA. - group: gemini-review-${{ github.event.pull_request.number || github.event.inputs.pr_number }} - cancel-in-progress: true - -jobs: - review: - runs-on: ubuntu-latest - # Gemini 3 Pro preview can exceed 10 minutes on multi-agent reviews - # even for small PRs; give the council enough room to post a verdict - # instead of burning quota and failing the required check on timeout. - timeout-minutes: 20 - permissions: - contents: read - pull-requests: write - checks: write - steps: - - name: Determine PR number - id: pr - run: | - if [ -n "${{ github.event.pull_request.number }}" ]; then - echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" - else - echo "number=${{ github.event.inputs.pr_number }}" >> "$GITHUB_OUTPUT" - fi - - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Classify low-risk PRs - id: low-risk - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.pr.outputs.number }} - run: | - set -euo pipefail - - files_output="$(mktemp)" - policy_output="$(mktemp)" - trap 'rm -f "$files_output" "$policy_output"' EXIT - if ! gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' > "$files_output"; then - echo "::warning::Could not fetch PR file list; running the full code review council." - echo "skip=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # Path policy: docs-only and lockfile-only PRs auto-pass the AI gate; - # workflow/config/review-script changes always run the council. - python3 .github/scripts/classify_ci_paths.py "$files_output" > "$policy_output" - skip="$(grep '^skip_ai_review=' "$policy_output" | cut -d= -f2-)" - reason="$(grep '^ai_review_reason=' "$policy_output" | cut -d= -f2-)" - echo "skip=${skip:-false}" >> "$GITHUB_OUTPUT" - echo "reason=${reason:-Code or workflow paths changed; running AI review.}" >> "$GITHUB_OUTPUT" - if [[ "${skip:-false}" == "true" ]]; then - echo "$reason" - fi - - - name: Check PR author (skip bots) - id: skip-bot - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.pr.outputs.number }} - run: | - # Use the REST PR endpoint, not `gh pr view` (which queries the - # GraphQL API and returns HTTP 401 on this runner even though the - # REST `gh api` calls in the steps above succeed with the same - # GITHUB_TOKEN). REST exposes `.user.type == "Bot"` instead of - # GraphQL's `.author.is_bot`. - author=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.user.login') - user_type=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.user.type') - # Bot authors get an auto-pass — Gemini doesn't add value on - # dependabot bumps and we'd burn quota for nothing. - if [[ "$user_type" == "Bot" ]] || [[ "$author" == *"[bot]"* ]] || [[ "$author" == app/dependabot ]] || [[ "$author" == "dependabot" ]] || [[ "$author" == "renovate"* ]]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "Bot author '$author' — auto-passing gate without invoking Gemini." - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Auto-pass low-risk review skips - if: steps.skip-bot.outputs.skip == 'true' || steps.low-risk.outputs.skip == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.pr.outputs.number }} - SKIP_REASON: ${{ steps.low-risk.outputs.reason || 'Bot author — review skipped' }} - run: | - head_sha=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha') - gh api "repos/$GITHUB_REPOSITORY/check-runs" \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -f name='gemini-review' \ - -f head_sha="$head_sha" \ - -f status='completed' \ - -f conclusion='success' \ - -f "output[title]=${SKIP_REASON}" \ - -f "output[summary]=${SKIP_REASON} Standard CI checks still gate the merge." - - - name: Set up Python - if: steps.skip-bot.outputs.skip != 'true' && steps.low-risk.outputs.skip != 'true' - uses: actions/setup-python@v7 - with: - python-version: "3.13" - - - name: Install code-review-council - if: steps.skip-bot.outputs.skip != 'true' && steps.low-risk.outputs.skip != 'true' - run: | - # Install the in-tree council package — pulls google-genai, - # httpx (OpenRouter route), and pydantic transitively via - # the package's pyproject deps. ``pip install -e`` so the - # script picks up the latest source on the PR's branch (the - # workflow file lives on develop but the script runs against - # the PR's checkout). - pip install --no-cache-dir -e ./packages/code-review-council - - - name: Run code review council - if: steps.skip-bot.outputs.skip != 'true' && steps.low-risk.outputs.skip != 'true' - env: - # Provider keys: OpenRouter preferred, Gemini fallback. The - # script picks based on which is non-empty (see - # ``_select_provider`` in gemini_review.py). Both are passed - # so the same workflow works whether the org standardized on - # OpenRouter or stayed on Gemini's free tier. - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.pr.outputs.number }} - # Model overrides — flip via repo / org / env vars without - # touching the script. Empty string ⇒ provider default. - OPENROUTER_MODEL: ${{ vars.OPENROUTER_MODEL || '' }} - GEMINI_MODEL: ${{ vars.GEMINI_MODEL || 'gemini-2.5-flash' }} - run: | - set +e - timeout 8m python .github/scripts/gemini_review.py - status=$? - set -e - - if [[ "$status" -eq 124 ]]; then - head_sha=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha') - gh api "repos/$GITHUB_REPOSITORY/check-runs" \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -f name='gemini-review' \ - -f head_sha="$head_sha" \ - -f status='completed' \ - -f conclusion='success' \ - -f 'output[title]=Council review timed out' \ - -f 'output[summary]=The external code-review council did not return within 8 minutes. Standard CI checks still gate this PR; rerun the workflow when a human AI review is required.' - exit 0 - fi - - exit "$status"