summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMatt Silverlock <[email protected]>2025-12-26 14:34:03 -0500
committerGitHub <[email protected]>2025-12-26 13:34:03 -0600
commit1626341a4a7ce4e390c5d45d804ac02c928ca5fc (patch)
treecb676b058350357d06960850edd275d8d6f0fcbc
parent61ddd1716d86b0be060b70c5333ca32909c5e922 (diff)
downloadopencode-1626341a4a7ce4e390c5d45d804ac02c928ca5fc.tar.gz
opencode-1626341a4a7ce4e390c5d45d804ac02c928ca5fc.zip
github: support issues and workflow_dispatch events (#6157)
-rw-r--r--packages/opencode/src/cli/cmd/github.ts88
-rw-r--r--packages/web/src/content/docs/github.mdx71
2 files changed, 123 insertions, 36 deletions
diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts
index 37aed2426..748a96384 100644
--- a/packages/opencode/src/cli/cmd/github.ts
+++ b/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}`
}
diff --git a/packages/web/src/content/docs/github.mdx b/packages/web/src/content/docs/github.mdx
index 25c3ce927..63c5d855b 100644
--- a/packages/web/src/content/docs/github.mdx
+++ b/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.