Skip to content

Commit 318b86b

Browse files
authored
feat: add draft PR ready-for-review reminder bot (hiero-ledger#1786)
Signed-off-by: Parv Ninama <ninamaparv@gmail.com> Signed-off-by: Parv <ninamaparv@gmail.com>
1 parent 9467983 commit 318b86b

File tree

3 files changed

+254
-0
lines changed

3 files changed

+254
-0
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/**
2+
* PR Draft Ready Reminder Bot
3+
*
4+
* Triggers when commits are pushed to a draft PR that has
5+
* CHANGES_REQUESTED reviews, and posts a reminder comment.
6+
*
7+
* Safety:
8+
* - Runs on pull_request_target (secure pattern)
9+
* - Skips non-draft PRs
10+
* - Skips bot-authored PRs
11+
* - Skips if reviewDecision !== CHANGES_REQUESTED
12+
* - Prevents duplicate comments via marker
13+
*/
14+
15+
const COMMENT_MARKER = "<!-- draft-ready-reminder-bot -->";
16+
17+
/**
18+
* Checks if the reminder comment already exists.
19+
* Uses GitHub pagination to safely scan all comments.
20+
*
21+
* @param {import("@actions/github").GitHub} params.github - Authenticated GitHub client.
22+
* @param {string} params.owner - Repository owner.
23+
* @param {string} params.repo - Repository name.
24+
* @param {number} params.issueNumber - Pull request number.
25+
* @param {string} params.marker - Unique marker string to detect duplicate comments.
26+
* @returns {Promise<boolean>} - True if a comment with the marker exists.
27+
*/
28+
29+
async function commentExists({ github, owner, repo, issueNumber, marker }) {
30+
console.log("Checking for existing reminder comments...");
31+
32+
let scanned = 0;
33+
const MAX_COMMENTS = 500;
34+
35+
for await (const response of github.paginate.iterator(
36+
github.rest.issues.listComments,
37+
{
38+
owner,
39+
repo,
40+
issue_number: issueNumber,
41+
per_page: 100,
42+
}
43+
)) {
44+
for (const comment of response.data) {
45+
scanned++;
46+
if (comment.body?.includes(marker)) {
47+
console.log(`Found existing reminder comment (scanned ${scanned} comments).`);
48+
return true;
49+
}
50+
if (scanned >= MAX_COMMENTS) {
51+
console.log(`Reached scan limit (${MAX_COMMENTS} comments) — assuming no duplicate.`);
52+
return false;
53+
}
54+
}
55+
}
56+
57+
console.log(`No existing reminder comment found (scanned ${scanned} comments).`);
58+
return false;
59+
}
60+
61+
/**
62+
* Builds the draft-ready reminder comment body.
63+
*
64+
* @param {string} username - PR author username.
65+
* @returns {string} - Formatted reminder message.
66+
*/
67+
function buildReminderComment(username) {
68+
return `
69+
${COMMENT_MARKER}
70+
👋 Hi @${username},
71+
72+
We noticed your pull request has had *recent changes pushed* after *changes were requested*.
73+
74+
If these updates address the feedback, you can:
75+
- resolve any open review conversations (reply if clarification is needed, or mark them as resolved),
76+
- click **“Ready for review”** (recommended), or
77+
- use the \`/review\` command.
78+
79+
Thanks for keeping things moving! 🙌
80+
— Hiero SDK Automation Team
81+
`.trim();
82+
}
83+
84+
/**
85+
* Main entry point for the PR Draft Ready Reminder Bot.
86+
*
87+
* This function:
88+
* 1. Resolves the PR number from the event context or environment.
89+
* 2. Ensures the PR is a draft and not bot-authored.
90+
* 3. Checks whether the review decision is CHANGES_REQUESTED.
91+
* 4. Prevents duplicate reminder comments.
92+
* 5. Posts a reminder comment.
93+
*/
94+
95+
module.exports = async function ({ github, context }) {
96+
97+
// Resolve PR number from event or workflow_dispatch
98+
const prNumber =
99+
context.payload?.pull_request?.number ||
100+
Number(process.env.PR_NUMBER);
101+
102+
if (!prNumber) {
103+
console.log("No PR number found in context or environment — exiting.");
104+
return;
105+
}
106+
107+
const { owner, repo } = context.repo;
108+
109+
console.log(`Processing PR #${prNumber} in ${owner}/${repo}`);
110+
111+
// Fetch PR details
112+
let pr;
113+
try {
114+
console.log("Fetching PR details...");
115+
({ data: pr } = await github.rest.pulls.get({
116+
owner,
117+
repo,
118+
pull_number: prNumber,
119+
}));
120+
} catch (err) {
121+
console.log(`Failed to fetch PR #${prNumber} in ${owner}/${repo}: ${err.message}`);
122+
return;
123+
}
124+
125+
console.log(`PR state → draft=${pr.draft}, author=${pr.user.login}, type=${pr.user?.type}`);
126+
127+
// Early exit: only draft PRs
128+
if (!pr.draft) {
129+
console.log("PR is not a draft — skipping reminder.");
130+
return;
131+
}
132+
133+
// Early exit: Bot authored PRs
134+
if (pr.user?.type === "Bot") {
135+
console.log("PR authored by bot — skipping reminder.");
136+
return;
137+
}
138+
139+
// Fetch reviewDecision via GraphQL
140+
console.log("Fetching reviewDecision via GraphQL...");
141+
142+
const query = `
143+
query ($owner: String!, $repo: String!, $number: Int!) {
144+
repository(owner: $owner, name: $repo) {
145+
pullRequest(number: $number) {
146+
reviewDecision
147+
}
148+
}
149+
}
150+
`;
151+
152+
let reviewDecision;
153+
154+
try {
155+
const result = await github.graphql(query, {
156+
owner,
157+
repo,
158+
number: prNumber,
159+
});
160+
161+
reviewDecision = result?.repository?.pullRequest?.reviewDecision;
162+
163+
if (!reviewDecision) {
164+
console.log("No reviewDecision returned — skipping.");
165+
return;
166+
}
167+
168+
console.log(`reviewDecision = ${reviewDecision}`);
169+
} catch (err) {
170+
console.log(`Failed to fetch reviewDecision for PR #${prNumber} in ${owner}/${repo} — skipping reminder.`);
171+
console.log(`Error: ${err.message}`);
172+
return;
173+
}
174+
175+
// Only trigger when changes were requested
176+
if (reviewDecision !== "CHANGES_REQUESTED") {
177+
console.log("reviewDecision is not CHANGES_REQUESTED — no reminder needed.");
178+
return;
179+
}
180+
181+
// Prevent duplicate comments
182+
let alreadyCommented = false;
183+
try {
184+
alreadyCommented = await commentExists({
185+
github,
186+
owner,
187+
repo,
188+
issueNumber: prNumber,
189+
marker: COMMENT_MARKER,
190+
});
191+
} catch (err) {
192+
console.log(`Failed to check existing comments on PR #${prNumber} in ${owner}/${repo}: ${err.message}`);
193+
console.log("Skipping reminder to avoid potential duplicate.");
194+
return;
195+
}
196+
197+
if (alreadyCommented) {
198+
console.log("Reminder already exists — skipping.");
199+
return;
200+
}
201+
try {
202+
await github.rest.issues.createComment({
203+
owner,
204+
repo,
205+
issue_number: prNumber,
206+
body: buildReminderComment(pr.user.login),
207+
});
208+
209+
console.log(`Reminder successfully posted on PR #${prNumber}`);
210+
} catch (error) {
211+
// Permission handling
212+
console.log("Failed to create reminder comment.");
213+
console.log(`PR #${prNumber} in ${owner}/${repo}`);
214+
console.log(`Error status: ${error.status}`);
215+
console.log(`Error message: ${error.message}`);
216+
}
217+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This workflow reminds draft PR authors to mark ready for review after pushing changes
2+
3+
name: PR Draft Ready Reminder Bot
4+
on:
5+
pull_request_target:
6+
types: [synchronize]
7+
8+
permissions:
9+
contents: read
10+
pull-requests: read
11+
issues: write
12+
13+
jobs:
14+
draft-ready-reminder:
15+
runs-on: ubuntu-latest
16+
concurrency:
17+
group: draft-ready-reminder-${{ github.event.pull_request.number }}
18+
cancel-in-progress: true
19+
steps:
20+
- name: Harden the runner
21+
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
22+
with:
23+
egress-policy: audit
24+
25+
- name: Checkout repository
26+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
27+
with:
28+
ref: main
29+
30+
- name: Run draft-ready reminder bot
31+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0
32+
with:
33+
github-token: ${{ secrets.GITHUB_TOKEN }}
34+
script: |
35+
const script = require('./.github/scripts/bot-pr-draft-ready-reminder.js');
36+
await script({ github, context });

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1919
- Format `tests/unit/endpoint_test.py` using black. (`#1792`)
2020

2121
### .github
22+
- Added GitHub Actions workflow to remind draft PR authors to mark ready for review after pushing changes. (#1722)
2223
- Fixed bot workflow runtime failure caused by strict `FAILED_WORKFLOW_NAME` validation. (`#1690`)
2324
- Reverted PR #1739 checking assignment counts
2425
- chore: update step-security/harden-runner from 2.14.1 to 2.14.2 in a workflow

0 commit comments

Comments
 (0)