Copilot Autofix Dispatch #2249
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: Copilot Autofix Dispatch | |
| # v2 (fixed via bug #1 resolution · validated by V-F03a2/F04a2): | |
| # Codex Cloud does auto-REVIEW but does NOT autopush (0-commit evidence by chatgpt-codex-connector[bot]). | |
| # Only Copilot Coding Agent (copilot-swe-agent bot) can autopush fix commits (L2 execute level). | |
| # | |
| # This workflow closes the autofix loop by automatically commenting `@copilot please fix ...` | |
| # on the PR whenever: | |
| # (a) a blocking review is submitted (bot comment with non-approve state), or | |
| # (b) one of our mechanical gates fails. | |
| # | |
| # The comment is posted as github-actions[bot]. PR must be assigned to Copilot first | |
| # for @copilot mention to be picked up (per docs.github.com/copilot/how-tos/use-copilot-agents/coding-agent/make-changes-to-an-existing-pr). | |
| # | |
| # Loop prevention: checks last N comments for 'copilot-autofix-dispatch: <head_sha>' marker | |
| # to avoid re-@'ing within same PR head SHA. | |
| on: | |
| pull_request_review: | |
| types: [submitted] | |
| workflow_run: | |
| workflows: | |
| - "CI" | |
| - "Semgrep" | |
| - "Trivy" | |
| - "Gitleaks" | |
| - "PR Lint" | |
| types: [completed] | |
| permissions: | |
| pull-requests: write | |
| actions: read | |
| concurrency: | |
| group: codex-autofix-${{ github.event.pull_request.number || github.event.workflow_run.pull_requests[0].number || github.run_id }} | |
| cancel-in-progress: false | |
| jobs: | |
| dispatch: | |
| runs-on: ubuntu-latest | |
| # Only run for real PRs and actual blocking signals. | |
| # Codex Cloud reviews arrive as state == 'commented' (not | |
| # 'changes_requested'), even when they raise P0/P1 issues. We | |
| # therefore trigger on any non-approval review from a bot (so | |
| # we don't dispatch on human comments). | |
| if: > | |
| (github.event_name == 'pull_request_review' | |
| && github.event.review.state != 'approved' | |
| && github.event.review.user.type == 'Bot' | |
| && github.event.review.user.login != 'github-actions[bot]') | |
| || | |
| (github.event_name == 'workflow_run' | |
| && github.event.workflow_run.conclusion == 'failure' | |
| && github.event.workflow_run.event == 'pull_request') | |
| steps: | |
| - name: Identify PR | |
| id: pr | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v8.0.0 | |
| with: | |
| script: | | |
| let prNumber = null; | |
| let trigger = ''; | |
| let details = ''; | |
| if (context.eventName === 'pull_request_review') { | |
| prNumber = context.payload.pull_request.number; | |
| trigger = 'blocking-review'; | |
| details = `Review by @${context.payload.review.user.login}: ${context.payload.review.body || '(no body)'}`; | |
| } else if (context.eventName === 'workflow_run') { | |
| const prs = context.payload.workflow_run.pull_requests || []; | |
| if (prs.length === 0) { | |
| core.setOutput('skip', 'true'); | |
| core.notice('workflow_run had no associated PR; skipping'); | |
| return; | |
| } | |
| prNumber = prs[0].number; | |
| trigger = 'ci-failure'; | |
| details = `Workflow "${context.payload.workflow_run.name}" failed on run ${context.payload.workflow_run.id}`; | |
| } | |
| if (!prNumber) { | |
| core.setOutput('skip', 'true'); | |
| return; | |
| } | |
| core.setOutput('pr_number', prNumber); | |
| core.setOutput('trigger', trigger); | |
| core.setOutput('details', details); | |
| - name: Check if Codex was already asked for this head SHA | |
| id: loopcheck | |
| if: steps.pr.outputs.skip != 'true' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v8.0.0 | |
| with: | |
| script: | | |
| const prNumber = parseInt('${{ steps.pr.outputs.pr_number }}', 10); | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| }); | |
| const headSha = pr.data.head.sha; | |
| core.setOutput('head_sha', headSha); | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| per_page: 30, | |
| }); | |
| // Loop guard: skip if there is already a dispatch marker | |
| // for this head SHA. | |
| const marker = `copilot-autofix-dispatch: ${headSha}`; | |
| const already = comments.data.some(c => (c.body || '').includes(marker)); | |
| core.setOutput('skip', already ? 'true' : 'false'); | |
| if (already) core.notice(`already dispatched for head ${headSha}, skipping`); | |
| - name: Post Codex dispatch comment | |
| if: steps.pr.outputs.skip != 'true' && steps.loopcheck.outputs.skip != 'true' | |
| env: | |
| PR_NUMBER: ${{ steps.pr.outputs.pr_number }} | |
| TRIGGER: ${{ steps.pr.outputs.trigger }} | |
| DETAILS: ${{ steps.pr.outputs.details }} | |
| HEAD_SHA: ${{ steps.loopcheck.outputs.head_sha }} | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v8.0.0 | |
| with: | |
| script: | | |
| const prNumber = parseInt(process.env.PR_NUMBER, 10); | |
| const trigger = process.env.TRIGGER || ''; | |
| const details = process.env.DETAILS || '(no details)'; | |
| const headSha = process.env.HEAD_SHA || ''; | |
| const body = [ | |
| '@copilot please address the blocking signals on this PR and push a fix commit.', | |
| '', | |
| '**Trigger**: ' + trigger, | |
| '**Details**: ' + details, | |
| '', | |
| 'Follow the PR\'s `.github/copilot-instructions.md` hard rules. Do not suppress lints, skip tests, or weaken CI.', | |
| '', | |
| '<!-- copilot-autofix-dispatch: ' + headSha + ' -->', | |
| ].join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body, | |
| }); | |
| core.notice('Copilot dispatched on PR #' + prNumber + ' (head ' + headSha + ')'); |