-
Notifications
You must be signed in to change notification settings - Fork 299
feat: add PR draft explainer workflow #1907
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
manishdait
merged 15 commits into
hiero-ledger:main
from
parvninama:feat/pr-draft-explainer
Mar 13, 2026
+254
−0
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
d5a4626
feat: add PR draft explainer
parvninama 1da7fa6
Merge remote-tracking branch 'upstream/main' into feat/pr-draft-expla…
parvninama c75d2e1
fix: added dry run support
parvninama bf1a509
Merge branch 'main' into feat/pr-draft-explainer
parvninama c3884ec
chore: Update CHANGELOG.md
parvninama f2b1953
chore: Update bot-pr-draft-explainer.js
parvninama ff5a25d
chore: Update bot-pr-draft-explainer.js
parvninama 63e9309
fix: Update .github/scripts/bot-pr-draft-explainer.js
parvninama 5b3d6ec
Merge branch 'main' into feat/pr-draft-explainer
parvninama 78b84da
fix: add draft check and improve review-state detection in PR draft e…
parvninama 753efe7
Merge branch 'main' into feat/pr-draft-explainer
parvninama b9ffdbf
Merge branch 'main' into feat/pr-draft-explainer
parvninama 033a1c9
Merge branch 'main' into feat/pr-draft-explainer
parvninama 2a67fab
Merge branch 'main' into feat/pr-draft-explainer
parvninama 0d404ec
Merge branch 'main' into feat/pr-draft-explainer
parvninama File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| /** | ||
| * PR Draft Explainer Bot | ||
| * | ||
| * Triggers when a pull request is converted to draft. | ||
| * | ||
| * Safety: | ||
| * - Prevents duplicate comments using a unique HTML marker. | ||
| * - Only posts if a CHANGES_REQUESTED review exists. | ||
| * - Fails safely if review lookup fails. | ||
| * - Uses pagination to scan existing comments safely. | ||
| */ | ||
|
|
||
| const COMMENT_MARKER = "<!-- pr-draft-explainer -->"; | ||
| const DRY_RUN = process.env.DRY_RUN === "true"; | ||
| const manualPRNumber = process.env.PR_NUMBER; | ||
|
|
||
| /** | ||
| * Checks if the reminder comment already exists. | ||
| * Uses GitHub pagination to safely scan all comments. | ||
| * | ||
| * @param {import("@actions/github").GitHub} params.github - Authenticated GitHub client. | ||
| * @param {string} params.owner - Repository owner. | ||
| * @param {string} params.repo - Repository name. | ||
| * @param {number} params.issueNumber - Pull request number. | ||
| * @param {string} params.marker - Unique marker string to detect duplicate comments. | ||
| * @returns {Promise<boolean>} - True if a comment with the marker exists. | ||
| */ | ||
|
|
||
| async function commentExists({ github, owner, repo, issueNumber, marker }) { | ||
| console.log("Checking for existing explanation comments..."); | ||
|
|
||
| let scanned = 0; | ||
| const MAX_COMMENTS = 500; | ||
|
|
||
| for await (const response of github.paginate.iterator( | ||
| github.rest.issues.listComments, | ||
| { | ||
| owner, | ||
| repo, | ||
| issue_number: issueNumber, | ||
| per_page: 100, | ||
| } | ||
| )) { | ||
| for (const comment of response.data) { | ||
| scanned++; | ||
| if (comment.body?.includes(marker)) { | ||
| console.log(`Found existing explanation comment (scanned ${scanned} comments).`); | ||
| return true; | ||
| } | ||
| if (scanned >= MAX_COMMENTS) { | ||
| console.log(`Reached scan limit (${MAX_COMMENTS} comments) — assuming no duplicate.`); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log(`No existing explainer comment found (scanned ${scanned} comments).`); | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Builds the draft explainer comment body. | ||
| * | ||
| * @param {string} greetingTarget - Formatted username to greet (e.g., "@username"). | ||
| * @returns {string} - Formatted reminder message. | ||
| */ | ||
|
|
||
| function buildExplainerComment(greetingTarget) { | ||
| return ` | ||
| ${COMMENT_MARKER} | ||
| Hi ${greetingTarget}! | ||
|
|
||
| We suggested a few updates and moved this PR to **draft** while you apply the feedback. This keeps it out of the review queue until it is ready again. | ||
|
|
||
| ### What happens next? | ||
| - Make the requested changes. | ||
| - When you are ready, click **“Ready for review”** (recommended) or use the \`/review\` command. | ||
|
|
||
| Thanks again for your contribution! | ||
| `.trim(); | ||
| } | ||
|
|
||
| /** | ||
| * Main entry point. | ||
| * | ||
| * Execution Flow: | ||
| * 1. Ensure PR exists in event payload. | ||
| * 2. Prevent duplicate bot comments. | ||
| * 3. Confirm at least one CHANGES_REQUESTED review exists. | ||
| * 4. Post explanation comment. | ||
| */ | ||
|
|
||
| module.exports = async ({ github, context }) => { | ||
| let pr = context.payload.pull_request; | ||
| let prNumber = pr?.number || manualPRNumber; | ||
| const { owner, repo } = context.repo; | ||
|
|
||
| if (!prNumber) { | ||
| console.log("No PR number found in payload or environment. Exiting."); | ||
| return; | ||
| } | ||
| if (!pr) { | ||
| console.log(`Fetching PR #${prNumber} (manual workflow_dispatch run)...`); | ||
|
|
||
| try { | ||
| const prResponse = await github.rest.pulls.get({ | ||
| owner, | ||
| repo, | ||
| pull_number: prNumber, | ||
| }); | ||
|
|
||
| pr = prResponse.data; | ||
| } catch (error) { | ||
| console.log(`Failed to fetch PR #${prNumber}: ${error.message}`); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| const authorLogin = pr.user?.login; | ||
| const greetingTarget = authorLogin ? `@${authorLogin}` : "there"; | ||
|
|
||
| if (!pr.draft) { | ||
| console.log(`PR #${prNumber} is not draft. Skipping.`); | ||
| return; | ||
| } | ||
|
parvninama marked this conversation as resolved.
|
||
|
|
||
| console.log(`PR #${prNumber} was converted to draft. Checking if explanation is needed.`); | ||
|
|
||
| // Prevent duplicate comments | ||
| let alreadyCommented = false; | ||
| try { | ||
| alreadyCommented = await commentExists({ | ||
| github, | ||
| owner, | ||
| repo, | ||
| issueNumber: prNumber, | ||
| marker: COMMENT_MARKER, | ||
| }); | ||
| } catch (err) { | ||
| console.log(`Failed to check existing comments on PR #${prNumber} in ${owner}/${repo}: ${err.message}`); | ||
| console.log("Skipping explanation to avoid potential duplicate."); | ||
| return; | ||
| } | ||
|
|
||
| if (alreadyCommented) { | ||
| console.log("Explanation already exists — skipping."); | ||
| return; | ||
| } | ||
|
|
||
| // Only proceed if changes were previously requested on this PR | ||
| try { | ||
| const reviews = await github.rest.pulls.listReviews({ | ||
| owner, | ||
| repo, | ||
| pull_number: prNumber, | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| // Track the latest review from each reviewer | ||
| const latestReviews = new Map(); | ||
|
|
||
| for (const review of reviews.data) { | ||
| const reviewer = review.user?.login; | ||
| if (!reviewer) continue; | ||
|
|
||
| const previous = latestReviews.get(reviewer); | ||
|
|
||
| if (!previous || new Date(review.submitted_at) > new Date(previous.submitted_at)) { | ||
| latestReviews.set(reviewer, review); | ||
| } | ||
| } | ||
|
|
||
| const hasChangeRequest = [...latestReviews.values()].some( | ||
| (review) => review.state === "CHANGES_REQUESTED" | ||
| ); | ||
|
|
||
| if (!hasChangeRequest) { | ||
| console.log("No CHANGES_REQUESTED review found. Skipping explanation comment."); | ||
| return; | ||
| } | ||
|
parvninama marked this conversation as resolved.
|
||
|
|
||
| } catch (error) { | ||
| console.log(`Review lookup failed for PR #${prNumber}: ${error.message}. Skipping to avoid a false explanation.`); | ||
| return; | ||
| } | ||
|
|
||
| // Post explanation comment | ||
| try { | ||
| if (DRY_RUN) { | ||
| console.log(`[DRY RUN] Explanation comment would be posted on PR #${prNumber}.`); | ||
| return; | ||
| } | ||
| await github.rest.issues.createComment({ | ||
| owner, | ||
| repo, | ||
| issue_number: prNumber, | ||
| body: buildExplainerComment(greetingTarget), | ||
| }); | ||
| console.log(`Posted draft explanation comment on PR #${prNumber}.`); | ||
| } catch (error) { | ||
| console.log(`Failed to post draft explanation on PR #${prNumber}: ${error.message}`); | ||
| } | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # This workflow posts a friendly explanation when a PR is moved to draft status after changes are requested | ||
|
|
||
| name: PR Draft Explainer | ||
| on: | ||
| pull_request: | ||
| types: [converted_to_draft] | ||
| workflow_dispatch: | ||
| inputs: | ||
| pr_number: | ||
| description: "PR number to test" | ||
| required: true | ||
| type: number | ||
| dry_run: | ||
| description: "Run without posting comment" | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
|
|
||
| permissions: | ||
| pull-requests: read | ||
| issues: write | ||
| contents: read | ||
|
|
||
| jobs: | ||
| pr-draft-explainer: | ||
| runs-on: ubuntu-latest | ||
| concurrency: | ||
| group: pr-draft-explainer-${{ github.event.pull_request.number || github.event.inputs.pr_number }} | ||
| cancel-in-progress: true | ||
| steps: | ||
| - name: Harden the runner | ||
| uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: Checkout repository | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| ref: main | ||
|
|
||
| - name: Run draft explainer bot | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} | ||
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 | ||
| with: | ||
| script: | | ||
| const script = require('./.github/scripts/bot-pr-draft-explainer.js'); | ||
| await script({ github, context }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.