Skip to content
Open
Show file tree
Hide file tree
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
11 changes: 11 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## What does this PR change?
<!-- Briefly describe your changes -->

## Why is this needed?
<!-- Explain the motivation or issue -->

## Checklist
- [ ] Code runs locally
- [ ] Tests pass
- [ ] No hardcoded secrets
- [ ] Documentation updated
Comment on lines +1 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix markdownlint violations in the PR template.

Line 1 should be a top-level heading, Line 7 should have a blank line after it, and the file should end with a single trailing newline.

💡 Suggested patch
-## What does this PR change?
+# What does this PR change?
 <!-- Briefly describe your changes -->
 
 ## Why is this needed?
 <!-- Explain the motivation or issue -->
 
 ## Checklist
+
 - [ ] Code runs locally
 - [ ] Tests pass
 - [ ] No hardcoded secrets
 - [ ] Documentation updated
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)


[warning] 7-7: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 11-11: Files should end with a single newline character

(MD047, single-trailing-newline)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/pull_request_template.md around lines 1 - 11, Change the first
heading "## What does this PR change?" to a top-level heading "# What does this
PR change?", add a blank line immediately after the "## Checklist" heading to
satisfy markdownlint's required blank line after headings, and ensure the file
ends with exactly one trailing newline; update the headings and trailing-newline
in the .github/pull_request_template.md content (look for the "What does this PR
change?", "Why is this needed?", and "## Checklist" lines) to apply these fixes.

114 changes: 114 additions & 0 deletions .github/workflows/peer-review-reminder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: Peer Review Reminder

on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
pull_request_review:
types: [submitted, dismissed]

permissions:
pull-requests: write

jobs:
check-peer-review:
runs-on: ubuntu-latest
name: Check PR has a peer review
steps:
- name: Check for approved peer review and comment if missing
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const repo = context.repo;
const pr = context.payload.pull_request;
const prNumber = pr.number;

// Skip draft PRs
if (pr.draft) {
console.log(`PR #${prNumber} is a draft, skipping peer review check`);
return;
}

// Check for exempt users (bots and maintainers)
const exemptUsers = ['A1L13N', 'copilot-swe-agent[bot]', 'copilot[bot]', 'github-copilot[bot]', 'dependabot[bot]'];
if (exemptUsers.includes(pr.user.login)) {
console.log(`PR author ${pr.user.login} is exempt from peer review requirement`);
return;
}

// Check if PR branch is a copilot branch
if (pr.head.ref.startsWith('copilot/')) {
console.log(`PR branch ${pr.head.ref} starts with 'copilot/', exempt from peer review requirement`);
return;
}

// Fetch all reviews for this PR
const { data: reviews } = await github.rest.pulls.listReviews({
owner: repo.owner,
repo: repo.repo,
pull_number: prNumber,
});
Comment on lines +46 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/peer-review-reminder.yml

Repository: alphaonelabs/scholarai

Length of output: 5167


🌐 Web query:

GitHub API listReviews default page size pagination

💡 Result:

For the REST endpoint “List reviews for a pull request” (GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews):

  • Default page size: per_page=30 (max 100) [1]
  • Default page: page=1 [1]
  • Pagination: request additional pages via ?page=N (and optionally per_page=...), or follow the response Link header (rel="next", rel="last", etc.) [2]

Example:

GET /repos/OWNER/REPO/pulls/123/reviews?per_page=100&page=2

Sources: [1] [2]


🌐 Web query:

@actions/github-script paginate function available v7

💡 Result:

Yes. In actions/github-script@v7, the Octokit pagination helper is still available as github.paginate (it’s explicitly noted as unchanged). (github.com)

Example:

- uses: actions/github-script@v7
  with:
    script: |
      const issues = await github.paginate(
        github.rest.issues.listForRepo,
        { owner: context.repo.owner, repo: context.repo.repo, state: "open", per_page: 100 }
      )
      core.info(`Fetched ${issues.length} issues`)

Citations:


Paginate reviews and comments to avoid false reminders on busy PRs.

The GitHub API defaults to 30 items per page for both listReviews and listComments endpoints. On PRs with more than 30 reviews or comments, the script may miss approvals on later pages (posting false reminders) or fail to detect previous reminder comments (posting duplicates).

Replace the destructured calls with github.paginate() at lines 46–50 and 61–65:

💡 Suggested patch
-            const { data: reviews } = await github.rest.pulls.listReviews({
-              owner: repo.owner,
-              repo: repo.repo,
-              pull_number: prNumber,
-            });
+            const reviews = await github.paginate(github.rest.pulls.listReviews, {
+              owner: repo.owner,
+              repo: repo.repo,
+              pull_number: prNumber,
+              per_page: 100,
+            });
...
-            const { data: comments } = await github.rest.issues.listComments({
-              owner: repo.owner,
-              repo: repo.repo,
-              issue_number: prNumber,
-            });
+            const comments = await github.paginate(github.rest.issues.listComments, {
+              owner: repo.owner,
+              repo: repo.repo,
+              issue_number: prNumber,
+              per_page: 100,
+            });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/peer-review-reminder.yml around lines 46 - 50, The current
calls using github.rest.pulls.listReviews and github.rest.issues.listComments
destructure a single page ({ data: reviews } / { data: comments }) and will miss
items beyond the first page; replace both with github.paginate(...) to fetch all
pages for reviews and comments (use
github.paginate(github.rest.pulls.listReviews, { owner: repo.owner, repo:
repo.repo, pull_number: prNumber }) and
github.paginate(github.rest.issues.listComments, { owner: repo.owner, repo:
repo.repo, issue_number: prNumber })) and assign their full arrays to the
existing reviews and comments variables so the reminder logic uses complete
datasets and avoids duplicate or false reminders.


// Check if there is at least one APPROVED review from someone other than the PR author
const approvedReview = reviews.find(
review => review.state === 'APPROVED' && review.user.login !== pr.user.login
);
Comment on lines +53 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/peer-review-reminder.yml

Repository: alphaonelabs/scholarai

Length of output: 5167


🌐 Web query:

GitHub REST API pulls listReviews multiple reviews same user documentation

💡 Result:

GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews returns all review submissions, in chronological order, not a deduplicated “one review per user” list—so it’s normal to see multiple entries from the same user if they reviewed more than once (e.g., “CHANGES_REQUESTED” then later “APPROVED”). [1]

If you need “latest review per user”, you have to post-process the list (group by user.login, take the one with the greatest submitted_at / last in the array). If you specifically need the latest review by a specific user more efficiently, GitHub’s GraphQL API supports querying PR reviews filtered by author and limited to last: 1. [2]

Sources: [1] GitHub REST docs – Pull request reviews (“List reviews… returns in chronological order”) ; [2] GitHub Community discussion with GraphQL approach for “latest review by user”.


Track each reviewer's latest state instead of matching the first approval.

The GitHub API's listReviews endpoint returns all historical reviews in chronological order, including multiple entries per reviewer. When using find() to match the first APPROVED review, the code may select an outdated approval even if that reviewer later requested changes or dismissed their review. This causes the reminder to be incorrectly removed or skipped.

Build a Map of the latest review state per reviewer by processing reviews sorted by submitted_at, then check for approval in that deduplicated set.

💡 Suggested patch
-            const approvedReview = reviews.find(
-              review => review.state === 'APPROVED' && review.user.login !== pr.user.login
-            );
+            const latestReviewStateByUser = new Map();
+            for (const review of reviews.sort(
+              (a, b) => new Date(a.submitted_at) - new Date(b.submitted_at)
+            )) {
+              const reviewer = review.user?.login;
+              if (!reviewer || reviewer === pr.user.login) continue;
+              latestReviewStateByUser.set(reviewer, review.state);
+            }
+            const approvedReviewer = [...latestReviewStateByUser.entries()]
+              .find(([, state]) => state === 'APPROVED')?.[0];
 
-            if (approvedReview) {
-              console.log(`PR #${prNumber} has an approved review from ${approvedReview.user.login}`);
+            if (approvedReviewer) {
+              console.log(`PR #${prNumber} has an approved review from ${approvedReviewer}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const approvedReview = reviews.find(
review => review.state === 'APPROVED' && review.user.login !== pr.user.login
);
const latestReviewStateByUser = new Map();
for (const review of reviews.sort(
(a, b) => new Date(a.submitted_at) - new Date(b.submitted_at)
)) {
const reviewer = review.user?.login;
if (!reviewer || reviewer === pr.user.login) continue;
latestReviewStateByUser.set(reviewer, review.state);
}
const approvedReviewer = [...latestReviewStateByUser.entries()]
.find(([, state]) => state === 'APPROVED')?.[0];
if (approvedReviewer) {
console.log(`PR #${prNumber} has an approved review from ${approvedReviewer}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/peer-review-reminder.yml around lines 53 - 55, The current
logic uses reviews.find(...) to locate an 'APPROVED' review which can pick an
outdated approval; instead, iterate the reviews sorted by submitted_at and build
a Map keyed by review.user.login storing each reviewer’s latest review.state,
then check that deduplicated map for any reviewer (excluding pr.user.login)
whose final state is 'APPROVED' to decide approval; update the code around the
reviews variable and the approvedReview lookup (replace the find usage) to
perform sorting by submitted_at, populating the Map, and checking the Map for
final approvals.


const botUserName = 'github-actions[bot]';
const commentMarker = '<!-- peer-review-reminder -->';

// Fetch existing comments
const { data: comments } = await github.rest.issues.listComments({
owner: repo.owner,
repo: repo.repo,
issue_number: prNumber,
});

const previousComment = comments.find(
comment => comment.user.login === botUserName && comment.body.includes(commentMarker)
);

if (approvedReview) {
console.log(`PR #${prNumber} has an approved review from ${approvedReview.user.login}`);

// Remove the reminder comment if it exists and PR now has approval
if (previousComment) {
await github.rest.issues.deleteComment({
owner: repo.owner,
repo: repo.repo,
comment_id: previousComment.id,
});
console.log(`Removed peer review reminder comment from PR #${prNumber}`);
}
return;
}

// No approved review found — post a reminder if not already posted
if (previousComment) {
console.log(`Already commented about missing peer review on PR #${prNumber}, skipping duplicate`);
return;
}

const message = [
commentMarker,
'## 👀 Peer Review Required',
'',
`Hi @${pr.user.login}! This pull request does not yet have a **peer review**.`,
'',
'Before this PR can be merged, please request a review from one of your peers:',
'',
'- Go to the PR page and click **"Reviewers"** on the right sidebar.',
'- Select a team member or contributor to review your changes.',
'- Once they approve, this reminder will be automatically removed.',
'',
'Thank you for contributing! 🎉',
].join('\n');

await github.rest.issues.createComment({
owner: repo.owner,
repo: repo.repo,
issue_number: prNumber,
body: message,
});

console.log(`Posted peer review reminder on PR #${prNumber}`);
19 changes: 19 additions & 0 deletions .github/workflows/pr-title-format-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: PR Title Format Check

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

jobs:
check-title:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prTitle = context.payload.pull_request.title;
const valid = /^(fix|feat|docs|add|update):/i.test(prTitle);
if (!valid) {
throw new Error('PR title must start with fix:, feat:, docs:, add:, or update:');
}