diff --git a/.github/workflows/maint-68-sync-consumer-repos.yml b/.github/workflows/maint-68-sync-consumer-repos.yml index 553b2b846..7d9cc86fc 100644 --- a/.github/workflows/maint-68-sync-consumer-repos.yml +++ b/.github/workflows/maint-68-sync-consumer-repos.yml @@ -580,6 +580,7 @@ jobs: "agents-pr-meta.yml" "agents-keepalive-loop.yml" "agents-verifier.yml" + "agents-bot-comment-handler.yml" "autofix.yml" "pr-00-gate.yml" ) diff --git a/.github/workflows/reusable-bot-comment-handler.yml b/.github/workflows/reusable-bot-comment-handler.yml new file mode 100644 index 000000000..11b98c23b --- /dev/null +++ b/.github/workflows/reusable-bot-comment-handler.yml @@ -0,0 +1,404 @@ +# Reusable workflow to address bot review comments on PRs +# +# Collects unresolved review comments from known bot authors (Copilot, CodeRabbit, etc.) +# and dispatches the configured agent to address them. +# +# Triggers: +# - Called by consumer repo workflows (label trigger or keepalive integration) +# - Manual dispatch for testing +# +# Agent selection: +# - Reads PR labels (agent:codex, agent:claude, etc.) to determine which agent +# - Falls back to Codex if no agent label found +# +# Outputs: +# - comments_found: 'true' if unresolved bot comments were found +# - comments_count: Number of unresolved bot comments found +# - agent_triggered: 'true' if the agent was triggered to address comments + +name: Reusable Bot Comment Handler + +on: + workflow_call: + inputs: + pr_number: + description: 'PR number to process' + required: true + type: string + dry_run: + description: 'Preview what would be done without making changes' + required: false + type: boolean + default: false + bot_authors: + description: 'Comma-separated list of bot login names including [bot] suffix (default: copilot[bot],github-actions[bot],coderabbitai[bot])' + required: false + type: string + default: 'copilot[bot],github-actions[bot],coderabbitai[bot]' + skip_if_human_replied: + description: 'Skip comments where a human has already replied' + required: false + type: boolean + default: true + outputs: + comments_found: + description: 'Whether unresolved bot comments were found' + value: ${{ jobs.collect.outputs.comments_found }} + comments_count: + description: 'Number of unresolved bot comments found' + value: ${{ jobs.collect.outputs.comments_count }} + agent_triggered: + description: 'Whether the agent was triggered to address comments' + value: ${{ jobs.dispatch.outputs.triggered }} + secrets: + service_bot_pat: + description: 'PAT for service bot (comments, labels)' + required: false + gh_app_id: + description: 'GitHub App ID for authentication' + required: false + gh_app_private_key: + description: 'GitHub App private key' + required: false + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + # Collect unresolved bot comments + collect: + name: Collect bot comments + runs-on: ubuntu-latest + outputs: + comments_found: ${{ steps.collect.outputs.found }} + comments_count: ${{ steps.collect.outputs.count }} + comments_json: ${{ steps.collect.outputs.comments }} + agent: ${{ steps.agent.outputs.agent }} + agent_workflow: ${{ steps.agent.outputs.workflow }} + steps: + - name: Generate token (if App configured) + id: token + uses: actions/create-github-app-token@v1 + + continue-on-error: true + with: + app-id: ${{ secrets.gh_app_id }} + private-key: ${{ secrets.gh_app_private_key }} + + - name: Resolve token + id: auth + env: + TOKEN_OUTPUT: ${{ steps.token.outputs.token }} + SERVICE_PAT: ${{ secrets.service_bot_pat }} + GITHUB_TOKEN: ${{ github.token }} + run: | + if [ -n "${TOKEN_OUTPUT}" ]; then + echo "token=${TOKEN_OUTPUT}" >> "$GITHUB_OUTPUT" + elif [ -n "${SERVICE_PAT}" ]; then + echo "token=${SERVICE_PAT}" >> "$GITHUB_OUTPUT" + else + echo "token=${GITHUB_TOKEN}" >> "$GITHUB_OUTPUT" + fi + + - name: Detect agent from PR labels + id: agent + uses: actions/github-script@v7 + with: + github-token: ${{ steps.auth.outputs.token }} + script: | + const prNumber = parseInt('${{ inputs.pr_number }}'); + + let pr; + try { + const response = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + pr = response.data; + } catch (error) { + console.log(`Failed to fetch PR #${prNumber}: ${error.message}`); + core.setFailed(`Could not fetch PR #${prNumber}`); + return; + } + + const labels = pr.labels.map(l => l.name); + let agent = 'codex'; // default + let workflow = 'reusable-codex-run.yml'; + + if (labels.includes('agent:claude')) { + agent = 'claude'; + workflow = 'reusable-claude-run.yml'; + } else if (labels.includes('agent:gemini')) { + agent = 'gemini'; + workflow = 'reusable-gemini-run.yml'; + } else if (labels.includes('agent:codex')) { + agent = 'codex'; + workflow = 'reusable-codex-run.yml'; + } + + core.setOutput('agent', agent); + core.setOutput('workflow', workflow); + console.log(`Detected agent: ${agent}, workflow: ${workflow}`); + + - name: Collect unresolved bot comments + id: collect + uses: actions/github-script@v7 + with: + github-token: ${{ steps.auth.outputs.token }} + script: | + const prNumber = parseInt('${{ inputs.pr_number }}'); + const botAuthors = '${{ inputs.bot_authors }}'.split(',').map(s => s.trim()); + const skipIfHumanReplied = ${{ inputs.skip_if_human_replied }}; + + console.log(`Collecting comments for PR #${prNumber}`); + console.log(`Bot authors: ${botAuthors.join(', ')}`); + + // Get all review comments + const { data: comments } = await github.rest.pulls.listReviewComments({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + per_page: 100 + }); + + // Filter for unresolved bot comments + const botComments = []; + const processedThreads = new Set(); + + for (const comment of comments) { + // Skip if not from a known bot + if (!botAuthors.some(bot => comment.user.login.toLowerCase() === bot.toLowerCase())) { + continue; + } + + // Skip resolved threads (check via GraphQL would be better, but this approximates) + // We'll use in_reply_to_id to track threads + const threadId = comment.in_reply_to_id || comment.id; + + if (processedThreads.has(threadId)) { + continue; + } + + // Check if human replied to this thread (check all comments in thread) + if (skipIfHumanReplied) { + const threadReplies = comments.filter(c => + (c.in_reply_to_id === comment.id || c.in_reply_to_id === threadId) && + !botAuthors.some(bot => c.user.login.toLowerCase() === bot.toLowerCase()) + ); + if (threadReplies.length > 0) { + console.log(`Skipping comment ${comment.id} - human already replied`); + processedThreads.add(threadId); + continue; + } + } + + processedThreads.add(threadId); + + // Extract useful info + botComments.push({ + id: comment.id, + path: comment.path, + line: comment.line || comment.original_line, + body: comment.body, + author: comment.user.login, + url: comment.html_url, + diff_hunk: comment.diff_hunk + }); + } + + const found = botComments.length > 0; + core.setOutput('found', found ? 'true' : 'false'); + core.setOutput('count', botComments.length); + core.setOutput('comments', JSON.stringify(botComments)); + + console.log(`Found ${botComments.length} unresolved bot comments`); + + if (botComments.length > 0) { + console.log('Comments to address:'); + for (const c of botComments) { + console.log(`- ${c.path}:${c.line} (${c.author}): ${c.body.substring(0, 100)}...`); + } + } + + - name: Post summary + if: steps.collect.outputs.found == 'true' + uses: actions/github-script@v7 + env: + COMMENTS_JSON: ${{ steps.collect.outputs.comments }} + with: + github-token: ${{ steps.auth.outputs.token }} + script: | + const comments = JSON.parse(process.env.COMMENTS_JSON); + const count = comments.length; + const agent = '${{ steps.agent.outputs.agent }}'; + const dryRun = ${{ inputs.dry_run }}; + + let summary = `## πŸ€– Bot Comment Handler\n\n`; + summary += `Found **${count}** unresolved bot comment(s) to address.\n\n`; + summary += `| File | Line | Bot | Preview |\n`; + summary += `|------|------|-----|--------|\n`; + + for (const c of comments.slice(0, 10)) { + const preview = c.body.substring(0, 50).replace(/\n/g, ' ') + '...'; + summary += `| \`${c.path}\` | ${c.line || 'N/A'} | ${c.author} | ${preview} |\n`; + } + + if (comments.length > 10) { + summary += `\n*...and ${comments.length - 10} more*\n`; + } + + if (dryRun) { + summary += `\n⚠️ **Dry run** - agent will not be triggered.\n`; + } else { + summary += `\nπŸš€ Dispatching **${agent}** to address these comments...\n`; + } + + await core.summary.addRaw(summary).write(); + + # Generate prompt file with bot comments + prepare: + name: Prepare agent prompt + needs: collect + if: needs.collect.outputs.comments_found == 'true' && inputs.dry_run == false + runs-on: ubuntu-latest + outputs: + prompt_ready: ${{ steps.prompt.outputs.ready }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate bot comments prompt + id: prompt + env: + COMMENTS_JSON: ${{ needs.collect.outputs.comments_json }} + run: | + set -euo pipefail + + mkdir -p .github/codex/prompts + + cat > .github/codex/prompts/fix_bot_comments_dynamic.md << 'PROMPT_HEADER' + # Fix Bot Review Comments + + Review bots have left suggestions on this PR. Address each one: + + ## Instructions + + 1. Read each bot comment below + 2. Implement the suggested fix if it improves the code + 3. If a suggestion is incorrect or doesn't apply, skip it and note why + 4. After fixing, summarize what you addressed in your commit message + + ## Bot Comments to Address + + PROMPT_HEADER + + # Append each comment using jq with proper escaping + echo "${COMMENTS_JSON}" | jq -r '.[] | "### \(.path):\(.line // "N/A")\n\n**From:** \(.author)\n\n```\n\(.body)\n```\n\n**Context (diff hunk):**\n```diff\n\(.diff_hunk)\n```\n\n---\n"' >> .github/codex/prompts/fix_bot_comments_dynamic.md + + cat >> .github/codex/prompts/fix_bot_comments_dynamic.md << 'PROMPT_FOOTER' + + ## After Addressing Comments + + - Commit your changes with message: "fix: address bot review comments" + - Include which suggestions you addressed vs skipped in the commit message + PROMPT_FOOTER + + echo "ready=true" >> "$GITHUB_OUTPUT" + echo "Generated prompt with $(echo "${COMMENTS_JSON}" | jq length) comments" + + - name: Upload prompt artifact + uses: actions/upload-artifact@v4 + with: + name: bot-comments-prompt-${{ github.run_id }} + path: .github/codex/prompts/fix_bot_comments_dynamic.md + retention-days: 1 + + # Dispatch to agent + dispatch: + name: Dispatch to agent + needs: [collect, prepare] + if: needs.collect.outputs.comments_found == 'true' && inputs.dry_run == false + runs-on: ubuntu-latest + outputs: + triggered: ${{ steps.dispatch.outputs.triggered }} + steps: + - name: Generate token (if App configured) + id: token + uses: actions/create-github-app-token@v1 + continue-on-error: true + with: + app-id: ${{ secrets.gh_app_id }} + private-key: ${{ secrets.gh_app_private_key }} + + - name: Resolve token + id: auth + env: + TOKEN_OUTPUT: ${{ steps.token.outputs.token }} + SERVICE_PAT: ${{ secrets.service_bot_pat }} + GITHUB_TOKEN: ${{ github.token }} + run: | + if [ -n "${TOKEN_OUTPUT}" ]; then + echo "token=${TOKEN_OUTPUT}" >> "$GITHUB_OUTPUT" + elif [ -n "${SERVICE_PAT}" ]; then + echo "token=${SERVICE_PAT}" >> "$GITHUB_OUTPUT" + else + echo "token=${GITHUB_TOKEN}" >> "$GITHUB_OUTPUT" + fi + + - name: Post agent command comment + id: dispatch + uses: actions/github-script@v7 + with: + github-token: ${{ steps.auth.outputs.token }} + script: | + const prNumber = parseInt('${{ inputs.pr_number }}'); + const agent = '${{ needs.collect.outputs.agent }}'; + const count = '${{ needs.collect.outputs.comments_count }}'; + + // Post a comment that triggers the agent via pr-meta workflow + // This uses the existing concurrency/dispatch infrastructure + const comment = `@${agent} Please address the ${count} bot review comment(s) on this PR. + + Focus on: + 1. Implementing suggested fixes that improve the code + 2. Skipping suggestions that don't apply (note why in your response) + + The bot comment handler workflow has prepared context for you.`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: comment + }); + + core.setOutput('triggered', 'true'); + console.log(`Posted @${agent} command on PR #${prNumber}`); + + # Summary + summary: + name: Generate summary + needs: [collect, dispatch] + if: always() + runs-on: ubuntu-latest + steps: + - name: Final summary + env: + COMMENTS_COUNT: ${{ needs.collect.outputs.comments_count }} + AGENT: ${{ needs.collect.outputs.agent }} + TRIGGERED: ${{ needs.dispatch.outputs.triggered }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + { + echo "## Bot Comment Handler Results" + echo "" + echo "| Metric | Value |" + echo "|--------|-------|" + echo "| Comments Found | ${COMMENTS_COUNT:-0} |" + echo "| Agent | ${AGENT:-N/A} |" + echo "| Agent Triggered | ${TRIGGERED:-false} |" + echo "| Dry Run | ${DRY_RUN} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/bot-comment-handler.md b/docs/bot-comment-handler.md new file mode 100644 index 000000000..6f5acccf5 --- /dev/null +++ b/docs/bot-comment-handler.md @@ -0,0 +1,164 @@ +# Bot Comment Handler + +Automatically addresses review comments from bots (Copilot, CodeRabbit, etc.) using the configured AI coding agent. + +## Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ reusable-bot-comment-handler.yml (Workflows repo) β”‚ +β”‚ - Collects unresolved bot comments via GitHub API β”‚ +β”‚ - Detects agent from PR labels (agent:codex, agent:claude) β”‚ +β”‚ - Posts @agent command to trigger fix β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + Label trigger Gate completion Manual dispatch + (one-off PRs) (agent PRs) (testing) +``` + +## Triggers + +| Trigger | When | Use Case | +|---------|------|----------| +| `autofix:bot-comments` label | Manual | One-off PRs, ad-hoc fixes | +| Gate workflow completion | Automatic | Agent PRs (has `agent:*` label) | +| `workflow_dispatch` | Manual | Testing, debugging | + +## Agent Selection + +The workflow reads the PR's labels to determine which agent to use: + +| Label | Agent | Workflow | +|-------|-------|----------| +| `agent:codex` | Codex CLI | `reusable-codex-run.yml` | +| `agent:claude` | Claude | `reusable-claude-run.yml` | +| `agent:gemini` | Gemini | `reusable-gemini-run.yml` | +| (none) | Codex (default) | `reusable-codex-run.yml` | + +**To switch agents:** Change the PR label. No workflow changes needed. + +## Bot Authors + +By default, the workflow processes comments from: +- `copilot[bot]` - GitHub Copilot code review +- `github-actions[bot]` - GitHub Actions (lint, type check suggestions) +- `coderabbitai[bot]` - CodeRabbit AI review + +Configure via the `bot_authors` input. + +## Behavior + +### What Gets Processed + +- βœ… Unresolved review comments from known bots +- βœ… Inline code suggestions +- ❌ Comments where a human has already replied (skipped by default) +- ❌ General PR comments (not inline reviews) +- ❌ Resolved threads + +### Agent Instructions + +The agent is instructed to: +1. **Fix** suggestions that improve the code +2. **Skip** suggestions that don't apply or are incorrect +3. **Document** decisions in the commit message + +### After Processing + +- Agent commits fixes with message documenting what was addressed vs skipped +- Summary posted to workflow run showing all comments found +- Skipped/complex items are highlighted in the summary for potential follow-up + +## Consumer Repo Setup + +### 1. Add the workflow + +Copy `agents-bot-comment-handler.yml` to `.github/workflows/`: + +```bash +curl -sL https://raw.githubusercontent.com/stranske/Workflows/main/templates/consumer-repo/.github/workflows/agents-bot-comment-handler.yml \ + -o .github/workflows/agents-bot-comment-handler.yml +``` + +### 2. Add the prompt template + +```bash +mkdir -p .github/codex/prompts +curl -sL https://raw.githubusercontent.com/stranske/Workflows/main/templates/consumer-repo/.github/codex/prompts/fix_bot_comments.md \ + -o .github/codex/prompts/fix_bot_comments.md +``` + +### 3. Create the label + +Create `autofix:bot-comments` label in your repository: +- **Name:** `autofix:bot-comments` +- **Color:** `#7057ff` (purple) +- **Description:** Trigger bot to address review bot comments + +## Usage + +### One-off PRs + +Add the `autofix:bot-comments` label to any PR with bot review comments. The workflow will: +1. Collect all unresolved bot comments +2. Post `@` command to trigger the agent +3. Remove the label after processing + +### Agent PRs (Automatic) + +For PRs created by agents (with `agent:*` labels), the workflow automatically runs after Gate completes: +1. Checks if Gate succeeded +2. Collects any bot review comments +3. Dispatches the agent to address them + +### Testing + +```bash +# Dry run - see what would be processed +gh workflow run agents-bot-comment-handler.yml -f pr_number=123 -f dry_run=true + +# Full run +gh workflow run agents-bot-comment-handler.yml -f pr_number=123 +``` + +## Configuration + +### Inputs + +| Input | Default | Description | +|-------|---------|-------------| +| `pr_number` | (required) | PR number to process | +| `dry_run` | `false` | Preview without triggering agent | +| `bot_authors` | `copilot[bot],github-actions[bot],coderabbitai[bot]` | Bot login names to process | +| `skip_if_human_replied` | `true` | Skip threads with human replies | + +### Secrets + +| Secret | Required | Description | +|--------|----------|-------------| +| `SERVICE_BOT_PAT` | No | PAT for service bot account | +| `GH_APP_ID` | No | GitHub App ID (alternative auth) | +| `GH_APP_PRIVATE_KEY` | No | GitHub App private key | + +## Troubleshooting + +### No comments found + +- Check that bot authors match exactly (including `[bot]` suffix) +- Verify comments are review comments, not PR comments +- Check if threads were already resolved + +### Agent not triggered + +- Ensure `dry_run` is not enabled +- Check workflow permissions (needs `pull-requests: write`) +- Verify authentication tokens are configured + +### Gate trigger not working + +- Ensure PR has an `agent:*` label +- Check that Gate workflow completed successfully +- Verify workflow_run trigger is configured correctly diff --git a/docs/ci/WORKFLOWS.md b/docs/ci/WORKFLOWS.md index 5747799da..99e990bd8 100644 --- a/docs/ci/WORKFLOWS.md +++ b/docs/ci/WORKFLOWS.md @@ -78,6 +78,7 @@ pull_request ──▢ Gate ──▢ Summary comment & status | [`reusable-70-orchestrator-main.yml`](../../.github/workflows/reusable-70-orchestrator-main.yml) | None. | Consumes init outputs; reports via summaries/artifacts. | | [`reusable-agents-issue-bridge.yml`](../../.github/workflows/reusable-agents-issue-bridge.yml) | None. | Bridge emits PRs/comments only. | | [`reusable-agents-verifier.yml`](../../.github/workflows/reusable-agents-verifier.yml) | None. | Post-merge verification with CI wait logic; creates follow-up issues in consumer repos. | +| [`reusable-bot-comment-handler.yml`](../../.github/workflows/reusable-bot-comment-handler.yml) | `comments_found`, `comments_count`, `agent_triggered`. | Collects and dispatches agent to address bot review comments. | ## Pull Request Gate diff --git a/docs/ci/WORKFLOW_SYSTEM.md b/docs/ci/WORKFLOW_SYSTEM.md index 60f6b00e4..62cb93c45 100644 --- a/docs/ci/WORKFLOW_SYSTEM.md +++ b/docs/ci/WORKFLOW_SYSTEM.md @@ -386,7 +386,7 @@ fires where” without diving into the full tables: [workflow history](https://github.com/stranske/Trend_Model_Project/actions/workflows/agents-guard.yml). - **Error checking, linting, and testing topology** - **Primary workflows.** `reusable-10-ci-python.yml`, `reusable-12-ci-docker.yml`, - `reusable-16-agents.yml`, `reusable-18-autofix.yml`, `reusable-20-pr-meta.yml`, `reusable-agents-issue-bridge.yml`, `reusable-agents-verifier.yml`, `reusable-codex-run.yml`, and `selftest-reusable-ci.yml`. + `reusable-16-agents.yml`, `reusable-18-autofix.yml`, `reusable-20-pr-meta.yml`, `reusable-agents-issue-bridge.yml`, `reusable-agents-verifier.yml`, `reusable-bot-comment-handler.yml`, `reusable-codex-run.yml`, and `selftest-reusable-ci.yml`. - **Triggers.** Invoked via `workflow_call` by Gate, Gate summary job, and manual reruns. `selftest-reusable-ci.yml` handles the nightly rehearsal (cron at 06:30Β UTC) and manual publication modes via `workflow_dispatch`. diff --git a/templates/consumer-repo/.github/codex/prompts/fix_bot_comments.md b/templates/consumer-repo/.github/codex/prompts/fix_bot_comments.md new file mode 100644 index 000000000..bb2945e50 --- /dev/null +++ b/templates/consumer-repo/.github/codex/prompts/fix_bot_comments.md @@ -0,0 +1,43 @@ +# Fix Bot Review Comments + +Review bots have left suggestions on this PR. Your task is to address each one. + +## Instructions + +1. **Read each bot comment** in the sections below +2. **Implement suggested fixes** that improve code quality, correctness, or maintainability +3. **Skip suggestions** that are incorrect, don't apply, or would make the code worse +4. **Document your decisions** in your commit message + +## Guidelines + +### When to FIX a suggestion: +- It corrects a genuine bug or issue +- It improves type safety or error handling +- It follows established project patterns +- It improves readability or maintainability + +### When to SKIP a suggestion: +- It contradicts project conventions +- It's based on incomplete context +- The suggested change would break functionality +- It's purely stylistic with no clear benefit + +## After Addressing Comments + +1. **Commit** with message format: `fix: address bot review comments` +2. **Include** in your commit message which suggestions you addressed and skipped: + ``` + fix: address bot review comments + + Addressed: + - Fixed type annotation in auth.py + - Added null check in handler.py + + Skipped: + - Suggestion to rename variable (matches project convention) + ``` + +## Bot Comments + + diff --git a/templates/consumer-repo/.github/workflows/agents-bot-comment-handler.yml b/templates/consumer-repo/.github/workflows/agents-bot-comment-handler.yml new file mode 100644 index 000000000..cc451426b --- /dev/null +++ b/templates/consumer-repo/.github/workflows/agents-bot-comment-handler.yml @@ -0,0 +1,178 @@ +# Bot Comment Handler - Thin caller for consumer repos +# +# Addresses unresolved review comments from bots (Copilot, CodeRabbit, etc.) +# by dispatching the configured agent to fix them. +# +# Triggers: +# - PR labeled with 'autofix:bot-comments' (manual trigger) +# - Gate workflow completion (automatic for agent PRs) +# - Manual dispatch for testing +# +# Agent selection: +# - Uses PR's agent:* label (agent:codex, agent:claude, etc.) +# - Falls back to Codex if no agent label +# +# Workflow file: .github/workflows/agents-bot-comment-handler.yml + +name: Agents Bot Comment Handler + +on: + # Manual trigger via label + pull_request: + types: [labeled] + + # Automatic trigger after Gate completes (for agent PRs) + # Note: branches-ignore is not supported for workflow_run, filtering is done in job logic + workflow_run: + workflows: ["Gate"] + types: [completed] + + # Manual dispatch for testing + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to process' + required: true + type: string + dry_run: + description: 'Preview without making changes' + required: false + type: boolean + default: false + +permissions: + contents: read + pull-requests: write + issues: write + actions: read + +concurrency: + group: bot-comments-${{ github.event.pull_request.number || github.event.workflow_run.pull_requests[0].number || inputs.pr_number || github.run_id }} + cancel-in-progress: false + +jobs: + # Resolve PR number from different trigger types + resolve: + name: Resolve PR + runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.resolve.outputs.pr_number }} + should_run: ${{ steps.resolve.outputs.should_run }} + steps: + - name: Resolve PR number and check conditions + id: resolve + uses: actions/github-script@v7 + with: + script: | + const eventName = context.eventName; + let prNumber = null; + let shouldRun = false; + + if (eventName === 'workflow_dispatch') { + prNumber = '${{ inputs.pr_number }}'; + shouldRun = true; + console.log(`Manual dispatch for PR #${prNumber}`); + } + else if (eventName === 'pull_request') { + // Only run if labeled with autofix:bot-comments + const label = context.payload.label?.name; + if (label === 'autofix:bot-comments') { + prNumber = context.payload.pull_request.number; + shouldRun = true; + console.log(`Label trigger for PR #${prNumber}`); + } else { + console.log(`Ignoring label: ${label}`); + } + } + else if (eventName === 'workflow_run') { + // Only run if Gate succeeded and PR has agent:* label + const workflowRun = context.payload.workflow_run; + + // Skip main branch + if (workflowRun.head_branch === 'main') { + console.log('Skipping main branch'); + core.setOutput('should_run', 'false'); + return; + } + + if (workflowRun.conclusion !== 'success') { + console.log(`Gate did not succeed (${workflowRun.conclusion}), skipping`); + core.setOutput('should_run', 'false'); + return; + } + + // Get PR from workflow run + const prs = workflowRun.pull_requests; + if (!prs || prs.length === 0) { + console.log('No PR associated with workflow run'); + core.setOutput('should_run', 'false'); + return; + } + + prNumber = prs[0].number; + + // Check if PR has agent label + let pr; + try { + const response = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + pr = response.data; + } catch (error) { + console.log(`Failed to fetch PR #${prNumber} details, skipping. Error: ${error.message || error}`); + core.setOutput('should_run', 'false'); + return; + } + + const hasAgentLabel = pr.labels.some(l => /^agent:/.test(l.name)); + if (!hasAgentLabel) { + console.log(`PR #${prNumber} has no agent label, skipping`); + core.setOutput('should_run', 'false'); + return; + } + + shouldRun = true; + console.log(`Gate completion trigger for agent PR #${prNumber}`); + } + + core.setOutput('pr_number', prNumber || ''); + core.setOutput('should_run', shouldRun ? 'true' : 'false'); + + # Call the reusable workflow + handle: + name: Handle bot comments + needs: resolve + if: needs.resolve.outputs.should_run == 'true' + uses: stranske/Workflows/.github/workflows/reusable-bot-comment-handler.yml@main + with: + pr_number: ${{ needs.resolve.outputs.pr_number }} + dry_run: ${{ inputs.dry_run == true }} + secrets: + service_bot_pat: ${{ secrets.SERVICE_BOT_PAT }} + gh_app_id: ${{ secrets.GH_APP_ID }} + gh_app_private_key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + # Remove the trigger label after processing + cleanup: + name: Cleanup + needs: [resolve, handle] + if: always() && github.event_name == 'pull_request' && github.event.label.name == 'autofix:bot-comments' + runs-on: ubuntu-latest + steps: + - name: Remove trigger label + uses: actions/github-script@v7 + with: + script: | + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + name: 'autofix:bot-comments' + }); + console.log('Removed autofix:bot-comments label'); + } catch (error) { + console.log(`Could not remove label: ${error.message}`); + } diff --git a/tests/workflows/test_workflow_naming.py b/tests/workflows/test_workflow_naming.py index b44eb5480..671595254 100644 --- a/tests/workflows/test_workflow_naming.py +++ b/tests/workflows/test_workflow_naming.py @@ -209,6 +209,7 @@ def test_workflow_display_names_are_unique(): "reusable-70-orchestrator-main.yml": "Agents 70 Main (Reusable)", "reusable-agents-issue-bridge.yml": "Reusable Agents Issue Bridge", "reusable-agents-verifier.yml": "Reusable Agents Verifier", + "reusable-bot-comment-handler.yml": "Reusable Bot Comment Handler", "selftest-reusable-ci.yml": "Selftest: Reusables", "selftest-ci.yml": "Selftest CI", }