feat(model): allow select() and friends to start a query-builder chain (#3346) #391
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Wheels Bot — Reviewer | |
| # Single-pass reviewer. The former Reviewer A / Reviewer B critique loop | |
| # (bot-review-b.yml + /review-the-review + /respond-to-critique) was retired | |
| # per maintainer decision 2026-06-11: the loop was expensive and flaky, and | |
| # its marginal catch rate no longer justified a second model pass. Reviewer | |
| # B's anti-sycophancy / false-positive mandate now lives inside /review-pr | |
| # itself as an explicit self-adversarial step before posting. | |
| # | |
| # One trigger path: pull_request (PR opens, syncs, or marks ready). The | |
| # Reviewer submits its single substantive review of the diff via /review-pr. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, ready_for_review] | |
| branches: [develop] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| # The group key retains the legacy "review-a" name: it is shared with | |
| # bot-review-fork.yml so a fork review and an internal review for the same | |
| # PR number can never overlap, and renaming it buys nothing. A PR is either | |
| # fork or internal, so in practice only one of the two workflows matches. | |
| group: wheels-bot-review-a-${{ github.event.pull_request.number }} | |
| cancel-in-progress: false | |
| jobs: | |
| review: | |
| name: Reviewer | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Reviews bot PRs (even draft) and human ready-for-review PRs. | |
| if: | | |
| vars.WHEELS_BOT_ENABLED == 'true' | |
| && github.event.pull_request.user.login != 'dependabot[bot]' | |
| && (github.event.pull_request.user.login == 'wheels-bot[bot]' | |
| || github.event.pull_request.draft == false) | |
| steps: | |
| - name: Generate App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.WHEELS_BOT_APP_ID }} | |
| private-key: ${{ secrets.WHEELS_BOT_PRIVATE_KEY }} | |
| - name: Resolve PR info | |
| id: pr | |
| env: | |
| # Pass event values through env (never interpolate ${{ }} straight | |
| # into the script body). head.sha is captured exactly ONCE here, at | |
| # run start — it is both what the Checkout step pins and the marker | |
| # SHA threaded into the prompt (issue #2848). | |
| PR_NUM: ${{ github.event.pull_request.number }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then | |
| echo "::error::PR number is not numeric: $PR_NUM" | |
| exit 1 | |
| fi | |
| if ! [[ "$HEAD_SHA" =~ ^[0-9a-fA-F]{7,40}$ ]]; then | |
| echo "::error::head SHA is not a hex commit id: $HEAD_SHA" | |
| exit 1 | |
| fi | |
| echo "pr_num=${PR_NUM}" >> "$GITHUB_OUTPUT" | |
| echo "sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT" | |
| - name: Checkout PR head | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ steps.pr.outputs.sha }} | |
| fetch-depth: 0 | |
| - name: Skip check | |
| id: gate | |
| uses: ./.github/actions/wheels-bot-skip-check | |
| with: | |
| target-type: pr | |
| target-number: ${{ steps.pr.outputs.pr_num }} | |
| # No trailing colon: the review marker is | |
| # `wheels-bot:review-a:<pr>:<sha>` with no suffix. Adding a trailing | |
| # `:` made this a no-op gate (issue #2558). The marker keeps its | |
| # legacy `review-a` name so reviews posted before the single-reviewer | |
| # consolidation still satisfy idempotency on already-reviewed SHAs. | |
| marker-pattern: 'wheels-bot:review-a:${{ steps.pr.outputs.pr_num }}:${{ steps.pr.outputs.sha }}' | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| - name: Run Reviewer | |
| if: steps.gate.outputs.skip == 'false' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| # Allows the App's bot identity (and github-actions[bot] for | |
| # consistency). The Reviewer runs on the bot's own PRs — a | |
| # legitimately bot-driven trigger. | |
| allowed_bots: 'wheels-bot[bot],github-actions[bot]' | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| github_token: ${{ steps.app-token.outputs.token }} | |
| # Thread the SHA resolved in "Resolve PR info" — the exact commit the | |
| # Checkout step pinned — into the prompt as a second argument. The | |
| # model emits the idempotency marker from this value instead of | |
| # re-deriving it with `gh pr view`, which races with pushes that land | |
| # mid-session and left the marker pointing at the wrong commit | |
| # (issue #2848). The Run Reviewer step's Bash allowlist is gh + | |
| # read-only git (no echo/printenv), so the model can't read a step | |
| # env var — the SHA must travel in the prompt text, the same channel | |
| # the PR number already uses. | |
| prompt: | | |
| /review-pr ${{ steps.pr.outputs.pr_num }} ${{ steps.pr.outputs.sha }} | |
| # Model policy: judging gate = opus (was Fable 5 until its 2026-06 deactivation), coding stages = opus, janitorial = sonnet. | |
| claude_args: | | |
| --model claude-opus-4-8 | |
| --max-turns 250 | |
| --allowedTools "Bash(gh:*),Bash(git log:*),Bash(git diff:*),Bash(git show:*),Bash(git grep:*),Bash(git status),Read,Grep,Glob" | |
| # Post-submission guard (issue #2558). The Reviewer is trusted to issue a | |
| # single, substantive `gh pr review` per session. When the model misbehaves | |
| # — e.g. probing the CLI with `--body "test body"` before issuing the | |
| # real review — the placeholder leaks out as a public review. This step | |
| # scans wheels-bot reviews on the current SHA after the model exits and | |
| # auto-dismisses any that look like probes (too short, or missing the | |
| # canonical `wheels-bot:review-a` marker). Runs on `always()` so it still | |
| # fires when the Claude step itself failed mid-session. | |
| - name: Validate Reviewer output | |
| if: always() && steps.gate.outputs.skip == 'false' | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| PR_NUMBER: ${{ steps.pr.outputs.pr_num }} | |
| HEAD_SHA: ${{ steps.pr.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| # Allow-list APPROVED and CHANGES_REQUESTED only: those are the | |
| # exclusive set of states that (a) GitHub's dismiss API accepts and | |
| # (b) gate merging. Feeding the dismiss API any other state returns | |
| # HTTP 422 — e.g. "Can not dismiss a commented pull request review" | |
| # for COMMENTED, the same shape for PENDING — which would fail this | |
| # step and red-X the check. Originally observed on PR #2795 commit | |
| # 0db188a5 when a COMMENTED placeholder leaked through. | |
| reviews=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \ | |
| | jq -c --arg sha "$HEAD_SHA" \ | |
| '[.[] | select(.user.login == "wheels-bot[bot]") | select(.commit_id == $sha) | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED")]') | |
| count=$(echo "$reviews" | jq 'length') | |
| if [[ "$count" == "0" ]]; then | |
| echo "::notice::No active wheels-bot reviews on ${HEAD_SHA} to validate" | |
| exit 0 | |
| fi | |
| # Dismissal criteria: body must be (a) >= 200 chars AND (b) contain | |
| # `wheels-bot:review-a` as a substring. Both conditions are required — | |
| # a long body without the marker still gets dismissed, because a | |
| # marker-less review breaks downstream idempotency (the skip-check | |
| # action greps for the marker on subsequent runs) and is itself a | |
| # signal that the prompt was not followed. Belt-and-suspenders: the | |
| # guard treats prompt compliance as load-bearing, not advisory. | |
| dismissed=0 | |
| while IFS= read -r row; do | |
| id=$(echo "$row" | jq -r '.id') | |
| body=$(echo "$row" | jq -r '.body') | |
| body_len=${#body} | |
| if [[ "$body_len" -lt 200 ]] || ! grep -q 'wheels-bot:review-a' <<<"$body"; then | |
| echo "::warning::Dismissing bogus Reviewer review id=${id} len=${body_len}" | |
| gh api -X PUT \ | |
| "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews/${id}/dismissals" \ | |
| -f message="Auto-dismissed by the Reviewer guard: body is shorter than 200 characters or missing the canonical \`wheels-bot:review-a\` marker. See wheels-dev/wheels#2558 for context." | |
| dismissed=$((dismissed + 1)) | |
| fi | |
| done < <(echo "$reviews" | jq -c '.[]') | |
| if [[ "$dismissed" -gt 0 ]]; then | |
| # Idempotency check: a manual workflow re-trigger combined with | |
| # new bogus reviews on the same SHA could otherwise produce | |
| # duplicate guard comments. Skip if a guard comment for this | |
| # exact PR + SHA already exists. | |
| guard_marker="wheels-bot:review-a-guard:${PR_NUMBER}:${HEAD_SHA}" | |
| existing=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \ | |
| | jq -r --arg m "$guard_marker" '[.[] | select(.body | contains($m))] | length') | |
| if [[ "$existing" == "0" ]]; then | |
| short_sha=${HEAD_SHA:0:7} | |
| gh pr comment "$PR_NUMBER" --body "## Wheels Bot — Reviewer guard | |
| Detected and dismissed ${dismissed} bogus Reviewer review(s) on commit \`${short_sha}\`. Cause: review body shorter than 200 characters or missing the canonical \`wheels-bot:review-a\` marker. See [wheels-dev/wheels#2558](https://github.com/wheels-dev/wheels/issues/2558) for context. | |
| <!-- ${guard_marker} -->" | |
| else | |
| echo "::notice::Guard comment already present for ${PR_NUMBER}@${HEAD_SHA}; skipping duplicate" | |
| fi | |
| fi |