Docs: update scroll context manager blog #3066
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: AI Code Review | |
| on: | |
| pull_request_target: | |
| types: [opened, synchronize] | |
| permissions: {} | |
| jobs: | |
| # Maintainer must approve before AI review runs | |
| review-gate: | |
| name: AI Review Approval | |
| runs-on: ubuntu-latest | |
| environment: ai-review-approved | |
| permissions: {} | |
| steps: | |
| - run: | | |
| echo "Maintainer approved AI review for PR #${{ github.event.pull_request.number }}" | |
| ai-review: | |
| name: QwenPaw AI Review | |
| needs: review-gate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| verdict: ${{ steps.review.outputs.verdict }} | |
| high_count: ${{ steps.review.outputs.high_count }} | |
| medium_count: ${{ steps.review.outputs.medium_count }} | |
| env: | |
| REVIEW_PROVIDER: dashscope | |
| REVIEW_MODEL: ${{ vars.REVIEW_MODEL || 'qwen3.6-plus' }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PR_REPO: ${{ github.event.pull_request.base.repo.full_name }} | |
| QWENPAW_TOOL_GUARD_APPROVAL_TIMEOUT_SECONDS: 1 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.base.ref }} | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| cache: 'pip' | |
| - name: Install QwenPaw and dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| pip install httpx | |
| - name: Verify gh CLI and authentication | |
| run: | | |
| gh --version | |
| gh auth status | |
| echo "Verifying PR access..." | |
| gh pr view ${{ env.PR_NUMBER }} --repo ${{ env.PR_REPO }} --json number,title --jq '.title' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: Initialize QwenPaw workspace | |
| run: | | |
| qwenpaw init --defaults --accept-security | |
| python scripts/review-bot/setup_review_workspace.py | |
| env: | |
| DASHSCOPE_API_KEY: ${{ secrets.REVIEW_DASHSCOPE_API_KEY }} | |
| - name: Start QwenPaw server | |
| run: | | |
| QWENPAW_AUTH_ENABLED=false \ | |
| GH_TOKEN=${{ github.token }} \ | |
| GITHUB_TOKEN=${{ github.token }} \ | |
| python -m qwenpaw app --host 127.0.0.1 --port 8088 & | |
| echo $! > /tmp/qwenpaw.pid | |
| - name: Wait for QwenPaw to be ready | |
| run: | | |
| for i in $(seq 1 90); do | |
| if curl -sf http://localhost:8088/api/version > /dev/null 2>&1; then | |
| echo "QwenPaw is ready (took ${i}s)" | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "QwenPaw failed to start within 90 seconds" | |
| exit 1 | |
| - name: Verify review model is configured | |
| run: | | |
| python - <<'PY' | |
| import json | |
| import os | |
| import sys | |
| import urllib.request | |
| expected_provider = os.environ.get("REVIEW_PROVIDER", "dashscope") | |
| expected_model = os.environ.get("REVIEW_MODEL", "qwen3.6-plus") | |
| with urllib.request.urlopen("http://localhost:8088/api/models/active?scope=global") as resp: | |
| data = json.load(resp) | |
| active = data.get("active_llm") or {} | |
| provider = active.get("provider_id") | |
| model = active.get("model") | |
| print(f"Active model: {provider}/{model}") | |
| if provider != expected_provider or model != expected_model: | |
| print(f"ERROR: expected {expected_provider}/{expected_model}") | |
| sys.exit(1) | |
| PY | |
| - name: Remove secrets from disk | |
| run: | | |
| rm -f ~/.qwenpaw.secret/.master_key | |
| rm -rf ~/.qwenpaw.secret/providers | |
| echo "Secrets removed from disk (cached in server memory)" | |
| - name: Run AI Review | |
| id: review | |
| run: python scripts/review-bot/review_runner.py | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| - name: Upload AI review result | |
| if: success() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pr-ai-review-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: /tmp/review_result.md | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Stop QwenPaw | |
| if: always() | |
| run: | | |
| if [ -f /tmp/qwenpaw.pid ]; then | |
| kill $(cat /tmp/qwenpaw.pid) 2>/dev/null || true | |
| fi | |
| publish-review: | |
| name: Publish QwenPaw AI Review | |
| needs: [review-gate, ai-review] | |
| if: >- | |
| ${{ always() && needs.review-gate.result == 'success' && | |
| needs.ai-review.result != 'cancelled' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| pull-requests: write | |
| steps: | |
| - name: Download AI review result | |
| if: needs.ai-review.result == 'success' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: pr-ai-review-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: /tmp/ai-review | |
| - name: Post review comment and manage labels | |
| if: needs.ai-review.result == 'success' | |
| uses: actions/github-script@v7 | |
| env: | |
| REVIEW_VERDICT: ${{ needs.ai-review.outputs.verdict }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const prNumber = context.payload.pull_request.number; | |
| const expectedHeadSha = context.payload.pull_request.head.sha; | |
| const currentPull = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| }); | |
| if (currentPull.data.head.sha !== expectedHeadSha) { | |
| core.notice( | |
| `Skipping stale AI review for ${expectedHeadSha}; ` + | |
| `current head is ${currentPull.data.head.sha}`, | |
| ); | |
| return; | |
| } | |
| const verdict = process.env.REVIEW_VERDICT; | |
| if (!['APPROVE', 'REQUEST_CHANGES'].includes(verdict)) { | |
| core.setFailed(`Invalid AI review verdict: ${verdict}`); | |
| return; | |
| } | |
| const reviewBody = fs.readFileSync( | |
| '/tmp/ai-review/review_result.md', | |
| 'utf8', | |
| ); | |
| if (!reviewBody.trim()) { | |
| core.setFailed('AI review result is empty'); | |
| return; | |
| } | |
| const readyForHumanReview = verdict === 'APPROVE'; | |
| const icon = readyForHumanReview ? '✅' : '⚠️'; | |
| const title = readyForHumanReview | |
| ? 'Passed AI Review — Awaiting Human Review' | |
| : 'Changes Requested'; | |
| const header = `## ${icon} QwenPaw AI Review: ${title}\n\n`; | |
| const footer = '\n\n---\n' + | |
| `*Auto-generated by QwenPaw Review Agent | ` + | |
| `Triggered by maintainer approval*`; | |
| const fullBody = `${header}${reviewBody}${footer}`; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: fullBody, | |
| }); | |
| // Label logic: | |
| // APPROVE (high=0, medium<=2) → add ready-for-human-review | |
| // REQUEST_CHANGES → remove ready-for-human-review | |
| if (readyForHumanReview) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: ['ready-for-human-review'], | |
| }); | |
| } else { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| name: 'ready-for-human-review', | |
| }); | |
| } catch (e) {} | |
| } | |
| - name: Post fallback on review failure | |
| if: needs.ai-review.result == 'failure' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| const expectedHeadSha = context.payload.pull_request.head.sha; | |
| const currentPull = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| }); | |
| if (currentPull.data.head.sha !== expectedHeadSha) { | |
| core.notice( | |
| `Skipping stale failure notice for ${expectedHeadSha}; ` + | |
| `current head is ${currentPull.data.head.sha}`, | |
| ); | |
| return; | |
| } | |
| const body = [ | |
| '## ⚠️ QwenPaw AI Review: Review Failed\n', | |
| 'The AI Review Bot encountered an error and could not ', | |
| 'generate a review.\n', | |
| 'Possible causes:', | |
| '- LLM API unavailable or timed out', | |
| '- `gh` CLI authentication failure', | |
| '- QwenPaw internal service error\n', | |
| '**Please have a maintainer review this PR manually.**\n', | |
| '---', | |
| '*🤖 Auto-generated by QwenPaw Review Agent | ', | |
| 'Review failed — manual review required*', | |
| ].join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body, | |
| }); |