Skip to content

Commit 1626341

Browse files
authored
github: support issues and workflow_dispatch events (anomalyco#6157)
1 parent 61ddd17 commit 1626341

File tree

2 files changed

+123
-36
lines changed

2 files changed

+123
-36
lines changed

packages/opencode/src/cli/cmd/github.ts

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import * as github from "@actions/github"
99
import type { Context } from "@actions/github/lib/context"
1010
import type {
1111
IssueCommentEvent,
12+
IssuesEvent,
1213
PullRequestReviewCommentEvent,
14+
WorkflowDispatchEvent,
1315
WorkflowRunEvent,
1416
PullRequestEvent,
1517
} from "@octokit/webhooks-types"
@@ -132,7 +134,16 @@ type IssueQueryResponse = {
132134
const AGENT_USERNAME = "opencode-agent[bot]"
133135
const AGENT_REACTION = "eyes"
134136
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
135-
const SUPPORTED_EVENTS = ["issue_comment", "pull_request_review_comment", "schedule", "pull_request"] as const
137+
138+
// Event categories for routing
139+
// USER_EVENTS: triggered by user actions, have actor/issueId, support reactions/comments
140+
// REPO_EVENTS: triggered by automation, no actor/issueId, output to logs/PR only
141+
const USER_EVENTS = ["issue_comment", "pull_request_review_comment", "issues", "pull_request"] as const
142+
const REPO_EVENTS = ["schedule", "workflow_dispatch"] as const
143+
const SUPPORTED_EVENTS = [...USER_EVENTS, ...REPO_EVENTS] as const
144+
145+
type UserEvent = (typeof USER_EVENTS)[number]
146+
type RepoEvent = (typeof REPO_EVENTS)[number]
136147

137148
// Parses GitHub remote URLs in various formats:
138149
// - https://github.com/owner/repo.git
@@ -397,27 +408,38 @@ export const GithubRunCommand = cmd({
397408
core.setFailed(`Unsupported event type: ${context.eventName}`)
398409
process.exit(1)
399410
}
411+
412+
// Determine event category for routing
413+
// USER_EVENTS: have actor, issueId, support reactions/comments
414+
// REPO_EVENTS: no actor/issueId, output to logs/PR only
415+
const isUserEvent = USER_EVENTS.includes(context.eventName as UserEvent)
416+
const isRepoEvent = REPO_EVENTS.includes(context.eventName as RepoEvent)
400417
const isCommentEvent = ["issue_comment", "pull_request_review_comment"].includes(context.eventName)
418+
const isIssuesEvent = context.eventName === "issues"
401419
const isScheduleEvent = context.eventName === "schedule"
420+
const isWorkflowDispatchEvent = context.eventName === "workflow_dispatch"
402421

403422
const { providerID, modelID } = normalizeModel()
404423
const runId = normalizeRunId()
405424
const share = normalizeShare()
406425
const oidcBaseUrl = normalizeOidcBaseUrl()
407426
const { owner, repo } = context.repo
408-
// For schedule events, payload has no issue/comment data
427+
// For repo events (schedule, workflow_dispatch), payload has no issue/comment data
409428
const payload = context.payload as
410429
| IssueCommentEvent
430+
| IssuesEvent
411431
| PullRequestReviewCommentEvent
432+
| WorkflowDispatchEvent
412433
| WorkflowRunEvent
413434
| PullRequestEvent
414435
const issueEvent = isIssueCommentEvent(payload) ? payload : undefined
436+
// workflow_dispatch has an actor (the user who triggered it), schedule does not
415437
const actor = isScheduleEvent ? undefined : context.actor
416438

417-
const issueId = isScheduleEvent
439+
const issueId = isRepoEvent
418440
? undefined
419-
: context.eventName === "issue_comment"
420-
? (payload as IssueCommentEvent).issue.number
441+
: context.eventName === "issue_comment" || context.eventName === "issues"
442+
? (payload as IssueCommentEvent | IssuesEvent).issue.number
421443
: (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number
422444
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
423445
const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"
@@ -462,8 +484,8 @@ export const GithubRunCommand = cmd({
462484
if (!useGithubToken) {
463485
await configureGit(appToken)
464486
}
465-
// Skip permission check for schedule events (no actor to check)
466-
if (!isScheduleEvent) {
487+
// Skip permission check and reactions for repo events (no actor to check, no issue to react to)
488+
if (isUserEvent) {
467489
await assertPermissions()
468490
await addReaction(commentType)
469491
}
@@ -480,25 +502,30 @@ export const GithubRunCommand = cmd({
480502
})()
481503
console.log("opencode session", session.id)
482504

483-
// Handle 4 cases
484-
// 1. Schedule (no issue/PR context)
485-
// 2. Issue
486-
// 3. Local PR
487-
// 4. Fork PR
488-
if (isScheduleEvent) {
489-
// Schedule event - no issue/PR context, output goes to logs
490-
const branch = await checkoutNewBranch("schedule")
505+
// Handle event types:
506+
// REPO_EVENTS (schedule, workflow_dispatch): no issue/PR context, output to logs/PR only
507+
// USER_EVENTS on PR (pull_request, pull_request_review_comment, issue_comment on PR): work on PR branch
508+
// USER_EVENTS on Issue (issue_comment on issue, issues): create new branch, may create PR
509+
if (isRepoEvent) {
510+
// Repo event - no issue/PR context, output goes to logs
511+
if (isWorkflowDispatchEvent && actor) {
512+
console.log(`Triggered by: ${actor}`)
513+
}
514+
const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule"
515+
const branch = await checkoutNewBranch(branchPrefix)
491516
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
492517
const response = await chat(userPrompt, promptFiles)
493518
const { dirty, uncommittedChanges } = await branchIsDirty(head)
494519
if (dirty) {
495520
const summary = await summarize(response)
496-
await pushToNewBranch(summary, branch, uncommittedChanges, true)
521+
// workflow_dispatch has an actor for co-author attribution, schedule does not
522+
await pushToNewBranch(summary, branch, uncommittedChanges, isScheduleEvent)
523+
const triggerType = isWorkflowDispatchEvent ? "workflow_dispatch" : "scheduled workflow"
497524
const pr = await createPR(
498525
repoData.data.default_branch,
499526
branch,
500527
summary,
501-
`${response}\n\nTriggered by scheduled workflow${footer({ image: true })}`,
528+
`${response}\n\nTriggered by ${triggerType}${footer({ image: true })}`,
502529
)
503530
console.log(`Created PR #${pr}`)
504531
} else {
@@ -573,7 +600,7 @@ export const GithubRunCommand = cmd({
573600
} else if (e instanceof Error) {
574601
msg = e.message
575602
}
576-
if (!isScheduleEvent) {
603+
if (isUserEvent) {
577604
await createComment(`${msg}${footer()}`)
578605
await removeReaction(commentType)
579606
}
@@ -628,9 +655,15 @@ export const GithubRunCommand = cmd({
628655
}
629656

630657
function isIssueCommentEvent(
631-
event: IssueCommentEvent | PullRequestReviewCommentEvent | WorkflowRunEvent | PullRequestEvent,
658+
event:
659+
| IssueCommentEvent
660+
| IssuesEvent
661+
| PullRequestReviewCommentEvent
662+
| WorkflowDispatchEvent
663+
| WorkflowRunEvent
664+
| PullRequestEvent,
632665
): event is IssueCommentEvent {
633-
return "issue" in event
666+
return "issue" in event && "comment" in event
634667
}
635668

636669
function getReviewCommentContext() {
@@ -652,10 +685,11 @@ export const GithubRunCommand = cmd({
652685

653686
async function getUserPrompt() {
654687
const customPrompt = process.env["PROMPT"]
655-
// For schedule events, PROMPT is required since there's no comment to extract from
656-
if (isScheduleEvent) {
688+
// For repo events and issues events, PROMPT is required since there's no comment to extract from
689+
if (isRepoEvent || isIssuesEvent) {
657690
if (!customPrompt) {
658-
throw new Error("PROMPT input is required for scheduled events")
691+
const eventType = isRepoEvent ? "scheduled and workflow_dispatch" : "issues"
692+
throw new Error(`PROMPT input is required for ${eventType} events`)
659693
}
660694
return { userPrompt: customPrompt, promptFiles: [] }
661695
}
@@ -923,7 +957,7 @@ export const GithubRunCommand = cmd({
923957
await $`git config --local ${config} "${gitConfig}"`
924958
}
925959

926-
async function checkoutNewBranch(type: "issue" | "schedule") {
960+
async function checkoutNewBranch(type: "issue" | "schedule" | "dispatch") {
927961
console.log("Checking out new branch...")
928962
const branch = generateBranchName(type)
929963
await $`git checkout -b ${branch}`
@@ -952,16 +986,16 @@ export const GithubRunCommand = cmd({
952986
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
953987
}
954988

955-
function generateBranchName(type: "issue" | "pr" | "schedule") {
989+
function generateBranchName(type: "issue" | "pr" | "schedule" | "dispatch") {
956990
const timestamp = new Date()
957991
.toISOString()
958992
.replace(/[:-]/g, "")
959993
.replace(/\.\d{3}Z/, "")
960994
.split("T")
961995
.join("")
962-
if (type === "schedule") {
996+
if (type === "schedule" || type === "dispatch") {
963997
const hex = crypto.randomUUID().slice(0, 6)
964-
return `opencode/scheduled-${hex}-${timestamp}`
998+
return `opencode/${type}-${hex}-${timestamp}`
965999
}
9661000
return `opencode/${type}${issueId}-${timestamp}`
9671001
}

packages/web/src/content/docs/github.mdx

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,14 @@ Or you can set it up manually.
104104
105105
OpenCode can be triggered by the following GitHub events:
106106
107-
| Event Type | Triggered By | Details |
108-
| ----------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
109-
| `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. |
110-
| `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. |
111-
| `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. |
112-
| `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. |
107+
| Event Type | Triggered By | Details |
108+
| ----------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
109+
| `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. |
110+
| `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. |
111+
| `issues` | Issue opened or edited | Automatically trigger OpenCode when issues are created or modified. Requires `prompt` input. |
112+
| `pull_request` | PR opened or updated | Automatically trigger OpenCode when PRs are opened, synchronized, or reopened. Useful for automated reviews. |
113+
| `schedule` | Cron-based schedule | Run OpenCode on a schedule. Requires `prompt` input. Output goes to logs and PRs (no issue to comment on). |
114+
| `workflow_dispatch` | Manual trigger from GitHub UI | Trigger OpenCode on demand via Actions tab. Requires `prompt` input. Output goes to logs and PRs. |
113115

114116
### Schedule Example
115117

@@ -145,9 +147,7 @@ jobs:
145147
If you find issues worth addressing, open an issue to track them.
146148
```
147149

148-
For scheduled events, the `prompt` input is **required** since there's no comment to extract instructions from.
149-
150-
> **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.
150+
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.
151151

152152
---
153153

@@ -188,6 +188,59 @@ For `pull_request` events, if no `prompt` is provided, OpenCode defaults to revi
188188

189189
---
190190

191+
### Issues Triage Example
192+
193+
Automatically triage new issues. This example filters to accounts older than 30 days to reduce spam:
194+
195+
```yaml title=".github/workflows/opencode-triage.yml"
196+
name: Issue Triage
197+
198+
on:
199+
issues:
200+
types: [opened]
201+
202+
jobs:
203+
triage:
204+
runs-on: ubuntu-latest
205+
permissions:
206+
id-token: write
207+
contents: write
208+
pull-requests: write
209+
issues: write
210+
steps:
211+
- name: Check account age
212+
id: check
213+
uses: actions/github-script@v7
214+
with:
215+
script: |
216+
const user = await github.rest.users.getByUsername({
217+
username: context.payload.issue.user.login
218+
});
219+
const created = new Date(user.data.created_at);
220+
const days = (Date.now() - created) / (1000 * 60 * 60 * 24);
221+
return days >= 30;
222+
result-encoding: string
223+
224+
- uses: actions/checkout@v4
225+
if: steps.check.outputs.result == 'true'
226+
227+
- uses: sst/opencode/github@latest
228+
if: steps.check.outputs.result == 'true'
229+
env:
230+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
231+
with:
232+
model: anthropic/claude-sonnet-4-20250514
233+
prompt: |
234+
Review this issue. If there's a clear fix or relevant docs:
235+
- Provide documentation links
236+
- Add error handling guidance for code examples
237+
Otherwise, do not comment.
238+
```
239+
240+
For `issues` events, the `prompt` input is **required** since there's no comment to extract instructions from.
241+
242+
---
243+
191244
## Custom prompts
192245

193246
Override the default prompt to customize OpenCode's behavior for your workflow.

0 commit comments

Comments
 (0)