Files
public-apis/scripts/pr-review-automation.js
Juan Diaz 8dbb961dde Fix PR Review Automation for cross-repository PRs (#571)
* Add PR review automation workflow and script

closes #559

* Improve PR review automation with better error handling and permissions

* Update PR review automation to use pull_request_target event for improved security

* Fix PR automation to handle cross-repo references correctly

- Use SHA instead of ref names to avoid 404 errors
- Handle head content from source repo and base from target repo
- Add better error handling and debugging info
- Prevent workflow failures when API check fails
2025-07-11 17:01:56 -04:00

144 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { context, getOctokit } = require("@actions/github");
const fs = require("fs");
const token = process.env.GITHUB_TOKEN;
const octokit = getOctokit(token);
async function run() {
try {
const pr = context.payload.pull_request;
const { owner, repo } = context.repo;
const prNumber = pr.number;
console.log(`Working with repository: ${owner}/${repo}`);
console.log(`PR number: ${prNumber}`);
console.log(`PR author: ${pr.user.login}`);
console.log(`Head repo: ${pr.head.repo.full_name}`);
console.log(`Base repo: ${pr.base.repo.full_name}`);
console.log(`Head SHA: ${pr.head.sha}`);
console.log(`Base SHA: ${pr.base.sha}`);
console.log(`Head ref: ${pr.head.ref}`);
console.log(`Base ref: ${pr.base.ref}`);
const filesChanged = await octokit.rest.pulls.listFiles({
owner,
repo,
pull_number: prNumber,
});
const comments = [];
// Check 1: New API links in README.md
const readmeFile = filesChanged.data.find(
(file) => file.filename.toLowerCase() === "readme.md"
);
if (readmeFile) {
console.log("README.md modified. Checking for new API links...");
const newLinks = await checkForNewApiLinks(owner, repo, pr);
if (newLinks.length > 0) {
const linkComment =
newLinks.length === 1
? `**API link:** ${newLinks[0]}`
: [
"**New API links:**",
"",
...newLinks.map((link) => `- ${link}`),
].join("\n");
comments.push(linkComment);
}
}
// Check 2: Edits to /db folder
const dbFiles = filesChanged.data.filter((file) =>
file.filename.startsWith("db/")
);
if (dbFiles.length > 0) {
console.log(
`DB folder modifications detected in ${dbFiles.length} file(s)`
);
const dbWarning =
"Thanks for your contribution!\n❗ **Warning:** The `/db` folder is auto-generated, so please do not edit it. Changes related to public APIs should happen in the `README.md` file. Read the [contribution guidelines](https://github.com/marcelscruz/public-apis/blob/main/CONTRIBUTING.md) for more details.";
comments.push(dbWarning);
}
// Post all comments as a single comment
if (comments.length > 0) {
const finalComment = comments.join("\n\n---\n\n");
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: finalComment,
});
console.log("Comment posted with all checks.");
} else {
console.log("No issues found in this PR.");
}
} catch (error) {
console.error("Error in PR review automation:", error);
// Don't exit with error code to avoid failing the entire workflow
// Just log the error and continue
}
}
async function checkForNewApiLinks(owner, repo, pr) {
try {
// For pull_request_target, we need to get content from the correct repositories
// Base content from the target repository (upstream)
const baseRes = await octokit.rest.repos.getContent({
owner,
repo,
path: "README.md",
ref: pr.base.sha, // Use SHA instead of ref name
});
// Head content from the source repository (could be a fork)
const headRes = await octokit.rest.repos.getContent({
owner: pr.head.repo.owner.login,
repo: pr.head.repo.name,
path: "README.md",
ref: pr.head.sha, // Use SHA instead of ref name
});
const decode = (res) =>
Buffer.from(res.data.content, "base64").toString("utf8");
const baseContent = decode(baseRes);
const headContent = decode(headRes);
const baseLinks = new Set(
[...baseContent.matchAll(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g)].map(
(m) => m[2]
)
);
const headLinks = new Set(
[...headContent.matchAll(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g)].map(
(m) => m[2]
)
);
const newLinks = [...headLinks].filter((link) => !baseLinks.has(link));
console.log(`Base links found: ${baseLinks.size}`);
console.log(`Head links found: ${headLinks.size}`);
console.log(`New links found: ${newLinks.length}`);
if (newLinks.length > 0) {
console.log("New links:", newLinks);
}
return newLinks;
} catch (error) {
console.error("Error checking for new API links:", error);
return []; // Return empty array on error to avoid breaking the workflow
}
}
run().catch((error) => {
console.error(error);
process.exit(1);
});