diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml new file mode 100644 index 00000000000..a4ca4903f15 --- /dev/null +++ b/.github/workflows/opencode-review.yml @@ -0,0 +1,28 @@ +name: opencode review + +on: + pull_request: + types: [review_requested] + +jobs: + opencode-review: + if: github.event.requested_team.slug == 'opencode' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-bun + + - name: Run opencode review + uses: sst/opencode/github@latest + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + with: + model: opencode/glm-4.6 + prompt: Review this PR for code quality, potential bugs, security issues, and suggest improvements where needed diff --git a/flake.lock b/flake.lock index 231ac606b09..b0749bea4f0 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1764081664, - "narHash": "sha256-sUoHmPr/EwXzRMpv1u/kH+dXuvJEyyF2Q7muE+t0EU4=", + "lastModified": 1764138170, + "narHash": "sha256-2bCmfCUZyi2yj9FFXYKwsDiaZmizN75cLhI/eWmf3tk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dc205f7b4fdb04c8b7877b43edb7b73be7730081", + "rev": "bb813de6d2241bcb1b5af2d3059f560c66329967", "type": "github" }, "original": { diff --git a/github/README.md b/github/README.md index 36342b40995..823f9931df5 100644 --- a/github/README.md +++ b/github/README.md @@ -30,6 +30,18 @@ Leave the following comment on a GitHub PR. opencode will implement the requeste Delete the attachment from S3 when the note is removed /oc ``` +#### Automatic PR Reviews + +Add opencode as a reviewer to automatically review PRs. When the `opencode` team is requested as a reviewer, the action will automatically trigger a comprehensive code review. + +To set this up: + +1. Create a team in your GitHub organization called `opencode` +2. When creating or viewing a PR, add the `opencode` team as a reviewer +3. The review workflow will automatically trigger and provide feedback + +This requires the review workflow file `.github/workflows/opencode-review.yml` to be present (created automatically by `opencode github install`). + #### Review specific code lines Leave a comment directly on code lines in the PR's "Files" tab. opencode will automatically detect the file, line numbers, and diff context to provide precise responses. @@ -94,7 +106,41 @@ This will walk you through installing the GitHub app, creating the workflow, and model: anthropic/claude-sonnet-4-20250514 ``` -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. +3. (Optional) For automatic PR reviews, add `.github/workflows/opencode-review.yml`: + + ```yml + name: opencode review + + on: + pull_request: + types: [review_requested] + + jobs: + opencode-review: + # Trigger when 'opencode' team is requested for review + if: github.event.requested_team.slug == 'opencode' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run opencode review + uses: sst/opencode/github@latest + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + model: anthropic/claude-sonnet-4-20250514 + prompt: "Review this PR for code quality, potential bugs, security issues, and suggest improvements where needed" + ``` + + Then create a team called `opencode` in your GitHub organization. When you add this team as a reviewer to a PR, the workflow will automatically trigger a comprehensive code review. + +4. 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. ## Support diff --git a/github/action.yml b/github/action.yml index 0b7367ded42..2f0fedeb05e 100644 --- a/github/action.yml +++ b/github/action.yml @@ -13,6 +13,10 @@ inputs: description: "Share the opencode session (defaults to true for public repos)" required: false + prompt: + description: "Custom prompt for the action (optional)" + required: false + runs: using: "composite" steps: @@ -27,3 +31,4 @@ runs: env: MODEL: ${{ inputs.model }} SHARE: ${{ inputs.share }} + PROMPT: ${{ inputs.prompt }} diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index b255e17d1b3..07b43a87c80 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -7,7 +7,11 @@ import { graphql } from "@octokit/graphql" import * as core from "@actions/core" import * as github from "@actions/github" import type { Context } from "@actions/github/lib/context" -import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types" +import type { + IssueCommentEvent, + PullRequestReviewCommentEvent, + PullRequestReviewRequestedEvent, +} from "@octokit/webhooks-types" import { UI } from "../ui" import { cmd } from "./cmd" import { ModelsDev } from "../../provider/models" @@ -125,6 +129,7 @@ type IssueQueryResponse = { } const WORKFLOW_FILE = ".github/workflows/opencode.yml" +const REVIEW_WORKFLOW_FILE = ".github/workflows/opencode-review.yml" export const GithubCommand = cmd({ command: "github", @@ -176,10 +181,12 @@ export const GithubInstallCommand = cmd({ [ "Next steps:", "", - ` 1. Commit the \`${WORKFLOW_FILE}\` file and push`, + ` 1. Commit the workflow files and push`, step2, "", - " 3. Go to a GitHub issue and comment `/oc summarize` to see the agent in action", + " 3. Try the agent:", + " - Comment '/oc' on any issue or PR", + " - Add 'opencode' reviewer to a PR for automatic review", "", " Learn more about the GitHub agent - https://opencode.ai/docs/github/#usage-examples", ].join("\n"), @@ -355,6 +362,40 @@ jobs: ) prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`) + + // Add review workflow + await Bun.write( + path.join(app.root, REVIEW_WORKFLOW_FILE), + `name: opencode review + +on: + pull_request: + types: [review_requested] + +jobs: + opencode-review: + if: github.event.requested_team.slug == 'opencode' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run opencode review + uses: sst/opencode/github@latest${envStr} + with: + model: ${provider}/${model} + prompt: Review this PR for code quality, potential bugs, security issues, and suggest improvements where needed`, + ) + + prompts.log.success(`Added review workflow file: "${REVIEW_WORKFLOW_FILE}"`) + prompts.log.info( + `To use PR reviews: Create a team called "opencode" in your GitHub organization and add it as a reviewer to trigger automatic reviews.`, + ) } } }, @@ -380,7 +421,11 @@ export const GithubRunCommand = cmd({ const isMock = args.token || args.event const context = isMock ? (JSON.parse(args.event!) as Context) : github.context - if (context.eventName !== "issue_comment" && context.eventName !== "pull_request_review_comment") { + if ( + context.eventName !== "issue_comment" && + context.eventName !== "pull_request_review_comment" && + context.eventName !== "pull_request" + ) { core.setFailed(`Unsupported event type: ${context.eventName}`) process.exit(1) } @@ -389,14 +434,19 @@ export const GithubRunCommand = cmd({ const runId = normalizeRunId() const share = normalizeShare() const { owner, repo } = context.repo - const payload = context.payload as IssueCommentEvent | PullRequestReviewCommentEvent + const payload = context.payload as + | IssueCommentEvent + | PullRequestReviewCommentEvent + | PullRequestReviewRequestedEvent const issueEvent = isIssueCommentEvent(payload) ? payload : undefined const actor = context.actor const issueId = context.eventName === "pull_request_review_comment" ? (payload as PullRequestReviewCommentEvent).pull_request.number - : (payload as IssueCommentEvent).issue.number + : context.eventName === "pull_request" + ? (payload as PullRequestReviewRequestedEvent).pull_request.number + : (payload as IssueCommentEvent).issue.number const runUrl = `/${owner}/${repo}/actions/runs/${runId}` const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai" @@ -437,11 +487,16 @@ export const GithubRunCommand = cmd({ })() console.log("opencode session", session.id) - // Handle 3 cases - // 1. Issue - // 2. Local PR - // 3. Fork PR - if (context.eventName === "pull_request_review_comment" || issueEvent?.issue.pull_request) { + // Handle 4 cases + // 1. Pull request event (review_requested) + // 2. Issue + // 3. Local PR + // 4. Fork PR + if ( + context.eventName === "pull_request" || + context.eventName === "pull_request_review_comment" || + issueEvent?.issue.pull_request + ) { const prData = await fetchPR() // Local PR if (prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner) { @@ -539,11 +594,23 @@ export const GithubRunCommand = cmd({ } function isIssueCommentEvent( - event: IssueCommentEvent | PullRequestReviewCommentEvent, + event: IssueCommentEvent | PullRequestReviewCommentEvent | PullRequestReviewRequestedEvent, ): event is IssueCommentEvent { return "issue" in event } + function isPullRequestEvent( + event: IssueCommentEvent | PullRequestReviewCommentEvent | PullRequestReviewRequestedEvent, + ): event is PullRequestReviewRequestedEvent { + return ( + context.eventName === "pull_request" && + "action" in event && + event.action === "review_requested" && + "requested_team" in event && + event.requested_team !== null + ) + } + function getReviewCommentContext() { if (context.eventName !== "pull_request_review_comment") { return null @@ -562,9 +629,23 @@ export const GithubRunCommand = cmd({ } async function getUserPrompt() { + const envPrompt = process.env["PROMPT"] + if (envPrompt) { + return { userPrompt: envPrompt, promptFiles: [] } + } + + if (isPullRequestEvent(payload)) { + return { + userPrompt: + "Review this PR for code quality, potential bugs, security issues, and suggest improvements where needed", + promptFiles: [], + } + } + const reviewContext = getReviewCommentContext() let prompt = (() => { - const body = payload.comment.body.trim() + const commentPayload = payload as IssueCommentEvent | PullRequestReviewCommentEvent + const body = commentPayload.comment.body.trim() if (body === "/opencode" || body === "/oc") { if (reviewContext) { return `Review this code change and suggest improvements for the commented lines:\n\nFile: ${reviewContext.file}\nLines: ${reviewContext.line}\n\n${reviewContext.diffHunk}` @@ -1021,10 +1102,13 @@ query($owner: String!, $repo: String!, $number: Int!) { } function buildPromptDataForIssue(issue: GitHubIssue) { + const commentPayload = !isPullRequestEvent(payload) + ? (payload as IssueCommentEvent | PullRequestReviewCommentEvent) + : undefined const comments = (issue.comments?.nodes || []) .filter((c) => { const id = parseInt(c.databaseId) - return id !== commentId && id !== payload.comment.id + return id !== commentId && (!commentPayload || id !== commentPayload.comment.id) }) .map((c) => ` - ${c.author.login} at ${c.createdAt}: ${c.body}`) @@ -1140,10 +1224,13 @@ query($owner: String!, $repo: String!, $number: Int!) { } function buildPromptDataForPR(pr: GitHubPullRequest) { + const commentPayload = !isPullRequestEvent(payload) + ? (payload as IssueCommentEvent | PullRequestReviewCommentEvent) + : undefined const comments = (pr.comments?.nodes || []) .filter((c) => { const id = parseInt(c.databaseId) - return id !== commentId && id !== payload.comment.id + return id !== commentId && (!commentPayload || id !== commentPayload.comment.id) }) .map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)