From 8e85033fe8677ead962655102abb6d9f93e6d64d Mon Sep 17 00:00:00 2001 From: Thomas Eude Date: Sun, 10 May 2026 11:22:24 +0000 Subject: [PATCH] Add review thread replies for GitHub action --- github/README.md | 5 +++++ github/action.yml | 6 ++++++ github/index.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/github/README.md b/github/README.md index 17b24ffb1d6e..0aa203576b91 100644 --- a/github/README.md +++ b/github/README.md @@ -48,6 +48,8 @@ When commenting on specific lines, opencode receives: This allows for more targeted requests without needing to specify file paths or line numbers manually. +By default, opencode replies with a top-level PR comment. Set `reply_to_review_thread: true` to reply directly in the triggering review comment thread instead. + ## Installation Run the following command in the terminal from your GitHub repo: @@ -80,6 +82,8 @@ This will walk you through installing the GitHub app, creating the workflow, and runs-on: ubuntu-latest permissions: id-token: write + pull-requests: write + issues: write steps: - name: Checkout repository uses: actions/checkout@v6 @@ -95,6 +99,7 @@ This will walk you through installing the GitHub app, creating the workflow, and with: model: anthropic/claude-sonnet-4-20250514 use_github_token: true + reply_to_review_thread: true ``` 3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys. diff --git a/github/action.yml b/github/action.yml index 3d983a160995..527bbb911abf 100644 --- a/github/action.yml +++ b/github/action.yml @@ -38,6 +38,11 @@ inputs: description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai" required: false + reply_to_review_thread: + description: "Reply in the triggering pull request review thread when handling pull_request_review_comment events" + required: false + default: "false" + runs: using: "composite" steps: @@ -77,3 +82,4 @@ runs: MENTIONS: ${{ inputs.mentions }} VARIANT: ${{ inputs.variant }} OIDC_BASE_URL: ${{ inputs.oidc_base_url }} + REPLY_TO_REVIEW_THREAD: ${{ inputs.reply_to_review_thread }} diff --git a/github/index.ts b/github/index.ts index 51ee2a46a531..906e021e42ef 100644 --- a/github/index.ts +++ b/github/index.ts @@ -118,6 +118,7 @@ let accessToken: string let octoRest: Octokit let octoGraph: typeof graphql let commentId: number +let commentType: "issue" | "review" = "issue" let gitConfig: string let session: { id: string; title: string; version: string } let shareId: string | undefined @@ -331,6 +332,14 @@ function useEnvShare() { throw new Error(`Invalid share value: ${value}. Share must be a boolean.`) } +function useEnvReplyToReviewThread() { + const value = process.env["REPLY_TO_REVIEW_THREAD"] + if (!value) return false + if (value === "true") return true + if (value === "false") return false + throw new Error(`Invalid reply_to_review_thread value: ${value}. reply_to_review_thread must be a boolean.`) +} + function useEnvMock() { return { mockEvent: process.env["MOCK_EVENT"], @@ -349,6 +358,8 @@ function isMock() { function isPullRequest() { const context = useContext() + if (context.eventName === "pull_request_review_comment") return true + const payload = context.payload as IssueCommentEvent return Boolean(payload.issue.pull_request) } @@ -358,7 +369,13 @@ function useContext() { } function useIssueId() { - const payload = useContext().payload as IssueCommentEvent + const context = useContext() + if (context.eventName === "pull_request_review_comment") { + const payload = context.payload as PullRequestReviewCommentEvent + return payload.pull_request.number + } + + const payload = context.payload as IssueCommentEvent return payload.issue.number } @@ -402,7 +419,24 @@ async function getAccessToken() { async function createComment() { const { repo } = useContext() + const reviewContext = getReviewCommentContext() + + if (reviewContext && useEnvReplyToReviewThread()) { + const payload = useContext().payload as PullRequestReviewCommentEvent + const reviewCommentId = payload.comment.in_reply_to_id ?? payload.comment.id + console.log("Creating review thread reply...") + commentType = "review" + return await octoRest.rest.pulls.createReplyForReviewComment({ + owner: repo.owner, + repo: repo.repo, + pull_number: useIssueId(), + comment_id: reviewCommentId, + body: `[Working...](${useEnvRunUrl()})`, + }) + } + console.log("Creating comment...") + commentType = "issue" return await octoRest.rest.issues.createComment({ owner: repo.owner, repo: repo.repo, @@ -788,6 +822,15 @@ async function updateComment(body: string) { console.log("Updating comment...") const { repo } = useContext() + if (commentType === "review") { + return await octoRest.rest.pulls.updateReviewComment({ + owner: repo.owner, + repo: repo.repo, + comment_id: commentId, + body, + }) + } + return await octoRest.rest.issues.updateComment({ owner: repo.owner, repo: repo.repo,