Skip to content
Open
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
80 changes: 80 additions & 0 deletions .github/workflows/approve-uncontested-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Auto-approve uncontested PRs after 7 days

on:
schedule:
- cron: '26 5 * * *' # Run every day at 5:26 AM UTC (time chosen randomly to avoid creating peaks)
workflow_dispatch: # Allow manual triggering

permissions:
pull-requests: write

jobs:
approve-uncontested-prs:
runs-on: ubuntu-latest
steps:
- name: Auto-approve uncontested PRs
uses: actions/github-script@v7
with:
script: |
// Calculate the date 7 days ago
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

// Fetch all open pull requests, sorted by last update (oldest first)
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'asc'
});
Comment on lines +24 to +30
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing pagination handling will skip PRs in repositories with many open PRs.

The pulls.list API returns only 30 results by default (max 100 per page). If the repository has more open PRs than that, older PRs beyond the first page will never be processed.

Use github.paginate to iterate through all pages:

🔧 Proposed fix using pagination
-            // Fetch all open pull requests, sorted by last update (oldest first)
-            const { data: pullRequests } = await github.rest.pulls.list({
+            // Fetch all open pull requests, sorted by last update (oldest first)
+            const pullRequests = await github.paginate(github.rest.pulls.list, {
               owner: context.repo.owner,
               repo: context.repo.repo,
               state: 'open',
               sort: 'updated',
-              direction: 'asc'
+              direction: 'asc',
+              per_page: 100
             });
📝 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 { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'asc'
});
const pullRequests = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'asc',
per_page: 100
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/approve-uncontested-prs.yml around lines 24 - 30, The
current call to github.rest.pulls.list only returns a single page (default 30
PRs) and will miss PRs in large repositories; replace the single-page call with
GitHub's paginator (use github.paginate) to fetch all pull requests, e.g. call
github.paginate against github.rest.pulls.list (or its endpoint) with the same
params (owner, repo, state, sort, direction) and collect the results into the
pullRequests array used later; update references to the destructured { data:
pullRequests } to use the full array returned by paginate so downstream logic
(the code that iterates over pullRequests) processes every PR.


for (const pr of pullRequests) {
// Skip draft PRs
if (pr.draft) {
console.log(`Skipping PR #${pr.number}: Draft PR`);
continue;
}

// Use updated_at instead of created_at to ensure we don't auto-approve
// code that was just pushed to an old PR
const lastActivity = new Date(pr.updated_at);

// Only process PRs that haven't been updated in 7+ days
if (lastActivity < sevenDaysAgo) {
// Fetch reviews to check for objections and prior bot approval
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_request_number: pr.number
});

// Check if any reviewer has requested changes
const hasChangesRequested = reviews.some(
review => review.state === 'CHANGES_REQUESTED'
);

if (hasChangesRequested) {
console.log(`Skipping PR #${pr.number}: Has changes requested`);
continue;
}
Comment on lines +52 to +60
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Logic bug: Historical CHANGES_REQUESTED reviews block auto-approval even after they're resolved.

The GitHub API returns all reviews in history. If a reviewer requested changes but later approved (or their review was dismissed), both reviews appear in the list. The current logic will skip the PR because it finds a historical CHANGES_REQUESTED review, even though the objection was resolved.

Check only the most recent review per reviewer:

🐛 Proposed fix to check only latest review per reviewer
-            // Check if any reviewer has requested changes
-            const hasChangesRequested = reviews.some(
-              review => review.state === 'CHANGES_REQUESTED'
-            );
+            // Get the most recent review state per reviewer
+            const latestReviewByUser = new Map();
+            for (const review of reviews) {
+              if (review.user && review.state !== 'COMMENTED') {
+                latestReviewByUser.set(review.user.login, review.state);
+              }
+            }
+            
+            // Check if any reviewer's latest review requested changes
+            const hasChangesRequested = [...latestReviewByUser.values()].some(
+              state => state === 'CHANGES_REQUESTED'
+            );
📝 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
// Check if any reviewer has requested changes
const hasChangesRequested = reviews.some(
review => review.state === 'CHANGES_REQUESTED'
);
if (hasChangesRequested) {
console.log(`Skipping PR #${pr.number}: Has changes requested`);
continue;
}
// Get the most recent review state per reviewer
const latestReviewByUser = new Map();
for (const review of reviews) {
if (review.user && review.state !== 'COMMENTED') {
latestReviewByUser.set(review.user.login, review.state);
}
}
// Check if any reviewer's latest review requested changes
const hasChangesRequested = [...latestReviewByUser.values()].some(
state => state === 'CHANGES_REQUESTED'
);
if (hasChangesRequested) {
console.log(`Skipping PR #${pr.number}: Has changes requested`);
continue;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/approve-uncontested-prs.yml around lines 52 - 60, The
current hasChangesRequested check uses the entire reviews array and can pick up
historical CHANGES_REQUESTED entries; change the logic to consider only each
reviewer’s latest review: build a map from review.author.login (or
review.user.login) to their most recent review (compare review.submitted_at or
review.id), then set hasChangesRequested =
Array.from(latestReviews.values()).some(r => r.state === 'CHANGES_REQUESTED');
keep references to the existing reviews variable and the hasChangesRequested/
pr.number logic so the skip message remains unchanged.


// Check if already approved by this action to avoid spamming
const alreadyApproved = reviews.some(
review => review.user.login === 'github-actions[bot]' && review.state === 'APPROVED'
);
Comment on lines +62 to +65
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null safety for review.user to prevent runtime errors.

If a reviewer's account has been deleted, review.user will be null, causing review.user.login to throw a TypeError and crash the workflow.

🛡️ Proposed fix with null check
             // Check if already approved by this action to avoid spamming
             const alreadyApproved = reviews.some(
-              review => review.user.login === 'github-actions[bot]' && review.state === 'APPROVED'
+              review => review.user?.login === 'github-actions[bot]' && review.state === 'APPROVED'
             );
📝 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
// Check if already approved by this action to avoid spamming
const alreadyApproved = reviews.some(
review => review.user.login === 'github-actions[bot]' && review.state === 'APPROVED'
);
// Check if already approved by this action to avoid spamming
const alreadyApproved = reviews.some(
review => review.user?.login === 'github-actions[bot]' && review.state === 'APPROVED'
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/approve-uncontested-prs.yml around lines 62 - 65, The
check for existing approvals can throw when review.user is null; update the
alreadyApproved predicate to guard review.user before accessing
review.user.login (e.g., use review.user && review.user.login ===
'github-actions[bot]' or optional chaining like review.user?.login ===
'github-actions[bot]') and keep the review.state === 'APPROVED' check so
deleted-user reviews don't crash the workflow.


if (!alreadyApproved) {
// Submit an approval review
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_request_number: pr.number,
body: '✅ Auto-approving: This PR has been inactive for 7+ days with no objections.',
event: 'APPROVE'
});

console.log(`Approved PR #${pr.number}: ${pr.title}`);
}
}
}
Loading