Преглед изворни кода

github: support issues and workflow_dispatch events (#6157)

Matt Silverlock пре 1 месец
родитељ
комит
1626341a4a
2 измењених фајлова са 123 додато и 36 уклоњено
  1. 61 27
      packages/opencode/src/cli/cmd/github.ts
  2. 62 9
      packages/web/src/content/docs/github.mdx

+ 61 - 27
packages/opencode/src/cli/cmd/github.ts

@@ -9,7 +9,9 @@ import * as github from "@actions/github"
 import type { Context } from "@actions/github/lib/context"
 import type {
   IssueCommentEvent,
+  IssuesEvent,
   PullRequestReviewCommentEvent,
+  WorkflowDispatchEvent,
   WorkflowRunEvent,
   PullRequestEvent,
 } from "@octokit/webhooks-types"
@@ -132,7 +134,16 @@ type IssueQueryResponse = {
 const AGENT_USERNAME = "opencode-agent[bot]"
 const AGENT_REACTION = "eyes"
 const WORKFLOW_FILE = ".github/workflows/opencode.yml"
-const SUPPORTED_EVENTS = ["issue_comment", "pull_request_review_comment", "schedule", "pull_request"] as const
+
+// Event categories for routing
+// USER_EVENTS: triggered by user actions, have actor/issueId, support reactions/comments
+// REPO_EVENTS: triggered by automation, no actor/issueId, output to logs/PR only
+const USER_EVENTS = ["issue_comment", "pull_request_review_comment", "issues", "pull_request"] as const
+const REPO_EVENTS = ["schedule", "workflow_dispatch"] as const
+const SUPPORTED_EVENTS = [...USER_EVENTS, ...REPO_EVENTS] as const
+
+type UserEvent = (typeof USER_EVENTS)[number]
+type RepoEvent = (typeof REPO_EVENTS)[number]
 
 // Parses GitHub remote URLs in various formats:
 // - https://github.com/owner/repo.git
@@ -397,27 +408,38 @@ export const GithubRunCommand = cmd({
         core.setFailed(`Unsupported event type: ${context.eventName}`)
         process.exit(1)
       }
+
+      // Determine event category for routing
+      // USER_EVENTS: have actor, issueId, support reactions/comments
+      // REPO_EVENTS: no actor/issueId, output to logs/PR only
+      const isUserEvent = USER_EVENTS.includes(context.eventName as UserEvent)
+      const isRepoEvent = REPO_EVENTS.includes(context.eventName as RepoEvent)
       const isCommentEvent = ["issue_comment", "pull_request_review_comment"].includes(context.eventName)
+      const isIssuesEvent = context.eventName === "issues"
       const isScheduleEvent = context.eventName === "schedule"
+      const isWorkflowDispatchEvent = context.eventName === "workflow_dispatch"
 
       const { providerID, modelID } = normalizeModel()
       const runId = normalizeRunId()
       const share = normalizeShare()
       const oidcBaseUrl = normalizeOidcBaseUrl()
       const { owner, repo } = context.repo
-      // For schedule events, payload has no issue/comment data
+      // For repo events (schedule, workflow_dispatch), payload has no issue/comment data
       const payload = context.payload as
         | IssueCommentEvent
+        | IssuesEvent
         | PullRequestReviewCommentEvent
+        | WorkflowDispatchEvent
         | WorkflowRunEvent
         | PullRequestEvent
       const issueEvent = isIssueCommentEvent(payload) ? payload : undefined
+      // workflow_dispatch has an actor (the user who triggered it), schedule does not
       const actor = isScheduleEvent ? undefined : context.actor
 
-      const issueId = isScheduleEvent
+      const issueId = isRepoEvent
         ? undefined
-        : context.eventName === "issue_comment"
-          ? (payload as IssueCommentEvent).issue.number
+        : context.eventName === "issue_comment" || context.eventName === "issues"
+          ? (payload as IssueCommentEvent | IssuesEvent).issue.number
           : (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number
       const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
       const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"
@@ -462,8 +484,8 @@ export const GithubRunCommand = cmd({
         if (!useGithubToken) {
           await configureGit(appToken)
         }
-        // Skip permission check for schedule events (no actor to check)
-        if (!isScheduleEvent) {
+        // Skip permission check and reactions for repo events (no actor to check, no issue to react to)
+        if (isUserEvent) {
           await assertPermissions()
           await addReaction(commentType)
         }
@@ -480,25 +502,30 @@ export const GithubRunCommand = cmd({
         })()
         console.log("opencode session", session.id)
 
-        // Handle 4 cases
-        // 1. Schedule (no issue/PR context)
-        // 2. Issue
-        // 3. Local PR
-        // 4. Fork PR
-        if (isScheduleEvent) {
-          // Schedule event - no issue/PR context, output goes to logs
-          const branch = await checkoutNewBranch("schedule")
+        // Handle event types:
+        // REPO_EVENTS (schedule, workflow_dispatch): no issue/PR context, output to logs/PR only
+        // USER_EVENTS on PR (pull_request, pull_request_review_comment, issue_comment on PR): work on PR branch
+        // USER_EVENTS on Issue (issue_comment on issue, issues): create new branch, may create PR
+        if (isRepoEvent) {
+          // Repo event - no issue/PR context, output goes to logs
+          if (isWorkflowDispatchEvent && actor) {
+            console.log(`Triggered by: ${actor}`)
+          }
+          const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule"
+          const branch = await checkoutNewBranch(branchPrefix)
           const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
           const response = await chat(userPrompt, promptFiles)
           const { dirty, uncommittedChanges } = await branchIsDirty(head)
           if (dirty) {
             const summary = await summarize(response)
-            await pushToNewBranch(summary, branch, uncommittedChanges, true)
+            // workflow_dispatch has an actor for co-author attribution, schedule does not
+            await pushToNewBranch(summary, branch, uncommittedChanges, isScheduleEvent)
+            const triggerType = isWorkflowDispatchEvent ? "workflow_dispatch" : "scheduled workflow"
             const pr = await createPR(
               repoData.data.default_branch,
               branch,
               summary,
-              `${response}\n\nTriggered by scheduled workflow${footer({ image: true })}`,
+              `${response}\n\nTriggered by ${triggerType}${footer({ image: true })}`,
             )
             console.log(`Created PR #${pr}`)
           } else {
@@ -573,7 +600,7 @@ export const GithubRunCommand = cmd({
         } else if (e instanceof Error) {
           msg = e.message
         }
-        if (!isScheduleEvent) {
+        if (isUserEvent) {
           await createComment(`${msg}${footer()}`)
           await removeReaction(commentType)
         }
@@ -628,9 +655,15 @@ export const GithubRunCommand = cmd({
       }
 
       function isIssueCommentEvent(
-        event: IssueCommentEvent | PullRequestReviewCommentEvent | WorkflowRunEvent | PullRequestEvent,
+        event:
+          | IssueCommentEvent
+          | IssuesEvent
+          | PullRequestReviewCommentEvent
+          | WorkflowDispatchEvent
+          | WorkflowRunEvent
+          | PullRequestEvent,
       ): event is IssueCommentEvent {
-        return "issue" in event
+        return "issue" in event && "comment" in event
       }
 
       function getReviewCommentContext() {
@@ -652,10 +685,11 @@ export const GithubRunCommand = cmd({
 
       async function getUserPrompt() {
         const customPrompt = process.env["PROMPT"]
-        // For schedule events, PROMPT is required since there's no comment to extract from
-        if (isScheduleEvent) {
+        // For repo events and issues events, PROMPT is required since there's no comment to extract from
+        if (isRepoEvent || isIssuesEvent) {
           if (!customPrompt) {
-            throw new Error("PROMPT input is required for scheduled events")
+            const eventType = isRepoEvent ? "scheduled and workflow_dispatch" : "issues"
+            throw new Error(`PROMPT input is required for ${eventType} events`)
           }
           return { userPrompt: customPrompt, promptFiles: [] }
         }
@@ -923,7 +957,7 @@ export const GithubRunCommand = cmd({
         await $`git config --local ${config} "${gitConfig}"`
       }
 
-      async function checkoutNewBranch(type: "issue" | "schedule") {
+      async function checkoutNewBranch(type: "issue" | "schedule" | "dispatch") {
         console.log("Checking out new branch...")
         const branch = generateBranchName(type)
         await $`git checkout -b ${branch}`
@@ -952,16 +986,16 @@ export const GithubRunCommand = cmd({
         await $`git checkout -b ${localBranch} fork/${remoteBranch}`
       }
 
-      function generateBranchName(type: "issue" | "pr" | "schedule") {
+      function generateBranchName(type: "issue" | "pr" | "schedule" | "dispatch") {
         const timestamp = new Date()
           .toISOString()
           .replace(/[:-]/g, "")
           .replace(/\.\d{3}Z/, "")
           .split("T")
           .join("")
-        if (type === "schedule") {
+        if (type === "schedule" || type === "dispatch") {
           const hex = crypto.randomUUID().slice(0, 6)
-          return `opencode/scheduled-${hex}-${timestamp}`
+          return `opencode/${type}-${hex}-${timestamp}`
         }
         return `opencode/${type}${issueId}-${timestamp}`
       }

+ 62 - 9
packages/web/src/content/docs/github.mdx

@@ -104,12 +104,14 @@ Or you can set it up manually.
 
 OpenCode can be triggered by the following GitHub events:
 
-| Event Type                    | Triggered By                           | Details                                                                                                                                                             |
-| ----------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `issue_comment`               | Comment on an issue or PR              | Mention `/opencode` or `/oc` in your comment. OpenCode reads the issue/PR context and can create branches, open PRs, or reply with explanations.                    |
-| `pull_request_review_comment` | Comment on specific code lines in a PR | Mention `/opencode` or `/oc` while reviewing code. OpenCode receives file path, line numbers, and diff context for precise responses.                               |
-| `schedule`                    | Cron-based schedule                    | Run OpenCode on a schedule using the `prompt` input. Useful for automated code reviews, reports, or maintenance tasks. OpenCode can create issues or PRs as needed. |
-| `pull_request`                | PR opened or updated                   | Automatically trigger OpenCode when PRs are opened, synchronized, or reopened. Useful for automated reviews without needing to leave a comment.                     |
+| Event Type                    | Triggered By                           | Details                                                                                                           |
+| ----------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
+| `issue_comment`               | Comment on an issue or PR              | Mention `/opencode` or `/oc` in your comment. OpenCode reads context and can create branches, open PRs, or reply. |
+| `pull_request_review_comment` | Comment on specific code lines in a PR | Mention `/opencode` or `/oc` while reviewing code. OpenCode receives file path, line numbers, and diff context.   |
+| `issues`                      | Issue opened or edited                 | Automatically trigger OpenCode when issues are created or modified. Requires `prompt` input.                      |
+| `pull_request`                | PR opened or updated                   | Automatically trigger OpenCode when PRs are opened, synchronized, or reopened. Useful for automated reviews.      |
+| `schedule`                    | Cron-based schedule                    | Run OpenCode on a schedule. Requires `prompt` input. Output goes to logs and PRs (no issue to comment on).        |
+| `workflow_dispatch`           | Manual trigger from GitHub UI          | Trigger OpenCode on demand via Actions tab. Requires `prompt` input. Output goes to logs and PRs.                 |
 
 ### Schedule Example
 
@@ -145,9 +147,7 @@ jobs:
             If you find issues worth addressing, open an issue to track them.
 ```
 
-For scheduled events, the `prompt` input is **required** since there's no comment to extract instructions from.
-
-> **Note:** Scheduled workflows run without a user context to permission-check, so the workflow must grant `contents: write` and `pull-requests: write` if you expect OpenCode to create branches or PRs during a scheduled run.
+For scheduled events, the `prompt` input is **required** since there's no comment to extract instructions from. Scheduled workflows run without a user context to permission-check, so the workflow must grant `contents: write` and `pull-requests: write` if you expect OpenCode to create branches or PRs.
 
 ---
 
@@ -188,6 +188,59 @@ For `pull_request` events, if no `prompt` is provided, OpenCode defaults to revi
 
 ---
 
+### Issues Triage Example
+
+Automatically triage new issues. This example filters to accounts older than 30 days to reduce spam:
+
+```yaml title=".github/workflows/opencode-triage.yml"
+name: Issue Triage
+
+on:
+  issues:
+    types: [opened]
+
+jobs:
+  triage:
+    runs-on: ubuntu-latest
+    permissions:
+      id-token: write
+      contents: write
+      pull-requests: write
+      issues: write
+    steps:
+      - name: Check account age
+        id: check
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const user = await github.rest.users.getByUsername({
+              username: context.payload.issue.user.login
+            });
+            const created = new Date(user.data.created_at);
+            const days = (Date.now() - created) / (1000 * 60 * 60 * 24);
+            return days >= 30;
+          result-encoding: string
+
+      - uses: actions/checkout@v4
+        if: steps.check.outputs.result == 'true'
+
+      - uses: sst/opencode/github@latest
+        if: steps.check.outputs.result == 'true'
+        env:
+          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+        with:
+          model: anthropic/claude-sonnet-4-20250514
+          prompt: |
+            Review this issue. If there's a clear fix or relevant docs:
+            - Provide documentation links
+            - Add error handling guidance for code examples
+            Otherwise, do not comment.
+```
+
+For `issues` events, the `prompt` input is **required** since there's no comment to extract instructions from.
+
+---
+
 ## Custom prompts
 
 Override the default prompt to customize OpenCode's behavior for your workflow.