github: support issues and workflow_dispatch events (#6157)
This commit is contained in:
@@ -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}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user