Skip to content

Copilot Autofix Dispatch #2248

Copilot Autofix Dispatch

Copilot Autofix Dispatch #2248

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 + ')');