Qwen CI Failure Patrol #96
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: 'Qwen CI Failure Patrol' | |
| on: | |
| schedule: | |
| - cron: '*/10 * * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: 'read' | |
| concurrency: | |
| group: 'qwen-ci-flaky-rerun' | |
| cancel-in-progress: false | |
| env: | |
| ACTIVE_DAYS: '7' | |
| MAX_CANDIDATES_PER_RUN: '5' | |
| STALE_MINUTES: '30' | |
| WORKDIR: '${{ github.workspace }}/.qwen/ci-flaky-rerun' | |
| jobs: | |
| classify: | |
| name: 'Classify stale PR failures' | |
| if: "${{ github.repository == 'QwenLM/qwen-code' }}" | |
| runs-on: 'ubuntu-latest' | |
| timeout-minutes: 10 | |
| permissions: | |
| actions: 'read' | |
| contents: 'read' | |
| pull-requests: 'read' | |
| outputs: | |
| bot_login: '${{ steps.identity.outputs.bot_login }}' | |
| target_found: '${{ steps.scan.outputs.target_found }}' | |
| input_sha: '${{ steps.scan.outputs.input_sha }}' | |
| has_decisions: '${{ steps.decisions.outputs.has_decisions }}' | |
| steps: | |
| - name: 'Resolve patrol identity' | |
| id: 'identity' | |
| env: | |
| GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' | |
| run: |- | |
| bot_login="$(gh api user --jq .login)" | |
| test -n "${bot_login}" | |
| echo "bot_login=${bot_login}" >> "${GITHUB_OUTPUT}" | |
| - name: 'Checkout trusted event commit' | |
| uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' | |
| with: | |
| ref: '${{ github.sha }}' | |
| persist-credentials: false | |
| - name: 'Set up Node.js' | |
| uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' | |
| with: | |
| node-version: '22' | |
| - name: 'Scan stale PR failures' | |
| id: 'scan' | |
| env: | |
| GH_TOKEN: '${{ github.token }}' | |
| run: |- | |
| mkdir -p "${WORKDIR}" | |
| node .github/scripts/ci-flaky-rerun.mjs scan \ | |
| --repo "${{ github.repository }}" \ | |
| --workdir "${WORKDIR}" \ | |
| --stale-minutes "${STALE_MINUTES}" \ | |
| --active-days "${ACTIVE_DAYS}" \ | |
| --max-candidates "${MAX_CANDIDATES_PER_RUN}" \ | |
| --trusted-marker-login "${{ steps.identity.outputs.bot_login }}" > "${WORKDIR}/scan.out" | |
| grep -E '^(target_found|input_sha)=' "${WORKDIR}/scan.out" >> "${GITHUB_OUTPUT}" | |
| - name: 'Classify with ci-flaky-patrol skill' | |
| if: "${{ steps.scan.outputs.target_found == 'true' }}" | |
| # The model can outlast the WHOLE job budget when it is under load: | |
| # across a busy 9-hour window every scheduled patrol spent ~9m40s in | |
| # this step and was killed by the 10-minute job timeout, so Validate | |
| # and Upload never ran, `act` was skipped, and nothing was ever re-run | |
| # (28 of 30 runs cancelled; the 2 that passed took ~2 minutes at 00:0x | |
| # when the model was idle). Bounding the STEP below the job turns a | |
| # slow model into one wasted cycle instead of a dead patrol - the next | |
| # tick simply tries again. | |
| timeout-minutes: 5 | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: '' | |
| GITHUB_TOKEN: '' | |
| uses: 'QwenLM/qwen-code-action@05f81718976f3b7da657422e6e8e4d372b8621d7' | |
| with: | |
| OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}' | |
| OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' | |
| OPENAI_MODEL: '${{ vars.QWEN_PR_REVIEW_MODEL }}' | |
| qwen_cli_version: '0.19.9' | |
| settings: |- | |
| { | |
| "tools": { | |
| "sandbox": true, | |
| "core": ["read_file", "write_file"] | |
| } | |
| } | |
| prompt: |- | |
| Use .qwen/skills/ci-flaky-patrol/SKILL.md. | |
| Workdir: ${{ env.WORKDIR }} | |
| Read ci-flaky-input.json and write ci-flaky-decisions.json. | |
| - name: 'Validate patrol decisions' | |
| id: 'decisions' | |
| if: "${{ steps.scan.outputs.target_found == 'true' }}" | |
| run: |- | |
| # No decisions is a legitimate outcome now that the classifier can | |
| # time out without taking the job with it. A timeout can also kill | |
| # the process mid-write, leaving a non-empty but unparseable file, | |
| # so validate JSON syntax alongside the size check. Report the | |
| # result and let the run finish cleanly so the next tick retries, | |
| # rather than failing the patrol and skipping the act job that | |
| # reruns real flakes. | |
| if [[ -s "${WORKDIR}/ci-flaky-decisions.json" ]] && node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "${WORKDIR}/ci-flaky-decisions.json" 2>/dev/null; then | |
| echo 'has_decisions=true' >> "${GITHUB_OUTPUT}" | |
| else | |
| echo 'has_decisions=false' >> "${GITHUB_OUTPUT}" | |
| echo "::warning::patrol classifier produced no valid decisions this cycle; nothing to act on" | |
| fi | |
| - name: 'Upload patrol input and decisions' | |
| if: "${{ steps.decisions.outputs.has_decisions == 'true' }}" | |
| uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' | |
| with: | |
| name: 'ci-flaky-patrol-decisions' | |
| path: |- | |
| ${{ env.WORKDIR }}/ci-flaky-input.json | |
| ${{ env.WORKDIR }}/ci-flaky-decisions.json | |
| if-no-files-found: 'error' | |
| retention-days: 1 | |
| act: | |
| name: 'Act on classified PR failures' | |
| needs: 'classify' | |
| if: "${{ needs.classify.result == 'success' && needs.classify.outputs.has_decisions == 'true' }}" | |
| runs-on: 'ubuntu-latest' | |
| timeout-minutes: 10 | |
| # GitHub writes use CI_BOT_PAT; keep the generated GITHUB_TOKEN read-only. | |
| permissions: | |
| actions: 'read' | |
| contents: 'read' | |
| pull-requests: 'read' | |
| steps: | |
| - name: 'Checkout trusted event commit' | |
| uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' | |
| with: | |
| ref: '${{ github.sha }}' | |
| persist-credentials: false | |
| - name: 'Set up Node.js' | |
| uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' | |
| with: | |
| node-version: '22' | |
| - name: 'Download patrol decisions' | |
| if: "${{ needs.classify.outputs.target_found == 'true' }}" | |
| uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' | |
| with: | |
| name: 'ci-flaky-patrol-decisions' | |
| path: '${{ env.WORKDIR }}' | |
| - name: 'Act on patrol decisions' | |
| if: "${{ needs.classify.outputs.target_found == 'true' }}" | |
| env: | |
| GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' | |
| run: |- | |
| node .github/scripts/ci-flaky-rerun.mjs act \ | |
| --repo "${{ github.repository }}" \ | |
| --workdir "${WORKDIR}" \ | |
| --input-sha "${{ needs.classify.outputs.input_sha }}" \ | |
| --trusted-marker-login "${{ needs.classify.outputs.bot_login }}" | |
| - name: 'Reset successful failure state' | |
| if: '${{ always() }}' | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' | |
| run: |- | |
| node .github/scripts/ci-flaky-rerun.mjs reset \ | |
| --repo "${{ github.repository }}" \ | |
| --trusted-marker-login "${{ needs.classify.outputs.bot_login }}" |