Skip to content
Closed
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions github/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
45 changes: 44 additions & 1 deletion github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"],
Expand All @@ -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)
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -798,6 +832,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,
Expand Down
Loading