@@ -127,6 +127,7 @@ type IssueQueryResponse = {
127127const AGENT_USERNAME = "opencode-agent[bot]"
128128const AGENT_REACTION = "eyes"
129129const WORKFLOW_FILE = ".github/workflows/opencode.yml"
130+ const SUPPORTED_EVENTS = [ "issue_comment" , "pull_request_review_comment" , "schedule" ] as const
130131
131132// Parses GitHub remote URLs in various formats:
132133// - https://github.com/owner/repo.git
@@ -387,22 +388,27 @@ export const GithubRunCommand = cmd({
387388 const isMock = args . token || args . event
388389
389390 const context = isMock ? ( JSON . parse ( args . event ! ) as Context ) : github . context
390- if ( context . eventName !== "issue_comment" && context . eventName !== "pull_request_review_comment" ) {
391+ if ( ! SUPPORTED_EVENTS . includes ( context . eventName as ( typeof SUPPORTED_EVENTS ) [ number ] ) ) {
391392 core . setFailed ( `Unsupported event type: ${ context . eventName } ` )
392393 process . exit ( 1 )
393394 }
395+ const isScheduleEvent = context . eventName === "schedule"
394396
395397 const { providerID, modelID } = normalizeModel ( )
396398 const runId = normalizeRunId ( )
397399 const share = normalizeShare ( )
398400 const oidcBaseUrl = normalizeOidcBaseUrl ( )
399401 const { owner, repo } = context . repo
400- const payload = context . payload as IssueCommentEvent | PullRequestReviewCommentEvent
401- const issueEvent = isIssueCommentEvent ( payload ) ? payload : undefined
402- const actor = context . actor
403-
404- const issueId =
405- context . eventName === "pull_request_review_comment"
402+ // For schedule events, payload has no issue/comment data
403+ const payload = isScheduleEvent
404+ ? undefined
405+ : ( context . payload as IssueCommentEvent | PullRequestReviewCommentEvent )
406+ const issueEvent = payload && isIssueCommentEvent ( payload ) ? payload : undefined
407+ const actor = isScheduleEvent ? undefined : context . actor
408+
409+ const issueId = isScheduleEvent
410+ ? undefined
411+ : context . eventName === "pull_request_review_comment"
406412 ? ( payload as PullRequestReviewCommentEvent ) . pull_request . number
407413 : ( payload as IssueCommentEvent ) . issue . number
408414 const runUrl = `/${ owner } /${ repo } /actions/runs/${ runId } `
@@ -416,9 +422,13 @@ export const GithubRunCommand = cmd({
416422 let shareId : string | undefined
417423 let exitCode = 0
418424 type PromptFiles = Awaited < ReturnType < typeof getUserPrompt > > [ "promptFiles" ]
419- const triggerCommentId = payload . comment . id
425+ const triggerCommentId = payload ? .comment . id
420426 const useGithubToken = normalizeUseGithubToken ( )
421- const commentType = context . eventName === "pull_request_review_comment" ? "pr_review" : "issue"
427+ const commentType = isScheduleEvent
428+ ? undefined
429+ : context . eventName === "pull_request_review_comment"
430+ ? "pr_review"
431+ : "issue"
422432
423433 try {
424434 if ( useGithubToken ) {
@@ -442,9 +452,11 @@ export const GithubRunCommand = cmd({
442452 if ( ! useGithubToken ) {
443453 await configureGit ( appToken )
444454 }
445- await assertPermissions ( )
446-
447- await addReaction ( commentType )
455+ // Skip permission check for schedule events (no actor to check)
456+ if ( ! isScheduleEvent ) {
457+ await assertPermissions ( )
458+ await addReaction ( commentType ! )
459+ }
448460
449461 // Setup opencode session
450462 const repoData = await fetchRepo ( )
@@ -458,11 +470,31 @@ export const GithubRunCommand = cmd({
458470 } ) ( )
459471 console . log ( "opencode session" , session . id )
460472
461- // Handle 3 cases
462- // 1. Issue
463- // 2. Local PR
464- // 3. Fork PR
465- if ( context . eventName === "pull_request_review_comment" || issueEvent ?. issue . pull_request ) {
473+ // Handle 4 cases
474+ // 1. Schedule (no issue/PR context)
475+ // 2. Issue
476+ // 3. Local PR
477+ // 4. Fork PR
478+ if ( isScheduleEvent ) {
479+ // Schedule event - no issue/PR context, output goes to logs
480+ const branch = await checkoutNewBranch ( "schedule" )
481+ const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
482+ const response = await chat ( userPrompt , promptFiles )
483+ const { dirty, uncommittedChanges } = await branchIsDirty ( head )
484+ if ( dirty ) {
485+ const summary = await summarize ( response )
486+ await pushToNewBranch ( summary , branch , uncommittedChanges , true )
487+ const pr = await createPR (
488+ repoData . data . default_branch ,
489+ branch ,
490+ summary ,
491+ `${ response } \n\nTriggered by scheduled workflow${ footer ( { image : true } ) } ` ,
492+ )
493+ console . log ( `Created PR #${ pr } ` )
494+ } else {
495+ console . log ( "Response:" , response )
496+ }
497+ } else if ( context . eventName === "pull_request_review_comment" || issueEvent ?. issue . pull_request ) {
466498 const prData = await fetchPR ( )
467499 // Local PR
468500 if ( prData . headRepository . nameWithOwner === prData . baseRepository . nameWithOwner ) {
@@ -477,7 +509,7 @@ export const GithubRunCommand = cmd({
477509 }
478510 const hasShared = prData . comments . nodes . some ( ( c ) => c . body . includes ( `${ shareBaseUrl } /s/${ shareId } ` ) )
479511 await createComment ( `${ response } ${ footer ( { image : ! hasShared } ) } ` )
480- await removeReaction ( commentType )
512+ await removeReaction ( commentType ! )
481513 }
482514 // Fork PR
483515 else {
@@ -492,31 +524,31 @@ export const GithubRunCommand = cmd({
492524 }
493525 const hasShared = prData . comments . nodes . some ( ( c ) => c . body . includes ( `${ shareBaseUrl } /s/${ shareId } ` ) )
494526 await createComment ( `${ response } ${ footer ( { image : ! hasShared } ) } ` )
495- await removeReaction ( commentType )
527+ await removeReaction ( commentType ! )
496528 }
497529 }
498530 // Issue
499531 else {
500- const branch = await checkoutNewBranch ( )
532+ const branch = await checkoutNewBranch ( "issue" )
501533 const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
502534 const issueData = await fetchIssue ( )
503535 const dataPrompt = buildPromptDataForIssue ( issueData )
504536 const response = await chat ( `${ userPrompt } \n\n${ dataPrompt } ` , promptFiles )
505537 const { dirty, uncommittedChanges } = await branchIsDirty ( head )
506538 if ( dirty ) {
507539 const summary = await summarize ( response )
508- await pushToNewBranch ( summary , branch , uncommittedChanges )
540+ await pushToNewBranch ( summary , branch , uncommittedChanges , false )
509541 const pr = await createPR (
510542 repoData . data . default_branch ,
511543 branch ,
512544 summary ,
513545 `${ response } \n\nCloses #${ issueId } ${ footer ( { image : true } ) } ` ,
514546 )
515547 await createComment ( `Created PR #${ pr } ${ footer ( { image : true } ) } ` )
516- await removeReaction ( commentType )
548+ await removeReaction ( commentType ! )
517549 } else {
518550 await createComment ( `${ response } ${ footer ( { image : true } ) } ` )
519- await removeReaction ( commentType )
551+ await removeReaction ( commentType ! )
520552 }
521553 }
522554 } catch ( e : any ) {
@@ -528,8 +560,10 @@ export const GithubRunCommand = cmd({
528560 } else if ( e instanceof Error ) {
529561 msg = e . message
530562 }
531- await createComment ( `${ msg } ${ footer ( ) } ` )
532- await removeReaction ( commentType )
563+ if ( ! isScheduleEvent ) {
564+ await createComment ( `${ msg } ${ footer ( ) } ` )
565+ await removeReaction ( commentType ! )
566+ }
533567 core . setFailed ( msg )
534568 // Also output the clean error message for the action to capture
535569 //core.setOutput("prepare_error", e.message);
@@ -605,6 +639,14 @@ export const GithubRunCommand = cmd({
605639
606640 async function getUserPrompt ( ) {
607641 const customPrompt = process . env [ "PROMPT" ]
642+ // For schedule events, PROMPT is required since there's no comment to extract from
643+ if ( isScheduleEvent ) {
644+ if ( ! customPrompt ) {
645+ throw new Error ( "PROMPT input is required for scheduled events" )
646+ }
647+ return { userPrompt : customPrompt , promptFiles : [ ] }
648+ }
649+
608650 if ( customPrompt ) {
609651 return { userPrompt : customPrompt , promptFiles : [ ] }
610652 }
@@ -615,7 +657,7 @@ export const GithubRunCommand = cmd({
615657 . map ( ( m ) => m . trim ( ) . toLowerCase ( ) )
616658 . filter ( Boolean )
617659 let prompt = ( ( ) => {
618- const body = payload . comment . body . trim ( )
660+ const body = payload ! . comment . body . trim ( )
619661 const bodyLower = body . toLowerCase ( )
620662 if ( mentions . some ( ( m ) => bodyLower === m ) ) {
621663 if ( reviewContext ) {
@@ -865,9 +907,9 @@ export const GithubRunCommand = cmd({
865907 await $ `git config --local ${ config } "${ gitConfig } "`
866908 }
867909
868- async function checkoutNewBranch ( ) {
910+ async function checkoutNewBranch ( type : "issue" | "schedule" ) {
869911 console . log ( "Checking out new branch..." )
870- const branch = generateBranchName ( "issue" )
912+ const branch = generateBranchName ( type )
871913 await $ `git checkout -b ${ branch } `
872914 return branch
873915 }
@@ -894,23 +936,32 @@ export const GithubRunCommand = cmd({
894936 await $ `git checkout -b ${ localBranch } fork/${ remoteBranch } `
895937 }
896938
897- function generateBranchName ( type : "issue" | "pr" ) {
939+ function generateBranchName ( type : "issue" | "pr" | "schedule" ) {
898940 const timestamp = new Date ( )
899941 . toISOString ( )
900942 . replace ( / [: -] / g, "" )
901943 . replace ( / \. \d { 3 } Z / , "" )
902944 . split ( "T" )
903945 . join ( "" )
946+ if ( type === "schedule" ) {
947+ const hex = crypto . randomUUID ( ) . slice ( 0 , 6 )
948+ return `opencode/scheduled-${ hex } -${ timestamp } `
949+ }
904950 return `opencode/${ type } ${ issueId } -${ timestamp } `
905951 }
906952
907- async function pushToNewBranch ( summary : string , branch : string , commit : boolean ) {
953+ async function pushToNewBranch ( summary : string , branch : string , commit : boolean , isSchedule : boolean ) {
908954 console . log ( "Pushing to new branch..." )
909955 if ( commit ) {
910956 await $ `git add .`
911- await $ `git commit -m "${ summary }
957+ if ( isSchedule ) {
958+ // No co-author for scheduled events - the schedule is operating as the repo
959+ await $ `git commit -m "${ summary } "`
960+ } else {
961+ await $ `git commit -m "${ summary }
912962
913963Co-authored-by: ${ actor } <${ actor } @users.noreply.github.com>"`
964+ }
914965 }
915966 await $ `git push -u origin ${ branch } `
916967 }
@@ -958,14 +1009,15 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
9581009 }
9591010
9601011 async function assertPermissions ( ) {
1012+ // Only called for non-schedule events, so actor is defined
9611013 console . log ( `Asserting permissions for user ${ actor } ...` )
9621014
9631015 let permission
9641016 try {
9651017 const response = await octoRest . repos . getCollaboratorPermissionLevel ( {
9661018 owner,
9671019 repo,
968- username : actor ,
1020+ username : actor ! ,
9691021 } )
9701022
9711023 permission = response . data . permission
@@ -979,30 +1031,32 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
9791031 }
9801032
9811033 async function addReaction ( commentType : "issue" | "pr_review" ) {
1034+ // Only called for non-schedule events, so triggerCommentId is defined
9821035 console . log ( "Adding reaction..." )
9831036 if ( commentType === "pr_review" ) {
9841037 return await octoRest . rest . reactions . createForPullRequestReviewComment ( {
9851038 owner,
9861039 repo,
987- comment_id : triggerCommentId ,
1040+ comment_id : triggerCommentId ! ,
9881041 content : AGENT_REACTION ,
9891042 } )
9901043 }
9911044 return await octoRest . rest . reactions . createForIssueComment ( {
9921045 owner,
9931046 repo,
994- comment_id : triggerCommentId ,
1047+ comment_id : triggerCommentId ! ,
9951048 content : AGENT_REACTION ,
9961049 } )
9971050 }
9981051
9991052 async function removeReaction ( commentType : "issue" | "pr_review" ) {
1053+ // Only called for non-schedule events, so triggerCommentId is defined
10001054 console . log ( "Removing reaction..." )
10011055 if ( commentType === "pr_review" ) {
10021056 const reactions = await octoRest . rest . reactions . listForPullRequestReviewComment ( {
10031057 owner,
10041058 repo,
1005- comment_id : triggerCommentId ,
1059+ comment_id : triggerCommentId ! ,
10061060 content : AGENT_REACTION ,
10071061 } )
10081062
@@ -1012,7 +1066,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10121066 await octoRest . rest . reactions . deleteForPullRequestComment ( {
10131067 owner,
10141068 repo,
1015- comment_id : triggerCommentId ,
1069+ comment_id : triggerCommentId ! ,
10161070 reaction_id : eyesReaction . id ,
10171071 } )
10181072 return
@@ -1021,7 +1075,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10211075 const reactions = await octoRest . rest . reactions . listForIssueComment ( {
10221076 owner,
10231077 repo,
1024- comment_id : triggerCommentId ,
1078+ comment_id : triggerCommentId ! ,
10251079 content : AGENT_REACTION ,
10261080 } )
10271081
@@ -1031,17 +1085,18 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10311085 await octoRest . rest . reactions . deleteForIssueComment ( {
10321086 owner,
10331087 repo,
1034- comment_id : triggerCommentId ,
1088+ comment_id : triggerCommentId ! ,
10351089 reaction_id : eyesReaction . id ,
10361090 } )
10371091 }
10381092
10391093 async function createComment ( body : string ) {
1094+ // Only called for non-schedule events, so issueId is defined
10401095 console . log ( "Creating comment..." )
10411096 return await octoRest . rest . issues . createComment ( {
10421097 owner,
10431098 repo,
1044- issue_number : issueId ,
1099+ issue_number : issueId ! ,
10451100 body,
10461101 } )
10471102 }
@@ -1119,10 +1174,11 @@ query($owner: String!, $repo: String!, $number: Int!) {
11191174 }
11201175
11211176 function buildPromptDataForIssue ( issue : GitHubIssue ) {
1177+ // Only called for non-schedule events, so payload is defined
11221178 const comments = ( issue . comments ?. nodes || [ ] )
11231179 . filter ( ( c ) => {
11241180 const id = parseInt ( c . databaseId )
1125- return id !== payload . comment . id
1181+ return id !== payload ! . comment . id
11261182 } )
11271183 . map ( ( c ) => ` - ${ c . author . login } at ${ c . createdAt } : ${ c . body } ` )
11281184
@@ -1246,10 +1302,11 @@ query($owner: String!, $repo: String!, $number: Int!) {
12461302 }
12471303
12481304 function buildPromptDataForPR ( pr : GitHubPullRequest ) {
1305+ // Only called for non-schedule events, so payload is defined
12491306 const comments = ( pr . comments ?. nodes || [ ] )
12501307 . filter ( ( c ) => {
12511308 const id = parseInt ( c . databaseId )
1252- return id !== payload . comment . id
1309+ return id !== payload ! . comment . id
12531310 } )
12541311 . map ( ( c ) => `- ${ c . author . login } at ${ c . createdAt } : ${ c . body } ` )
12551312
0 commit comments