1
+ import { Temporal , toTemporalInstant } from "@js-temporal/polyfill" ;
2
+ Date . prototype . toTemporalInstant = toTemporalInstant ;
3
+
1
4
import * as core from "@actions/core" ;
2
5
import { getOctokit } from "@actions/github" ;
3
6
import { DefaultArtifactClient } from "@actions/artifact" ;
@@ -9,10 +12,12 @@ type WorkflowRunStatus = "in_progress" | "completed";
9
12
type Artifact = components [ "schemas" ] [ "artifact" ] ;
10
13
type GitHubClient = ReturnType < typeof getOctokit > ;
11
14
type WorkflowRun = components [ "schemas" ] [ "workflow-run" ] ;
15
+ type IssueComment = components [ "schemas" ] [ "issue-comment" ] ;
16
+ type PullRequest = components [ "schemas" ] [ "pull-request-simple" ] ;
12
17
type GetLatestArtifactResponse = {
13
18
artifact : Artifact ;
14
19
run : WorkflowRun ;
15
- } | null ;
20
+ } ;
16
21
17
22
async function main ( ) {
18
23
const ghToken = core . getInput ( "github-token" , { required : true } ) ;
@@ -23,6 +28,7 @@ async function main() {
23
28
const prNumber = parseInt ( core . getInput ( "pr-number" , { required : true } ) , 10 ) ;
24
29
const artifactName = core . getInput ( "artifact-name" , { required : true } ) ;
25
30
const considerInProgress = core . getInput ( "consider-inprogress" ) === "true" ;
31
+ const considerComments = core . getInput ( "consider-comments" ) === "true" ;
26
32
let path = core . getInput ( "path" , { required : false } ) ;
27
33
28
34
if ( ! path ) {
@@ -47,7 +53,55 @@ async function main() {
47
53
48
54
// Now let's get all workflows associated with this sha:
49
55
// 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
+ }
51
105
52
106
if ( considerInProgress ) {
53
107
found = await getLatestArtifact (
@@ -59,7 +113,6 @@ async function main() {
59
113
"in_progress" ,
60
114
artifactName ,
61
115
) ;
62
- core . setOutput ( "workflow-run-status" , "in_progress" ) ;
63
116
}
64
117
65
118
if ( ! found ) {
@@ -72,12 +125,22 @@ async function main() {
72
125
"completed" ,
73
126
artifactName ,
74
127
) ;
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 ;
76
136
}
77
137
78
138
if ( ! found ) {
79
139
throw new Error ( `No artifacts found with name ${ artifactName } ` ) ;
80
140
}
141
+
142
+ core . setOutput ( "workflow-run-status" , found . run . status ) ;
143
+
81
144
const { artifact : foundArtifact , run } = found ;
82
145
const artifact = new DefaultArtifactClient ( ) ;
83
146
const response = await artifact . downloadArtifact ( foundArtifact . id , {
@@ -99,10 +162,10 @@ async function getLatestArtifact(
99
162
repoOwner : string ,
100
163
repoName : string ,
101
164
workflowId : string ,
102
- headSha : string ,
165
+ headSha ? : string ,
103
166
status : WorkflowRunStatus ,
104
167
artifactName : string ,
105
- ) : Promise < GetLatestArtifactResponse > {
168
+ ) : Promise < GetLatestArtifactResponse | null > {
106
169
const {
107
170
data : { workflow_runs : workflowRuns } ,
108
171
} : { data : { workflow_runs : WorkflowRun [ ] } } =
@@ -113,25 +176,117 @@ async function getLatestArtifact(
113
176
workflow_id : workflowId ,
114
177
status : status ,
115
178
} ) ;
179
+
116
180
if ( workflowRuns . length === 0 ) {
117
181
console . log ( `No ${ status } runs found` ) ;
118
182
return null ;
119
183
}
120
184
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 ;
123
278
for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) {
124
279
const {
125
280
data : { artifacts } ,
126
281
} : { data : { artifacts : Artifact [ ] } } =
127
282
await client . rest . actions . listWorkflowRunArtifacts ( {
128
- owner : repoOwner ,
129
- repo : repoName ,
283
+ owner,
284
+ repo,
130
285
run_id : run . id ,
131
286
} ) ;
132
287
const artifact = artifacts . find ( ( art ) => art . name == artifactName ) ;
133
288
if ( artifact ) {
134
- console . log ( `Found ${ status } artifact` ) ;
289
+ console . log ( `Found ${ run . status } artifact` ) ;
135
290
return { artifact, run } ;
136
291
}
137
292
if ( attempt < maxAttempts ) {
@@ -142,4 +297,5 @@ async function getLatestArtifact(
142
297
console . log ( "No artifact found" ) ;
143
298
return null ;
144
299
}
300
+
145
301
await main ( ) ;
0 commit comments