Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions .github/workflows/pr-review-agent.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: PR Review Agent

on:
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: read
pull-requests: write

jobs:
summarize-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Summarize PR and post comment
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr_number = context.payload.pull_request.number;
const { owner, repo } = context.repo;

// Fetch PR metadata
const { data: pr } = await github.rest.pulls.get({
owner, repo, pull_number: pr_number,
});

// Fetch changed files (up to 100)
const { data: files } = await github.rest.pulls.listFiles({
owner, repo, pull_number: pr_number, per_page: 100,
});

// Build a compact file-change list
const fileLines = files.map(
f => `- \`${f.filename}\` (${f.status}): +${f.additions} −${f.deletions}`
);

// Collect diff patches for the first 15 text files
const patches = files
.filter(f => f.patch)
.slice(0, 15)
.map(f => `### \`${f.filename}\`\n\`\`\`diff\n${f.patch.slice(0, 2000)}\n\`\`\``)
.join('\n\n');

const userPrompt = [
'You are a senior engineer reviewing a pull request for AccessHub, a TypeScript',
'internal developer platform (Express + Prisma + SQLite API, React + Vite frontend).',
'',
'PR title: ' + pr.title,
'PR description: ' + (pr.body || '(none)'),
'',
'Changed files (' + files.length + ' total):',
fileLines.join('\n'),
'',
'Diff excerpts:',
patches || '(no text patches available)',
'',
'Write a concise PR summary using these sections:',
'## Summary',
'One or two sentences describing what this PR does.',
'',
'## Changes',
'Bullet-point list of the most important changes.',
'',
'## Potential concerns',
'Flag any security, architecture, or testing issues relevant to this codebase.',
'If there are none, say "None identified."',
].join('\n');

let body;

try {
const token = process.env.GITHUB_TOKEN;
const resp = await fetch('https://models.inference.ai.azure.com/chat/completions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: userPrompt }],
max_tokens: 1024,
}),
});

if (!resp.ok) {
const text = await resp.text();
throw new Error('Models API ' + resp.status + ': ' + text);
}

const json = await resp.json();
const aiSummary = json.choices[0].message.content.trim();
body = '## \uD83E\uDD16 PR Review Agent\n\n' + aiSummary +
'\n\n---\n*Generated automatically by the PR Review Agent.*';
} catch (err) {
// Fallback: structured summary without AI
core.warning('AI summarization unavailable (' + err.message + '); using structured fallback.');
body = [
'## \uD83E\uDD16 PR Review Agent',
'',
'## Summary',
'This PR — **' + pr.title + '** — modifies ' + files.length + ' file(s).',
'',
'## Changes',
fileLines.join('\n'),
'',
'## Potential concerns',
'_AI summarization was unavailable; please review manually._',
'',
'---',
'*Generated automatically by the PR Review Agent.*',
].join('\n');
}

// Remove any previous bot summary comment to keep the thread clean
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: pr_number,
});
for (const c of comments) {
if (
c.user?.type === 'Bot' &&
c.body?.includes('PR Review Agent')
) {
await github.rest.issues.deleteComment({
owner, repo, comment_id: c.id,
});
}
}

// Post the new summary
await github.rest.issues.createComment({
owner, repo, issue_number: pr_number, body,
});
Loading