1+ import { Temporal , toTemporalInstant } from "@js-temporal/polyfill" ;
2+ Date . prototype . toTemporalInstant = toTemporalInstant ;
3+
14import * as core from "@actions/core" ;
25import { getOctokit } from "@actions/github" ;
36import { DefaultArtifactClient } from "@actions/artifact" ;
@@ -9,10 +12,12 @@ type WorkflowRunStatus = "in_progress" | "completed";
912type Artifact = components [ "schemas" ] [ "artifact" ] ;
1013type GitHubClient = ReturnType < typeof getOctokit > ;
1114type WorkflowRun = components [ "schemas" ] [ "workflow-run" ] ;
15+ type IssueComment = components [ "schemas" ] [ "issue-comment" ] ;
16+ type PullRequest = components [ "schemas" ] [ "pull-request-simple" ] ;
1217type GetLatestArtifactResponse = {
1318 artifact : Artifact ;
1419 run : WorkflowRun ;
15- } | null ;
20+ } ;
1621
1722async function main ( ) {
1823 const ghToken = core . getInput ( "github-token" , { required : true } ) ;
@@ -23,6 +28,7 @@ async function main() {
2328 const prNumber = parseInt ( core . getInput ( "pr-number" , { required : true } ) , 10 ) ;
2429 const artifactName = core . getInput ( "artifact-name" , { required : true } ) ;
2530 const considerInProgress = core . getInput ( "consider-inprogress" ) === "true" ;
31+ const considerComments = core . getInput ( "consider-comments" ) === "true" ;
2632 let path = core . getInput ( "path" , { required : false } ) ;
2733
2834 if ( ! path ) {
@@ -47,7 +53,55 @@ async function main() {
4753
4854 // Now let's get all workflows associated with this sha:
4955 // We start with in-progress ones if those are to be considered.
50- let found : GetLatestArtifactResponse = null ;
56+ let found : GetLatestArtifactResponse | null = null ;
57+ let foundForComment : GetLatestArtifactResponse | null = null ;
58+
59+ if ( considerComments ) {
60+ // If we need to consider workflow runs triggered by comments, then we
61+ // cannot filter by head_sha but need to really go through all the runs of
62+ // a particular workflow and hope it's still available through paging.
63+ // Otherwise, we can only fallback and potentially get an outdated
64+ // artifact:
65+ //
66+ // First we need to get a list of all the comments inside a PR but only
67+ // those that happened before the PR was merged:
68+ const mergedAt = data . merged_at ;
69+ const comment = await getRelevantComment (
70+ client ,
71+ repoOwner ,
72+ repoName ,
73+ prNumber ,
74+ mergedAt ,
75+ ) ;
76+ if ( comment ) {
77+ // Now that we have a comment, we need to find workflows triggered by it.
78+ // The problem is, that there is no clear association between a workflow
79+ // run and a comment triggering it available through the API. For this we
80+ // need to look at workflows runs with the title of the PR (ideally at the
81+ // time of the comment) and filter for all workflow runs that were trigger
82+ // between [comment_time, comment_time+delta]. This is based on the
83+ // assumption, that the title of a PR is stable around the time the fetch
84+ // is made and the comment is created
85+ const workflowRun = await getWorkflowRunForComment (
86+ client ,
87+ repoOwner ,
88+ repoName ,
89+ workflowId ,
90+ data ,
91+ comment ,
92+ considerInProgress ,
93+ ) ;
94+ if ( workflowRun ) {
95+ foundForComment = await getWorkflowRunArtifact (
96+ client ,
97+ repoOwner ,
98+ repoName ,
99+ workflowRun ,
100+ artifactName ,
101+ ) ;
102+ }
103+ }
104+ }
51105
52106 if ( considerInProgress ) {
53107 found = await getLatestArtifact (
@@ -59,7 +113,6 @@ async function main() {
59113 "in_progress" ,
60114 artifactName ,
61115 ) ;
62- core . setOutput ( "workflow-run-status" , "in_progress" ) ;
63116 }
64117
65118 if ( ! found ) {
@@ -72,12 +125,22 @@ async function main() {
72125 "completed" ,
73126 artifactName ,
74127 ) ;
75- core . setOutput ( "workflow-run-status" , "completed" ) ;
128+ }
129+
130+ if (
131+ found &&
132+ foundForComment &&
133+ found . run . created_at < foundForComment . run . created_at
134+ ) {
135+ found = foundForComment ;
76136 }
77137
78138 if ( ! found ) {
79139 throw new Error ( `No artifacts found with name ${ artifactName } ` ) ;
80140 }
141+
142+ core . setOutput ( "workflow-run-status" , found . run . status ) ;
143+
81144 const { artifact : foundArtifact , run } = found ;
82145 const artifact = new DefaultArtifactClient ( ) ;
83146 const response = await artifact . downloadArtifact ( foundArtifact . id , {
@@ -99,10 +162,10 @@ async function getLatestArtifact(
99162 repoOwner : string ,
100163 repoName : string ,
101164 workflowId : string ,
102- headSha : string ,
165+ headSha ? : string ,
103166 status : WorkflowRunStatus ,
104167 artifactName : string ,
105- ) : Promise < GetLatestArtifactResponse > {
168+ ) : Promise < GetLatestArtifactResponse | null > {
106169 const {
107170 data : { workflow_runs : workflowRuns } ,
108171 } : { data : { workflow_runs : WorkflowRun [ ] } } =
@@ -113,25 +176,117 @@ async function getLatestArtifact(
113176 workflow_id : workflowId ,
114177 status : status ,
115178 } ) ;
179+
116180 if ( workflowRuns . length === 0 ) {
117181 console . log ( `No ${ status } runs found` ) ;
118182 return null ;
119183 }
120184 const run = workflowRuns [ 0 ] ;
121- // For in-progress workflows the artifact might not be available yet. For this scenario we do a bit of retrying:
122- const maxAttempts = status === "in_progress" ? 5 : 1 ;
185+ return getWorkflowRunArtifact ( client , repoOwner , repoName , run , artifactName ) ;
186+ }
187+
188+ async function getRelevantComment (
189+ client : GitHubClient ,
190+ owner : string ,
191+ repo : string ,
192+ prNumber : number ,
193+ mergedAt : string | null ,
194+ ) : Promise < IssueComment | null > {
195+ const comments : IssueComment [ ] = await client . paginate (
196+ client . rest . issues . listComments ,
197+ {
198+ owner,
199+ repo,
200+ issue_number : prNumber ,
201+ per_page : 100 ,
202+ } ,
203+ ) ;
204+ comments . reverse ( ) ;
205+ // Now drop all that are made *after* the PR was merged
206+ for ( const comment of comments ) {
207+ if ( mergedAt && comment . created_at >= mergedAt ) {
208+ continue ;
209+ }
210+ return comment ;
211+ }
212+ return null ;
213+ }
214+
215+ async function getWorkflowRunForComment (
216+ client : GitHubClient ,
217+ owner : string ,
218+ repo : string ,
219+ workflowId : string ,
220+ pr : PullRequest ,
221+ comment : IssueComment ,
222+ considerInProgress : boolean ,
223+ ) : Promise < WorkflowRun | null > {
224+ console . log (
225+ "Searching for workflows connected to comments. This can take a while…" ,
226+ ) ;
227+ const commentCreatedAt = Temporal . Instant . from ( comment . created_at ) ;
228+ const runsIterator = client . paginate . iterator (
229+ client . rest . actions . listWorkflowRuns ,
230+ {
231+ repo,
232+ owner,
233+ workflow_id : workflowId ,
234+ per_page : 100 ,
235+ created : `${ commentCreatedAt . toString ( ) } ..${ commentCreatedAt . add ( { minutes : 1 } ) . toString ( ) } ` ,
236+ } ,
237+ ) ;
238+
239+ let abort = false ;
240+ let page = 0 ;
241+
242+ const allowedStatus = [ "completed" , "success" ] ;
243+ if ( considerInProgress ) {
244+ allowedStatus . push ( "in_progress" ) ;
245+ }
246+
247+ for await ( const runs of runsIterator ) {
248+ for ( const run of runs . data ) {
249+ if ( run . event !== "issue_comment" ) {
250+ continue ;
251+ }
252+ if ( ! allowedStatus . includes ( run . status ) ) {
253+ continue ;
254+ }
255+ if ( run . display_title !== pr . title ) {
256+ continue ;
257+ }
258+ return run ;
259+ }
260+ if ( ++ page > 100 ) {
261+ console . log ( "Aborting search after 100 pages" ) ;
262+ abort = true ;
263+ }
264+ if ( abort ) {
265+ break ;
266+ }
267+ }
268+ }
269+
270+ async function getWorkflowRunArtifact (
271+ client : GitHubClient ,
272+ owner : string ,
273+ repo : string ,
274+ run : WorkflowRun ,
275+ artifactName : string ,
276+ ) : Promise < GetLatestArtifactResponse | null > {
277+ const maxAttempts = run . status === "in_progress" ? 5 : 1 ;
123278 for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) {
124279 const {
125280 data : { artifacts } ,
126281 } : { data : { artifacts : Artifact [ ] } } =
127282 await client . rest . actions . listWorkflowRunArtifacts ( {
128- owner : repoOwner ,
129- repo : repoName ,
283+ owner,
284+ repo,
130285 run_id : run . id ,
131286 } ) ;
132287 const artifact = artifacts . find ( ( art ) => art . name == artifactName ) ;
133288 if ( artifact ) {
134- console . log ( `Found ${ status } artifact` ) ;
289+ console . log ( `Found ${ run . status } artifact` ) ;
135290 return { artifact, run } ;
136291 }
137292 if ( attempt < maxAttempts ) {
@@ -142,4 +297,5 @@ async function getLatestArtifact(
142297 console . log ( "No artifact found" ) ;
143298 return null ;
144299}
300+
145301await main ( ) ;
0 commit comments