github: support issues and workflow_dispatch events (#6157)

This commit is contained in:
Matt Silverlock
2025-12-26 14:34:03 -05:00
committed by GitHub
parent 61ddd1716d
commit 1626341a4a
2 changed files with 123 additions and 36 deletions

View File

@@ -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}`
}