Skip to content

Remove edge middleware for routing Markdown requests #5

Remove edge middleware for routing Markdown requests

Remove edge middleware for routing Markdown requests #5

Workflow file for this run

name: Auto PR Review
on:
pull_request:
types: [opened, ready_for_review]
jobs:
review_pr:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Review PR with Oz agent
uses: warpdotdev/oz-agent-action@v1
env:
GH_TOKEN: ${{ github.token }}
with:
warp_api_key: ${{ secrets.WARP_API_KEY }}
skill: review-docs-pr
- name: Post Review
uses: actions/github-script@v7
if: always()
with:
script: |
const fs = require('fs');
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const commitSha = context.payload.pull_request.head.sha;
try {
if (!fs.existsSync('review.json')) {
console.log('No review.json found. Skipping review posting.');
return;
}
const reviewContent = fs.readFileSync('review.json', 'utf8');
let review;
try {
review = JSON.parse(reviewContent);
} catch (parseError) {
core.warning(`Failed to parse review.json: ${parseError.message}`);
const sanitized = reviewContent.replace(/[\u0000-\u001F]+/g, ' ');
try {
review = JSON.parse(sanitized);
} catch (sanitizedError) {
core.setFailed(`Failed to parse review.json even after sanitizing: ${sanitizedError.message}`);
return;
}
}
const decodeNewlines = (text) => {
if (typeof text !== 'string') return text;
return text.replace(/\r\n/g, '\n').replace(/\\n/g, '\n');
};
const rawComments = Array.isArray(review.comments) ? review.comments : [];
// Fetch valid file paths from the PR
const prFiles = await github.paginate(
github.rest.pulls.listFiles,
{ owner, repo, pull_number: prNumber }
);
const validPaths = new Set(prFiles.map(f => f.filename));
// Build a map of valid diff line numbers per file and side.
// GitHub's createReview API only accepts line numbers that appear
// in the PR diff; comments targeting other lines cause a 422.
const validLines = new Map(); // key: "path:side" -> Set of line numbers
for (const file of prFiles) {
if (!file.patch) continue;
const rightLines = new Set();
const leftLines = new Set();
let oldLine = 0;
let newLine = 0;
for (const raw of file.patch.split('\n')) {
const hunkHeader = raw.match(/^@@ -(?:\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
if (hunkHeader) {
const parts = raw.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
oldLine = parseInt(parts[1], 10);
newLine = parseInt(parts[2], 10);
continue;
}
if (raw.startsWith('+')) {
rightLines.add(newLine);
newLine++;
} else if (raw.startsWith('-')) {
leftLines.add(oldLine);
oldLine++;
} else {
// Context line — valid on both sides
rightLines.add(newLine);
leftLines.add(oldLine);
newLine++;
oldLine++;
}
}
validLines.set(`${file.filename}:RIGHT`, rightLines);
validLines.set(`${file.filename}:LEFT`, leftLines);
}
const comments = [];
const displaced = []; // comments whose lines aren't in the diff
for (const c of rawComments) {
if (!c || typeof c !== 'object') continue;
if (typeof c.body !== 'string' || !c.body.trim()) continue;
if (typeof c.path !== 'string' || !c.path.trim()) continue;
// Normalize path
const normalizedPath = c.path.trim()
.replace(/^([ab]\/)*/, '')
.replace(/^\.\//, '');
if (!validPaths.has(normalizedPath)) {
console.log(`Skipping comment with invalid path: ${c.path} -> ${normalizedPath}`);
continue;
}
const line = Number(c.line);
if (!Number.isInteger(line) || line <= 0) {
console.log('Skipping comment with invalid line:', c);
continue;
}
let side = (c.side || 'RIGHT').toString().toUpperCase();
if (side !== 'LEFT' && side !== 'RIGHT') {
console.log(`Invalid side '${c.side}', defaulting to RIGHT`);
side = 'RIGHT';
}
const body = decodeNewlines(c.body);
// Validate that this line number exists in the diff for this file/side
const key = `${normalizedPath}:${side}`;
const lineSet = validLines.get(key);
if (!lineSet || !lineSet.has(line)) {
console.log(`Comment targets line ${line} (${side}) in ${normalizedPath} which is outside the diff — moving to summary.`);
displaced.push({ path: normalizedPath, line, side, body });
continue;
}
comments.push({ path: normalizedPath, line, side, body });
}
let summary = typeof review.summary === 'string' ? decodeNewlines(review.summary).trim() : '';
// Append displaced comments to the summary so feedback isn't lost
if (displaced.length > 0) {
const extra = displaced.map(d =>
`**${d.path}** (line ${d.line}):\n${d.body}`
).join('\n\n---\n\n');
const header = '\n\n---\n\n**Additional comments** (targeting lines outside the diff):\n\n';
summary = summary ? summary + header + extra : header.trimStart() + extra;
}
const hasSummary = summary.length > 0;
if (!hasSummary && comments.length === 0) {
console.log('No valid summary or inline comments found. Skipping review posting.');
return;
}
const payload = {
owner,
repo,
pull_number: prNumber,
commit_id: commitSha,
event: 'COMMENT',
};
if (hasSummary) {
payload.body = summary;
} else {
payload.body = 'Automated review by Oz Agent';
}
if (comments.length > 0) {
payload.comments = comments;
}
await github.rest.pulls.createReview(payload);
console.log('Review posted successfully.');
} catch (error) {
console.error('Failed to post review:', error);
core.setFailed(`Failed to post review: ${error.message}`);
}