-
Notifications
You must be signed in to change notification settings - Fork 74
197 lines (177 loc) · 10 KB
/
ai-pr-summary.yml
File metadata and controls
197 lines (177 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# ──────────────────────────────────────────────────────────────────────────────
# AI PR Summary — Consolidates all AI agent feedback into one clean verdict
# ──────────────────────────────────────────────────────────────────────────────
# Triggered after each AI agent workflow completes. Fetches all agent comments
# on the PR, parses verdicts, and posts/updates a single summary table.
# Uses an HTML marker (<!-- ai-pr-summary -->) for idempotent upserts.
# ──────────────────────────────────────────────────────────────────────────────
name: AI PR Summary
on:
workflow_run:
workflows:
- "AI Code Review"
- "AI Security Scan"
- "AI Breaking Change Detector"
- "AI Docs Sync Check"
- "AI Test Generator"
types: [completed]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
summarize:
name: Post unified summary
runs-on: ubuntu-latest
# Only run when the triggering workflow was for a PR
if: >-
github.event.workflow_run.event == 'pull_request' ||
github.event.workflow_run.event == 'pull_request_target'
steps:
- name: Get PR number from triggering workflow
id: pr
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// The workflow_run event contains the head SHA; find the PR for it
const runId = context.payload.workflow_run.id;
const owner = context.repo.owner;
const repo = context.repo.repo;
const headSha = context.payload.workflow_run.head_sha;
const headBranch = context.payload.workflow_run.head_branch;
// Try to find PR by head branch
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
head: `${owner}:${headBranch}`,
per_page: 5,
});
let prNumber = null;
if (prs.length > 0) {
prNumber = prs[0].number;
} else {
// Fallback: search by commit SHA
const { data: searchPrs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner,
repo,
commit_sha: headSha,
});
const openPr = searchPrs.find(p => p.state === 'open');
if (openPr) prNumber = openPr.number;
}
if (!prNumber) {
core.info('No open PR found for this workflow run — skipping summary.');
core.setOutput('found', 'false');
return;
}
core.info(`Found PR #${prNumber}`);
core.setOutput('found', 'true');
core.setOutput('number', String(prNumber));
- name: Collect agent comments and post summary
if: steps.pr.outputs.found == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = parseInt('${{ steps.pr.outputs.number }}', 10);
const SUMMARY_MARKER = '<!-- ai-pr-summary -->';
// ── Agent registry ──────────────────────────────────────────
// Maps agent-type marker to display info
const AGENTS = [
{ marker: '<!-- ai-agent:code-reviewer -->', icon: '🔍', label: 'Code Review' },
{ marker: '<!-- ai-agent:security-scanner -->', icon: '🛡️', label: 'Security Scan' },
{ marker: '<!-- ai-agent:breaking-change -->', icon: '🔄', label: 'Breaking Changes' },
{ marker: '<!-- ai-agent:docs-sync -->', icon: '📝', label: 'Docs Sync' },
{ marker: '<!-- ai-agent:test-generator -->', icon: '🧪', label: 'Test Coverage' },
];
// ── Fetch all comments on the PR ────────────────────────────
let allComments = [];
let page = 1;
while (true) {
const { data: batch } = await github.rest.issues.listComments({
owner, repo, issue_number: prNumber, per_page: 100, page,
});
if (!batch.length) break;
allComments = allComments.concat(batch);
if (batch.length < 100) break;
page++;
}
// ── Parse each agent's verdict ──────────────────────────────
function parseVerdict(body) {
if (!body) return { status: '⏳', statusLabel: 'Pending', detail: 'Awaiting results' };
const lower = body.toLowerCase();
// Check for critical / error first
if (/critical|error|vulnerabilit(y|ies)\s+found|high\s+severity/i.test(body)) {
// Try to extract a useful detail line
const detailMatch = body.match(/^###\s+(.+)$/m) || body.match(/^.*(?:critical|error|vulnerab).*$/mi);
return { status: '❌', statusLabel: 'Failed', detail: detailMatch ? detailMatch[1] || detailMatch[0] : 'Issues detected' };
}
// Warnings
if (/warning|⚠️|potential|breaking\s+change|minor/i.test(body)) {
const warnMatch = body.match(/(\d+)\s*warning/i);
const detailMatch = body.match(/^###\s+(.+)$/m) || body.match(/^.*(?:warning|⚠️|potential|breaking).*$/mi);
const count = warnMatch ? warnMatch[1] : '';
const label = count ? `${count} Warning${count === '1' ? '' : 's'}` : 'Warning';
return { status: '⚠️', statusLabel: label, detail: detailMatch ? (detailMatch[1] || detailMatch[0]).replace(/^#+\s*/, '').trim() : 'See details' };
}
// Suggestions / info
if (/suggest|consider|recommend|ℹ️|info/i.test(body)) {
const detailMatch = body.match(/^###\s+(.+)$/m) || body.match(/^.*(?:suggest|consider|recommend).*$/mi);
return { status: 'ℹ️', statusLabel: 'Suggestion', detail: detailMatch ? (detailMatch[1] || detailMatch[0]).replace(/^#+\s*/, '').trim() : 'See suggestions' };
}
// Passed / clean
if (/no issues|pass(ed)?|clean|no vulnerabilit|up.to.date|no breaking|all good|looks good|lgtm/i.test(body)) {
const detailMatch = body.match(/^.*(?:no issues|pass|clean|no vulnerab|up.to.date|no breaking|all good|looks good).*$/mi);
return { status: '✅', statusLabel: 'Passed', detail: detailMatch ? detailMatch[0].replace(/^#+\s*/, '').trim().slice(0, 80) : 'No issues found' };
}
// Default: completed but unclear verdict
return { status: '✅', statusLabel: 'Completed', detail: 'Analysis complete' };
}
// Build rows
const rows = AGENTS.map(agent => {
const comment = allComments.find(c => c.body && c.body.includes(agent.marker));
const verdict = comment ? parseVerdict(comment.body) : { status: '⏳', statusLabel: 'Pending', detail: 'Awaiting results' };
return `| ${agent.icon} ${agent.label} | ${verdict.status} ${verdict.statusLabel} | ${verdict.detail} |`;
});
// ── Determine overall verdict ───────────────────────────────
const allVerdicts = rows.join('\n');
let overallVerdict;
if (allVerdicts.includes('❌')) {
overallVerdict = '**Verdict: ❌ Changes needed — see failures above**';
} else if (allVerdicts.includes('⚠️')) {
overallVerdict = '**Verdict: ⚠️ Ready for human review — see warnings above**';
} else if (allVerdicts.includes('⏳')) {
overallVerdict = '**Verdict: ⏳ Still running — some checks have not completed yet**';
} else {
overallVerdict = '**Verdict: ✅ Ready for human review**';
}
// ── Build summary body ──────────────────────────────────────
const summaryBody = [
SUMMARY_MARKER,
'## ✅ PR Review Summary',
'',
'| Check | Status | Details |',
'|-------|--------|---------|',
...rows,
'',
overallVerdict,
'',
'> 💡 Individual agent reports are collapsed below for reference.',
].join('\n');
// ── Upsert summary comment ──────────────────────────────────
const existing = allComments.find(c => c.body && c.body.includes(SUMMARY_MARKER));
if (existing) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existing.id, body: summaryBody,
});
core.info(`Updated existing summary comment ${existing.id}`);
} else {
await github.rest.issues.createComment({
owner, repo, issue_number: prNumber, body: summaryBody,
});
core.info('Created new summary comment');
}