Agentic Merge Queue Monitor #16
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
| # Agentic merge-queue monitor: when the CI workflow fails on a merge-queue run, | |
| # analyse the failure with opencode (via GitHub Models) and post a debugging | |
| # comment on the originating PR. | |
| # | |
| # The agent is invoked via .github/actions/run-agent — all opencode bootstrap | |
| # boilerplate (install, config, provider setup) lives there. | |
| # | |
| # Auth: | |
| # APP_ID / APP_PRIVATE_KEY - GitHub App: contents:write, pull-requests:write | |
| # MODELS_TOKEN - fine-grained PAT with models:read | |
| name: Agentic Merge Queue Monitor | |
| on: | |
| workflow_run: | |
| # Must match the `name:` field in .github/workflows/ci.yml exactly. | |
| workflows: ["CI"] | |
| types: [completed] | |
| # workflow_run always runs from the default branch. All writes go through the | |
| # App token; the workflow's own GITHUB_TOKEN stays minimal. | |
| permissions: | |
| contents: read | |
| actions: read | |
| concurrency: | |
| # One analysis per failed run; never cancel an in-flight analysis. | |
| group: mq-monitor-${{ github.event.workflow_run.id }} | |
| cancel-in-progress: false | |
| jobs: | |
| analyze: | |
| # Only act on actual failures; timed-out/cancelled runs are rarely | |
| # actionable as code regressions. | |
| if: ${{ github.event.workflow_run.conclusion == 'failure' }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| # Checkout needed so .github/actions/run-agent is available. | |
| - uses: actions/checkout@v4 | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| # ----------------------------------------------------------------------- | |
| # Resolve the PR number from the failed run's head branch / SHA. | |
| # Merge-queue branches look like: gh-readonly-queue/main/pr-NNN-<sha> | |
| # ----------------------------------------------------------------------- | |
| - name: Resolve target PR | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REPO: ${{ github.repository }} | |
| HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| run: | | |
| set -euo pipefail | |
| pr="" | |
| if [[ "$HEAD_BRANCH" =~ pr-([0-9]+) ]]; then | |
| pr="${BASH_REMATCH[1]}" | |
| echo "Parsed PR #$pr from branch '$HEAD_BRANCH'" | |
| fi | |
| # Fallback: find a PR associated with the failing commit SHA. | |
| if [ -z "$pr" ]; then | |
| pr=$(gh api "repos/$REPO/commits/$HEAD_SHA/pulls" \ | |
| --jq 'sort_by(.updated_at) | reverse | .[0].number' \ | |
| 2>/dev/null || true) | |
| [ "$pr" = "null" ] && pr="" | |
| [ -n "$pr" ] && echo "Resolved PR #$pr via commit SHA $HEAD_SHA" | |
| fi | |
| if [ -z "$pr" ]; then | |
| echo "No PR associated with this failure; nothing to comment on." | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| echo "number=$pr" >> "$GITHUB_OUTPUT" | |
| # ----------------------------------------------------------------------- | |
| # Deduplicate: skip if we already commented for this exact run. | |
| # Keyed on run id so a PR that fails twice in different queue batches | |
| # gets one comment per failure. | |
| # ----------------------------------------------------------------------- | |
| - name: Check for existing analysis comment | |
| id: dedup | |
| if: ${{ steps.pr.outputs.found == 'true' }} | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REPO: ${{ github.repository }} | |
| PR: ${{ steps.pr.outputs.number }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| run: | | |
| set -euo pipefail | |
| marker="<!-- mq-monitor:run-${RUN_ID} -->" | |
| if gh api "repos/$REPO/issues/$PR/comments" --paginate \ | |
| --jq '.[].body' | grep -qF "$marker"; then | |
| echo "Already analysed run $RUN_ID for PR #$PR; skipping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # ----------------------------------------------------------------------- | |
| # Fetch just the failed-step logs; keep the tail (errors are at the end). | |
| # ----------------------------------------------------------------------- | |
| - name: Collect failure logs | |
| if: ${{ steps.pr.outputs.found == 'true' && steps.dedup.outputs.skip != 'true' }} | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REPO: ${{ github.repository }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| run: | | |
| set -euo pipefail | |
| gh run view "$RUN_ID" --repo "$REPO" \ | |
| --json displayTitle,headBranch,headSha,event,jobs \ | |
| --jq '{ | |
| title: .displayTitle, | |
| branch: .headBranch, | |
| sha: .headSha, | |
| event: .event, | |
| failed_jobs: [.jobs[] | |
| | select(.conclusion == "failure") | |
| | {name, url, steps: [.steps[] | |
| | select(.conclusion == "failure") | |
| | {name, number}]}] | |
| }' > /tmp/run-meta.json | |
| gh run view "$RUN_ID" --repo "$REPO" --log-failed \ | |
| > /tmp/failed-log-full.txt 2>/dev/null || true | |
| # Truncate to ~50KB — errors are almost always at the tail. | |
| if [ -s /tmp/failed-log-full.txt ]; then | |
| tail -c 50000 /tmp/failed-log-full.txt > /tmp/failed-log.txt | |
| else | |
| echo "(no failed-step logs available)" > /tmp/failed-log.txt | |
| fi | |
| # ----------------------------------------------------------------------- | |
| # Build the prompt, then run the agent via the shared composite action. | |
| # ----------------------------------------------------------------------- | |
| - name: Build prompt | |
| if: ${{ steps.pr.outputs.found == 'true' && steps.dedup.outputs.skip != 'true' }} | |
| run: | | |
| { | |
| echo "You are a CI reliability engineer for the bootc project." | |
| echo "A CI run failed in the merge queue. Analyse the failure below." | |
| echo "" | |
| echo "## Failed run metadata" | |
| echo '```json' | |
| cat /tmp/run-meta.json | |
| echo '```' | |
| echo "" | |
| echo "## Failed-step logs (tail, truncated to 50KB)" | |
| echo '```text' | |
| cat /tmp/failed-log.txt | |
| echo '```' | |
| echo "" | |
| echo "Write a concise analysis (<=250 words, plain markdown, no top-level heading):" | |
| echo "1. Name the specific failing job/step and the key error line." | |
| echo "2. State whether this looks like a flaky/transient failure or a" | |
| echo " genuine regression, and why." | |
| echo "3. Give the most likely root cause." | |
| echo "4. Recommend one concrete next action (re-run, specific fix," | |
| echo " or what to inspect next)." | |
| echo "Do not speculate beyond the evidence in the logs." | |
| echo "" | |
| echo "IMPORTANT: Write your analysis to the file /tmp/analysis.md using" | |
| echo "the write tool. Do not just print it — write it to that file." | |
| } > /tmp/agent-prompt.txt | |
| - name: Run agent | |
| id: analyze | |
| if: ${{ steps.pr.outputs.found == 'true' && steps.dedup.outputs.skip != 'true' }} | |
| timeout-minutes: 5 | |
| uses: ./.github/actions/run-agent | |
| with: | |
| models-token: ${{ secrets.MODELS_TOKEN }} | |
| prompt-file: /tmp/agent-prompt.txt | |
| output-file: /tmp/analysis.md | |
| - name: Upload analysis artifact | |
| if: ${{ always() && steps.pr.outputs.found == 'true' && steps.dedup.outputs.skip != 'true' }} | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: mq-analysis-${{ steps.pr.outputs.number }} | |
| path: | | |
| /tmp/analysis.md | |
| /tmp/run-meta.json | |
| /tmp/failed-log.txt | |
| if-no-files-found: warn | |
| # ----------------------------------------------------------------------- | |
| # Post the debugging comment on the PR. | |
| # ----------------------------------------------------------------------- | |
| - name: Post analysis comment on PR | |
| if: ${{ steps.pr.outputs.found == 'true' && steps.dedup.outputs.skip != 'true' && steps.analyze.outcome == 'success' }} | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REPO: ${{ github.repository }} | |
| PR: ${{ steps.pr.outputs.number }} | |
| RUN_URL: ${{ github.event.workflow_run.html_url }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## 🤖 CI Failure Analysis" | |
| echo "" | |
| echo "Merge-queue CI run **failed** for commit \`${HEAD_SHA:0:8}\` — [view run]($RUN_URL)." | |
| echo "" | |
| cat /tmp/analysis.md | |
| echo "" | |
| echo "---" | |
| echo "_AI-generated by the [agentic-merge-queue-monitor](.github/workflows/agentic-merge-queue-monitor.yml) workflow using GitHub Models (gpt-4.1). Treat as a starting point, not ground truth._" | |
| echo "<!-- mq-monitor:run-${RUN_ID} -->" | |
| } > /tmp/comment.md | |
| gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment.md |