@@ -387,7 +387,12 @@ export const GithubRunCommand = cmd({
387387 const isMock = args . token || args . event
388388
389389 const context = isMock ? ( JSON . parse ( args . event ! ) as Context ) : github . context
390- if ( context . eventName !== "issue_comment" && context . eventName !== "pull_request_review_comment" ) {
390+ const isScheduleEvent = context . eventName === "schedule"
391+ if (
392+ context . eventName !== "issue_comment" &&
393+ context . eventName !== "pull_request_review_comment" &&
394+ context . eventName !== "schedule"
395+ ) {
391396 core . setFailed ( `Unsupported event type: ${ context . eventName } ` )
392397 process . exit ( 1 )
393398 }
@@ -397,12 +402,16 @@ export const GithubRunCommand = cmd({
397402 const share = normalizeShare ( )
398403 const oidcBaseUrl = normalizeOidcBaseUrl ( )
399404 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"
405+ // For schedule events, payload has no issue/comment data
406+ const payload = isScheduleEvent
407+ ? undefined
408+ : ( context . payload as IssueCommentEvent | PullRequestReviewCommentEvent )
409+ const issueEvent = payload && isIssueCommentEvent ( payload ) ? payload : undefined
410+ const actor = isScheduleEvent ? undefined : context . actor
411+
412+ const issueId = isScheduleEvent
413+ ? undefined
414+ : context . eventName === "pull_request_review_comment"
406415 ? ( payload as PullRequestReviewCommentEvent ) . pull_request . number
407416 : ( payload as IssueCommentEvent ) . issue . number
408417 const runUrl = `/${ owner } /${ repo } /actions/runs/${ runId } `
@@ -416,7 +425,7 @@ export const GithubRunCommand = cmd({
416425 let shareId : string | undefined
417426 let exitCode = 0
418427 type PromptFiles = Awaited < ReturnType < typeof getUserPrompt > > [ "promptFiles" ]
419- const triggerCommentId = payload . comment . id
428+ const triggerCommentId = payload ? .comment . id
420429 const useGithubToken = normalizeUseGithubToken ( )
421430 const commentType = context . eventName === "pull_request_review_comment" ? "pr_review" : "issue"
422431
@@ -442,9 +451,11 @@ export const GithubRunCommand = cmd({
442451 if ( ! useGithubToken ) {
443452 await configureGit ( appToken )
444453 }
445- await assertPermissions ( )
446-
447- await addReaction ( commentType )
454+ // Skip permission check for schedule events (no actor to check)
455+ if ( ! isScheduleEvent ) {
456+ await assertPermissions ( )
457+ await addReaction ( commentType )
458+ }
448459
449460 // Setup opencode session
450461 const repoData = await fetchRepo ( )
@@ -458,11 +469,31 @@ export const GithubRunCommand = cmd({
458469 } ) ( )
459470 console . log ( "opencode session" , session . id )
460471
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 ) {
472+ // Handle 4 cases
473+ // 1. Schedule (no issue/PR context)
474+ // 2. Issue
475+ // 3. Local PR
476+ // 4. Fork PR
477+ if ( isScheduleEvent ) {
478+ // Schedule event - no issue/PR context, output goes to logs
479+ const branch = await checkoutNewBranch ( "schedule" )
480+ const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
481+ const response = await chat ( userPrompt , promptFiles )
482+ const { dirty, uncommittedChanges } = await branchIsDirty ( head )
483+ if ( dirty ) {
484+ const summary = await summarize ( response )
485+ await pushToNewBranch ( summary , branch , uncommittedChanges , true )
486+ const pr = await createPR (
487+ repoData . data . default_branch ,
488+ branch ,
489+ summary ,
490+ `${ response } \n\nTriggered by scheduled workflow${ footer ( { image : true } ) } ` ,
491+ )
492+ console . log ( `Created PR #${ pr } ` )
493+ } else {
494+ console . log ( "Response:" , response )
495+ }
496+ } else if ( context . eventName === "pull_request_review_comment" || issueEvent ?. issue . pull_request ) {
466497 const prData = await fetchPR ( )
467498 // Local PR
468499 if ( prData . headRepository . nameWithOwner === prData . baseRepository . nameWithOwner ) {
@@ -497,15 +528,15 @@ export const GithubRunCommand = cmd({
497528 }
498529 // Issue
499530 else {
500- const branch = await checkoutNewBranch ( )
531+ const branch = await checkoutNewBranch ( "issue" )
501532 const head = ( await $ `git rev-parse HEAD` ) . stdout . toString ( ) . trim ( )
502533 const issueData = await fetchIssue ( )
503534 const dataPrompt = buildPromptDataForIssue ( issueData )
504535 const response = await chat ( `${ userPrompt } \n\n${ dataPrompt } ` , promptFiles )
505536 const { dirty, uncommittedChanges } = await branchIsDirty ( head )
506537 if ( dirty ) {
507538 const summary = await summarize ( response )
508- await pushToNewBranch ( summary , branch , uncommittedChanges )
539+ await pushToNewBranch ( summary , branch , uncommittedChanges , false )
509540 const pr = await createPR (
510541 repoData . data . default_branch ,
511542 branch ,
@@ -605,6 +636,14 @@ export const GithubRunCommand = cmd({
605636
606637 async function getUserPrompt ( ) {
607638 const customPrompt = process . env [ "PROMPT" ]
639+ // For schedule events, PROMPT is required since there's no comment to extract from
640+ if ( isScheduleEvent ) {
641+ if ( ! customPrompt ) {
642+ throw new Error ( "PROMPT input is required for scheduled events" )
643+ }
644+ return { userPrompt : customPrompt , promptFiles : [ ] }
645+ }
646+
608647 if ( customPrompt ) {
609648 return { userPrompt : customPrompt , promptFiles : [ ] }
610649 }
@@ -615,7 +654,7 @@ export const GithubRunCommand = cmd({
615654 . map ( ( m ) => m . trim ( ) . toLowerCase ( ) )
616655 . filter ( Boolean )
617656 let prompt = ( ( ) => {
618- const body = payload . comment . body . trim ( )
657+ const body = payload ! . comment . body . trim ( )
619658 const bodyLower = body . toLowerCase ( )
620659 if ( mentions . some ( ( m ) => bodyLower === m ) ) {
621660 if ( reviewContext ) {
@@ -865,9 +904,9 @@ export const GithubRunCommand = cmd({
865904 await $ `git config --local ${ config } "${ gitConfig } "`
866905 }
867906
868- async function checkoutNewBranch ( ) {
907+ async function checkoutNewBranch ( type : "issue" | "schedule" ) {
869908 console . log ( "Checking out new branch..." )
870- const branch = generateBranchName ( "issue" )
909+ const branch = generateBranchName ( type )
871910 await $ `git checkout -b ${ branch } `
872911 return branch
873912 }
@@ -894,23 +933,32 @@ export const GithubRunCommand = cmd({
894933 await $ `git checkout -b ${ localBranch } fork/${ remoteBranch } `
895934 }
896935
897- function generateBranchName ( type : "issue" | "pr" ) {
936+ function generateBranchName ( type : "issue" | "pr" | "schedule" ) {
898937 const timestamp = new Date ( )
899938 . toISOString ( )
900939 . replace ( / [: -] / g, "" )
901940 . replace ( / \. \d { 3 } Z / , "" )
902941 . split ( "T" )
903942 . join ( "" )
943+ if ( type === "schedule" ) {
944+ const hex = crypto . randomUUID ( ) . slice ( 0 , 6 )
945+ return `opencode/scheduled-${ hex } -${ timestamp } `
946+ }
904947 return `opencode/${ type } ${ issueId } -${ timestamp } `
905948 }
906949
907- async function pushToNewBranch ( summary : string , branch : string , commit : boolean ) {
950+ async function pushToNewBranch ( summary : string , branch : string , commit : boolean , isSchedule : boolean ) {
908951 console . log ( "Pushing to new branch..." )
909952 if ( commit ) {
910953 await $ `git add .`
911- await $ `git commit -m "${ summary }
954+ if ( isSchedule ) {
955+ // No co-author for scheduled events - the schedule is operating as the repo
956+ await $ `git commit -m "${ summary } "`
957+ } else {
958+ await $ `git commit -m "${ summary }
912959
913960Co-authored-by: ${ actor } <${ actor } @users.noreply.github.com>"`
961+ }
914962 }
915963 await $ `git push -u origin ${ branch } `
916964 }
@@ -958,14 +1006,15 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
9581006 }
9591007
9601008 async function assertPermissions ( ) {
1009+ // Only called for non-schedule events, so actor is defined
9611010 console . log ( `Asserting permissions for user ${ actor } ...` )
9621011
9631012 let permission
9641013 try {
9651014 const response = await octoRest . repos . getCollaboratorPermissionLevel ( {
9661015 owner,
9671016 repo,
968- username : actor ,
1017+ username : actor ! ,
9691018 } )
9701019
9711020 permission = response . data . permission
@@ -979,30 +1028,32 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
9791028 }
9801029
9811030 async function addReaction ( commentType : "issue" | "pr_review" ) {
1031+ // Only called for non-schedule events, so triggerCommentId is defined
9821032 console . log ( "Adding reaction..." )
9831033 if ( commentType === "pr_review" ) {
9841034 return await octoRest . rest . reactions . createForPullRequestReviewComment ( {
9851035 owner,
9861036 repo,
987- comment_id : triggerCommentId ,
1037+ comment_id : triggerCommentId ! ,
9881038 content : AGENT_REACTION ,
9891039 } )
9901040 }
9911041 return await octoRest . rest . reactions . createForIssueComment ( {
9921042 owner,
9931043 repo,
994- comment_id : triggerCommentId ,
1044+ comment_id : triggerCommentId ! ,
9951045 content : AGENT_REACTION ,
9961046 } )
9971047 }
9981048
9991049 async function removeReaction ( commentType : "issue" | "pr_review" ) {
1050+ // Only called for non-schedule events, so triggerCommentId is defined
10001051 console . log ( "Removing reaction..." )
10011052 if ( commentType === "pr_review" ) {
10021053 const reactions = await octoRest . rest . reactions . listForPullRequestReviewComment ( {
10031054 owner,
10041055 repo,
1005- comment_id : triggerCommentId ,
1056+ comment_id : triggerCommentId ! ,
10061057 content : AGENT_REACTION ,
10071058 } )
10081059
@@ -1012,7 +1063,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10121063 await octoRest . rest . reactions . deleteForPullRequestComment ( {
10131064 owner,
10141065 repo,
1015- comment_id : triggerCommentId ,
1066+ comment_id : triggerCommentId ! ,
10161067 reaction_id : eyesReaction . id ,
10171068 } )
10181069 return
@@ -1021,7 +1072,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10211072 const reactions = await octoRest . rest . reactions . listForIssueComment ( {
10221073 owner,
10231074 repo,
1024- comment_id : triggerCommentId ,
1075+ comment_id : triggerCommentId ! ,
10251076 content : AGENT_REACTION ,
10261077 } )
10271078
@@ -1031,17 +1082,18 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
10311082 await octoRest . rest . reactions . deleteForIssueComment ( {
10321083 owner,
10331084 repo,
1034- comment_id : triggerCommentId ,
1085+ comment_id : triggerCommentId ! ,
10351086 reaction_id : eyesReaction . id ,
10361087 } )
10371088 }
10381089
10391090 async function createComment ( body : string ) {
1091+ // Only called for non-schedule events, so issueId is defined
10401092 console . log ( "Creating comment..." )
10411093 return await octoRest . rest . issues . createComment ( {
10421094 owner,
10431095 repo,
1044- issue_number : issueId ,
1096+ issue_number : issueId ! ,
10451097 body,
10461098 } )
10471099 }
@@ -1119,10 +1171,11 @@ query($owner: String!, $repo: String!, $number: Int!) {
11191171 }
11201172
11211173 function buildPromptDataForIssue ( issue : GitHubIssue ) {
1174+ // Only called for non-schedule events, so payload is defined
11221175 const comments = ( issue . comments ?. nodes || [ ] )
11231176 . filter ( ( c ) => {
11241177 const id = parseInt ( c . databaseId )
1125- return id !== payload . comment . id
1178+ return id !== payload ! . comment . id
11261179 } )
11271180 . map ( ( c ) => ` - ${ c . author . login } at ${ c . createdAt } : ${ c . body } ` )
11281181
@@ -1246,10 +1299,11 @@ query($owner: String!, $repo: String!, $number: Int!) {
12461299 }
12471300
12481301 function buildPromptDataForPR ( pr : GitHubPullRequest ) {
1302+ // Only called for non-schedule events, so payload is defined
12491303 const comments = ( pr . comments ?. nodes || [ ] )
12501304 . filter ( ( c ) => {
12511305 const id = parseInt ( c . databaseId )
1252- return id !== payload . comment . id
1306+ return id !== payload ! . comment . id
12531307 } )
12541308 . map ( ( c ) => `- ${ c . author . login } at ${ c . createdAt } : ${ c . body } ` )
12551309
0 commit comments