Wheels 2.x > 4.x Performance Comparison #2066
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 — Propose Fix | |
| on: | |
| issue_comment: | |
| types: [created] | |
| workflow_dispatch: | |
| inputs: | |
| issue-number: | |
| description: 'Issue number to propose a fix for' | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: wheels-bot-propose-fix-${{ github.event.issue.number || inputs.issue-number }} | |
| cancel-in-progress: false | |
| jobs: | |
| propose-fix: | |
| name: Propose fix | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| # Phase 4 (auto-fire): runs from wheels-bot[bot] comments containing a | |
| # high- OR medium-confidence triage marker (bug path) or research marker | |
| # (framework-design path). Low-confidence stays manual. | |
| # Medium fires because propose-fix's own step-4 safety net (sensitive | |
| # areas: security / migrations / deploy / DI / cross-engine) aborts | |
| # before any PR is opened — the Reviewer catches the rest. | |
| # workflow_dispatch is preserved for manual reruns. | |
| # The bot-identity check is load-bearing: prevents humans from | |
| # triggering propose-fix by quoting a marker in a comment reply. | |
| if: | | |
| vars.WHEELS_BOT_ENABLED == 'true' | |
| && ( | |
| github.event_name == 'workflow_dispatch' | |
| || (github.event_name == 'issue_comment' | |
| && github.event.comment.user.login == 'wheels-bot[bot]' | |
| && (contains(github.event.comment.body, 'wheels-bot:triage-confidence:high') | |
| || contains(github.event.comment.body, 'wheels-bot:triage-confidence:medium') | |
| || contains(github.event.comment.body, 'wheels-bot:research-confidence:high') | |
| || contains(github.event.comment.body, 'wheels-bot:research-confidence:medium'))) | |
| ) | |
| env: | |
| ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue-number }} | |
| WHEELS_CI: "true" | |
| 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: Checkout develop | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: develop | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Skip check | |
| id: gate | |
| uses: ./.github/actions/wheels-bot-skip-check | |
| with: | |
| target-type: issue | |
| target-number: ${{ env.ISSUE_NUMBER }} | |
| marker-pattern: 'wheels-bot:fix:${{ env.ISSUE_NUMBER }}|wheels-bot:fix-held:${{ env.ISSUE_NUMBER }}' | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| # Campaign guard: session-driven campaign PRs race bot drafts (4 bot | |
| # drafts were superseded by human/session PRs on 2026-06-10 alone); the | |
| # human/session branch wins. Skip the (expensive) model invocation when | |
| # either signal shows a human is already on the issue: | |
| # (a) a `peter/issue-<N>-*` campaign branch exists on origin, or | |
| # (b) an open non-bot PR references "#<N>" in its body. | |
| # Logs only — no issue comment. Manual workflow_dispatch bypasses the | |
| # guard entirely: a maintainer dispatching by hand is explicit intent, | |
| # including over a lingering stale campaign branch. | |
| - name: Campaign guard | |
| if: steps.gate.outputs.skip == 'false' | |
| id: campaign | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then | |
| echo "::notice::Campaign guard bypassed: manual dispatch is explicit maintainer intent." | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then | |
| echo "::error::issue number must be numeric, got: $ISSUE_NUMBER" | |
| exit 1 | |
| fi | |
| # (a) session-driven campaign branch for this issue already on origin | |
| if [ -n "$(git ls-remote --heads origin "peter/issue-${ISSUE_NUMBER}-*")" ]; then | |
| echo "::notice::Campaign guard: branch peter/issue-${ISSUE_NUMBER}-* exists on origin — skipping bot draft (the human/session branch wins)." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # (b) an open non-bot PR whose body references this issue. The | |
| # --search narrows server-side; jq re-verifies the body references | |
| # "#<N>" with a trailing non-digit boundary (plain contains() would | |
| # let issue 123 match a body referencing #1234) and that the author | |
| # is not a bot. ISSUE_NUMBER is regex-safe: validated ^[0-9]+$ above. | |
| open_nonbot=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open \ | |
| --search "${ISSUE_NUMBER} in:body" \ | |
| --json number,author,body \ | |
| --jq "[.[] | select(.author.is_bot | not) | select(.body | test(\"#${ISSUE_NUMBER}([^0-9]|\$)\"))] | length") | |
| if [ "${open_nonbot}" != "0" ]; then | |
| echo "::notice::Campaign guard: ${open_nonbot} open non-bot PR(s) reference #${ISSUE_NUMBER} — skipping bot draft (the human/session PR wins)." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| - name: Compute branch slug | |
| if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false' | |
| id: branch | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| title=$(gh issue view "$ISSUE_NUMBER" --json title --jq '.title') | |
| slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' \ | |
| | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' \ | |
| | cut -c1-50 \ | |
| | sed -E 's/-+$//') | |
| [[ -z "$slug" ]] && slug="issue" | |
| name="fix/bot-${ISSUE_NUMBER}-${slug}" | |
| echo "name=$name" >> "$GITHUB_OUTPUT" | |
| - name: Configure git + create branch | |
| if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false' | |
| run: | | |
| git config user.name "wheels-bot[bot]" | |
| git config user.email "wheels-bot[bot]@users.noreply.github.com" | |
| git checkout -b "${{ steps.branch.outputs.name }}" | |
| - name: Set up Wheels test environment | |
| if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false' | |
| uses: ./.github/actions/setup-wheels-test-env | |
| with: | |
| port: '60007' | |
| install-playwright: 'false' | |
| - name: Run Propose Fix | |
| if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| # This workflow fires from wheels-bot[bot] comments containing | |
| # high- or medium-confidence triage or research markers. Allow | |
| # that bot identity (and github-actions[bot] for consistency). | |
| allowed_bots: 'wheels-bot[bot],github-actions[bot]' | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| github_token: ${{ steps.app-token.outputs.token }} | |
| prompt: | | |
| /propose-fix ${{ env.ISSUE_NUMBER }} | |
| # Model policy: judging gate = fable, coding stages = opus, janitorial = sonnet. | |
| claude_args: | | |
| --model claude-opus-4-8 | |
| --max-turns 1000 | |
| --allowedTools "Bash(gh:*),Bash(git:*),Bash(bash tools/test-local.sh*),Bash(curl:*),Read,Edit,Write,Grep,Glob" | |
| - name: Push branch | |
| if: steps.gate.outputs.skip == 'false' && steps.campaign.outputs.skip == 'false' | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| run: | | |
| if git diff --quiet origin/develop -- && git diff --cached --quiet; then | |
| echo "No changes produced — nothing to push." | |
| exit 0 | |
| fi | |
| git push -u origin "${{ steps.branch.outputs.name }}" | |
| - name: Stop Lucee server | |
| if: always() | |
| run: | | |
| if [ -f /tmp/lucli-server.pid ]; then | |
| kill $(cat /tmp/lucli-server.pid) 2>/dev/null || true | |
| fi | |
| lucli server stop 2>/dev/null || true |