@@ -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,7 +422,7 @@ 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 ( )
421427 const commentType = context . eventName === "pull_request_review_comment" ? "pr_review" : "issue"
422428
@@ -442,9 +448,11 @@ export const GithubRunCommand = cmd({
442448 if ( ! useGithubToken ) {
443449 await configureGit ( appToken )
444450 }
445- await assertPermissions ( )
446-
447- await addReaction ( commentType )
451+ // Skip permission check for schedule events (no actor to check)
452+ if ( ! isScheduleEvent ) {
453+ await assertPermissions ( )
454+ await addReaction ( commentType )
455+ }
448456
449457 // Setup opencode session
450458 const repoData = await fetchRepo ( )
@@ -458,11 +466,31 @@ export const GithubRunCommand = cmd({
458466 } ) ( )
459467 console . log ( "opencode session" , session . id )
460468
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 ) {
469+ // Handle 4 cases
470+ // 1. Schedule (no issue/PR context)
471+ // 2. Issue
472+ // 3. Local PR
473+ // 4. Fork PR
474+ if ( isScheduleEvent ) {
475+ // Schedule event - no issue/PR context, output goes to logs
476+ const branch = await checkoutNewBranch ( "schedule" )
477+ const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
478+ const response = await chat ( userPrompt , promptFiles )
479+ const { dirty, uncommittedChanges } = await branchIsDirty ( head )
480+ if ( dirty ) {
481+ const summary = await summarize ( response )
482+ await pushToNewBranch ( summary , branch , uncommittedChanges , true )
483+ const pr = await createPR (
484+ repoData . data . default_branch ,
485+ branch ,
486+ summary ,
487+ `${ response } \n\nTriggered by scheduled workflow${ footer ( { image : true } ) } ` ,
488+ )
489+ console . log ( `Created PR #${ pr } ` )
490+ } else {
491+ console . log ( "Response:" , response )
492+ }
493+ } else if ( context . eventName === "pull_request_review_comment" || issueEvent ?. issue . pull_request ) {
466494 const prData = await fetchPR ( )
467495 // Local PR
468496 if ( prData . headRepository . nameWithOwner === prData . baseRepository . nameWithOwner ) {
@@ -497,15 +525,15 @@ export const GithubRunCommand = cmd({
497525 }
498526 // Issue
499527 else {
500- const branch = await checkoutNewBranch ( )
528+ const branch = await checkoutNewBranch ( "issue" )
501529 const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
502530 const issueData = await fetchIssue ( )
503531 const dataPrompt = buildPromptDataForIssue ( issueData )
504532 const response = await chat ( `${ userPrompt } \n\n${ dataPrompt } ` , promptFiles )
505533 const { dirty, uncommittedChanges } = await branchIsDirty ( head )
506534 if ( dirty ) {
507535 const summary = await summarize ( response )
508- await pushToNewBranch ( summary , branch , uncommittedChanges )
536+ await pushToNewBranch ( summary , branch , uncommittedChanges , false )
509537 const pr = await createPR (
510538 repoData . data . default_branch ,
511539 branch ,
@@ -528,8 +556,10 @@ export const GithubRunCommand = cmd({
528556 } else if ( e instanceof Error ) {
529557 msg = e . message
530558 }
531- await createComment ( `${ msg } ${ footer ( ) } ` )
532- await removeReaction ( commentType )
559+ if ( ! isScheduleEvent ) {
560+ await createComment ( `${ msg } ${ footer ( ) } ` )
561+ await removeReaction ( commentType )
562+ }
533563 core . setFailed ( msg )
534564 // Also output the clean error message for the action to capture
535565 //core.setOutput("prepare_error", e.message);
@@ -605,6 +635,14 @@ export const GithubRunCommand = cmd({
605635
606636 async function getUserPrompt ( ) {
607637 const customPrompt = process . env [ "PROMPT" ]
638+ // For schedule events, PROMPT is required since there's no comment to extract from
639+ if ( isScheduleEvent ) {
640+ if ( ! customPrompt ) {
641+ throw new Error ( "PROMPT input is required for scheduled events" )
642+ }
643+ return { userPrompt : customPrompt , promptFiles : [ ] }
644+ }
645+
608646 if ( customPrompt ) {
609647 return { userPrompt : customPrompt , promptFiles : [ ] }
610648 }
@@ -615,7 +653,7 @@ export const GithubRunCommand = cmd({
615653 . map ( ( m ) => m . trim ( ) . toLowerCase ( ) )
616654 . filter ( Boolean )
617655 let prompt = ( ( ) => {
618- const body = payload . comment . body . trim ( )
656+ const body = payload ! . comment . body . trim ( )
619657 const bodyLower = body . toLowerCase ( )
620658 if ( mentions . some ( ( m ) => bodyLower === m ) ) {
621659 if ( reviewContext ) {
@@ -865,9 +903,9 @@ export const GithubRunCommand = cmd({
865903 await $ `git config --local ${ config } "${ gitConfig } "`
866904 }
867905
868- async function checkoutNewBranch ( ) {
906+ async function checkoutNewBranch ( type : "issue" | "schedule" ) {
869907 console . log ( "Checking out new branch..." )
870- const branch = generateBranchName ( "issue" )
908+ const branch = generateBranchName ( type )
871909 await $ `git checkout -b ${ branch } `
872910 return branch
873911 }
@@ -894,23 +932,32 @@ export const GithubRunCommand = cmd({
894932 await $ `git checkout -b ${ localBranch } fork/${ remoteBranch } `
895933 }
896934
897- function generateBranchName ( type : "issue" | "pr" ) {
935+ function generateBranchName ( type : "issue" | "pr" | "schedule" ) {
898936 const timestamp = new Date ( )
899937 . toISOString ( )
900938 . replace ( / [: -] / g, "" )
901939 . replace ( / \. \d { 3 } Z / , "" )
902940 . split ( "T" )
903941 . join ( "" )
942+ if ( type === "schedule" ) {
943+ const hex = crypto . randomUUID ( ) . slice ( 0 , 6 )
944+ return `opencode/scheduled-${ hex } -${ timestamp } `
945+ }
904946 return `opencode/${ type } ${ issueId } -${ timestamp } `
905947 }
906948
907- async function pushToNewBranch ( summary : string , branch : string , commit : boolean ) {
949+ async function pushToNewBranch ( summary : string , branch : string , commit : boolean , isSchedule : boolean ) {
908950 console . log ( "Pushing to new branch..." )
909951 if ( commit ) {
910952 await $ `git add .`
911- await $ `git commit -m "${ summary }
953+ if ( isSchedule ) {
954+ // No co-author for scheduled events - the schedule is operating as the repo
955+ await $ `git commit -m "${ summary } "`
956+ } else {
957+ await $ `git commit -m "${ summary }
912958
913959Co-authored-by: ${ actor } <${ actor } @users.noreply.github.com>"`
960+ }
914961 }
915962 await $ `git push -u origin ${ branch } `
916963 }
@@ -958,14 +1005,15 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
9581005 }
9591006
9601007 async function assertPermissions ( ) {
1008+ // Only called for non-schedule events, so actor is defined
9611009 console . log ( `Asserting permissions for user ${ actor } ...` )
9621010
9631011 let permission
9641012 try {
9651013 const response = await octoRest . repos . getCollaboratorPermissionLevel ( {
9661014 owner,
9671015 repo,
968- username : actor ,
1016+ username : actor ! ,
9691017 } )
9701018
9711019 permission = response . data . permission
@@ -979,30 +1027,32 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
9791027 }
9801028
9811029 async function addReaction ( commentType : "issue" | "pr_review" ) {
1030+ // Only called for non-schedule events, so triggerCommentId is defined
9821031 console . log ( "Adding reaction..." )
9831032 if ( commentType === "pr_review" ) {
9841033 return await octoRest . rest . reactions . createForPullRequestReviewComment ( {
9851034 owner,
9861035 repo,
987- comment_id : triggerCommentId ,
1036+ comment_id : triggerCommentId ! ,
9881037 content : AGENT_REACTION ,
9891038 } )
9901039 }
9911040 return await octoRest . rest . reactions . createForIssueComment ( {
9921041 owner,
9931042 repo,
994- comment_id : triggerCommentId ,
1043+ comment_id : triggerCommentId ! ,
9951044 content : AGENT_REACTION ,
9961045 } )
9971046 }
9981047
9991048 async function removeReaction ( commentType : "issue" | "pr_review" ) {
1049+ // Only called for non-schedule events, so triggerCommentId is defined
10001050 console . log ( "Removing reaction..." )
10011051 if ( commentType === "pr_review" ) {
10021052 const reactions = await octoRest . rest . reactions . listForPullRequestReviewComment ( {
10031053 owner,
10041054 repo,
1005- comment_id : triggerCommentId ,
1055+ comment_id : triggerCommentId ! ,
10061056 content : AGENT_REACTION ,
10071057 } )
10081058
@@ -1012,7 +1062,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10121062 await octoRest . rest . reactions . deleteForPullRequestComment ( {
10131063 owner,
10141064 repo,
1015- comment_id : triggerCommentId ,
1065+ comment_id : triggerCommentId ! ,
10161066 reaction_id : eyesReaction . id ,
10171067 } )
10181068 return
@@ -1021,7 +1071,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10211071 const reactions = await octoRest . rest . reactions . listForIssueComment ( {
10221072 owner,
10231073 repo,
1024- comment_id : triggerCommentId ,
1074+ comment_id : triggerCommentId ! ,
10251075 content : AGENT_REACTION ,
10261076 } )
10271077
@@ -1031,17 +1081,18 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10311081 await octoRest . rest . reactions . deleteForIssueComment ( {
10321082 owner,
10331083 repo,
1034- comment_id : triggerCommentId ,
1084+ comment_id : triggerCommentId ! ,
10351085 reaction_id : eyesReaction . id ,
10361086 } )
10371087 }
10381088
10391089 async function createComment ( body : string ) {
1090+ // Only called for non-schedule events, so issueId is defined
10401091 console . log ( "Creating comment..." )
10411092 return await octoRest . rest . issues . createComment ( {
10421093 owner,
10431094 repo,
1044- issue_number : issueId ,
1095+ issue_number : issueId ! ,
10451096 body,
10461097 } )
10471098 }
@@ -1119,10 +1170,11 @@ query($owner: String!, $repo: String!, $number: Int!) {
11191170 }
11201171
11211172 function buildPromptDataForIssue ( issue : GitHubIssue ) {
1173+ // Only called for non-schedule events, so payload is defined
11221174 const comments = ( issue . comments ?. nodes || [ ] )
11231175 . filter ( ( c ) => {
11241176 const id = parseInt ( c . databaseId )
1125- return id !== payload . comment . id
1177+ return id !== payload ! . comment . id
11261178 } )
11271179 . map ( ( c ) => ` - ${ c . author . login } at ${ c . createdAt } : ${ c . body } ` )
11281180
@@ -1246,10 +1298,11 @@ query($owner: String!, $repo: String!, $number: Int!) {
12461298 }
12471299
12481300 function buildPromptDataForPR ( pr : GitHubPullRequest ) {
1301+ // Only called for non-schedule events, so payload is defined
12491302 const comments = ( pr . comments ?. nodes || [ ] )
12501303 . filter ( ( c ) => {
12511304 const id = parseInt ( c . databaseId )
1252- return id !== payload . comment . id
1305+ return id !== payload ! . comment . id
12531306 } )
12541307 . map ( ( c ) => `- ${ c . author . login } at ${ c . createdAt } : ${ c . body } ` )
12551308
0 commit comments