-
Notifications
You must be signed in to change notification settings - Fork 225
Add resolve-pull-request-review-thread safe output #15668
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
pelikhan
merged 5 commits into
github:main
from
strawgate:resolve-pull-request-review-thread
Feb 14, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
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,185 @@ | ||
| // @ts-check | ||
| /// <reference types="@actions/github-script" /> | ||
|
|
||
| /** | ||
| * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction | ||
| */ | ||
|
|
||
| const { getErrorMessage } = require("./error_helpers.cjs"); | ||
|
|
||
| /** | ||
| * Type constant for handler identification | ||
| */ | ||
| const HANDLER_TYPE = "resolve_pull_request_review_thread"; | ||
|
|
||
| /** | ||
| * Look up a review thread's parent PR number via the GraphQL API. | ||
| * Used to validate the thread belongs to the triggering PR before resolving. | ||
| * @param {any} github - GitHub GraphQL instance | ||
| * @param {string} threadId - Review thread node ID (e.g., 'PRRT_kwDOABCD...') | ||
| * @returns {Promise<number|null>} The PR number the thread belongs to, or null if not found | ||
| */ | ||
| async function getThreadPullRequestNumber(github, threadId) { | ||
| const query = /* GraphQL */ ` | ||
| query ($threadId: ID!) { | ||
| node(id: $threadId) { | ||
| ... on PullRequestReviewThread { | ||
| pullRequest { | ||
| number | ||
| } | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| const result = await github.graphql(query, { threadId }); | ||
|
|
||
| return result?.node?.pullRequest?.number ?? null; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve a pull request review thread using the GraphQL API. | ||
| * @param {any} github - GitHub GraphQL instance | ||
| * @param {string} threadId - Review thread node ID (e.g., 'PRRT_kwDOABCD...') | ||
| * @returns {Promise<{threadId: string, isResolved: boolean}>} Resolved thread details | ||
| */ | ||
| async function resolveReviewThreadAPI(github, threadId) { | ||
| const query = /* GraphQL */ ` | ||
| mutation ($threadId: ID!) { | ||
| resolveReviewThread(input: { threadId: $threadId }) { | ||
| thread { | ||
| id | ||
| isResolved | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| const result = await github.graphql(query, { threadId }); | ||
|
|
||
| return { | ||
| threadId: result.resolveReviewThread.thread.id, | ||
| isResolved: result.resolveReviewThread.thread.isResolved, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Extract the triggering pull request number from the GitHub Actions event payload. | ||
| * Supports both pull_request events (payload.pull_request.number) and issue_comment | ||
| * events on PRs (payload.issue.number when payload.issue.pull_request is present). | ||
| * @param {any} payload - The context.payload from the GitHub Actions event | ||
| * @returns {number|undefined} The PR number, or undefined if not in a PR context | ||
| */ | ||
| function getTriggeringPRNumber(payload) { | ||
| return payload?.pull_request?.number || (payload?.issue?.pull_request ? payload.issue.number : undefined); | ||
| } | ||
|
|
||
| /** | ||
| * Main handler factory for resolve_pull_request_review_thread | ||
| * Returns a message handler function that processes individual resolve messages. | ||
| * | ||
| * Resolution is scoped to the triggering PR only — the handler validates that each | ||
| * thread belongs to the triggering pull request before resolving it. This prevents | ||
| * agents from resolving threads on unrelated PRs. | ||
| * @type {HandlerFactoryFunction} | ||
| */ | ||
| async function main(config = {}) { | ||
| // Extract configuration | ||
| const maxCount = config.max || 10; | ||
|
|
||
| // Determine the triggering PR number from context | ||
| const triggeringPRNumber = getTriggeringPRNumber(context.payload); | ||
|
|
||
| core.info(`Resolve PR review thread configuration: max=${maxCount}, triggeringPR=${triggeringPRNumber || "none"}`); | ||
|
|
||
| // Track how many items we've processed for max limit | ||
| let processedCount = 0; | ||
|
|
||
| /** | ||
| * Message handler function that processes a single resolve_pull_request_review_thread message | ||
| * @param {Object} message - The resolve message to process | ||
| * @param {Object} resolvedTemporaryIds - Map of temporary IDs to {repo, number} | ||
| * @returns {Promise<Object>} Result with success/error status | ||
| */ | ||
| return async function handleResolvePRReviewThread(message, resolvedTemporaryIds) { | ||
| // Check if we've hit the max limit | ||
| if (processedCount >= maxCount) { | ||
| core.warning(`Skipping resolve_pull_request_review_thread: max count of ${maxCount} reached`); | ||
| return { | ||
| success: false, | ||
| error: `Max count of ${maxCount} reached`, | ||
| }; | ||
| } | ||
|
|
||
| processedCount++; | ||
|
|
||
| const item = message; | ||
|
|
||
| try { | ||
| // Validate required fields | ||
| const threadId = item.thread_id; | ||
| if (!threadId || typeof threadId !== "string" || threadId.trim().length === 0) { | ||
| core.warning('Missing or invalid required field "thread_id" in resolve message'); | ||
| return { | ||
| success: false, | ||
| error: 'Missing or invalid required field "thread_id" - must be a non-empty string (GraphQL node ID)', | ||
| }; | ||
| } | ||
|
|
||
| // Validate triggering PR context | ||
| if (!triggeringPRNumber) { | ||
| core.warning("Cannot resolve review thread: not running in a pull request context"); | ||
| return { | ||
| success: false, | ||
| error: "Cannot resolve review threads outside of a pull request context", | ||
| }; | ||
| } | ||
|
|
||
| // Look up the thread to validate it belongs to the triggering PR | ||
| const threadPRNumber = await getThreadPullRequestNumber(github, threadId); | ||
| if (threadPRNumber === null) { | ||
| core.warning(`Review thread not found or not a PullRequestReviewThread: ${threadId}`); | ||
| return { | ||
| success: false, | ||
| error: `Review thread not found: ${threadId}`, | ||
| }; | ||
| } | ||
|
|
||
| if (threadPRNumber !== triggeringPRNumber) { | ||
| core.warning(`Thread ${threadId} belongs to PR #${threadPRNumber}, not triggering PR #${triggeringPRNumber}`); | ||
| return { | ||
| success: false, | ||
| error: `Thread belongs to PR #${threadPRNumber}, but only threads on the triggering PR #${triggeringPRNumber} can be resolved`, | ||
| }; | ||
| } | ||
|
|
||
| core.info(`Resolving review thread: ${threadId} (PR #${triggeringPRNumber})`); | ||
|
|
||
| const resolveResult = await resolveReviewThreadAPI(github, threadId); | ||
|
|
||
| if (resolveResult.isResolved) { | ||
| core.info(`Successfully resolved review thread: ${threadId}`); | ||
| return { | ||
| success: true, | ||
| thread_id: threadId, | ||
| is_resolved: true, | ||
| }; | ||
| } else { | ||
| core.error(`Failed to resolve review thread: ${threadId}`); | ||
| return { | ||
| success: false, | ||
| error: `Failed to resolve review thread: ${threadId}`, | ||
| }; | ||
| } | ||
| } catch (error) { | ||
| const errorMessage = getErrorMessage(error); | ||
| core.error(`Failed to resolve review thread: ${errorMessage}`); | ||
| return { | ||
| success: false, | ||
| error: errorMessage, | ||
| }; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| module.exports = { main, HANDLER_TYPE, getTriggeringPRNumber }; | ||
Oops, something went wrong.
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.