11import { extractInputs } from "./context.js" ;
22import { PER_PAGE_MAX } from "./github.js" ;
33
4+ const SUPPORTED_EVENTS = [ "workflow_run" , "check_run" , "check_suite" ] ;
5+
46/**
57 * @typedef {import('@octokit/plugin-rest-endpoint-methods').RestEndpointMethodTypes } RestEndpointMethodTypes
6- * @typedef {RestEndpointMethodTypes["checks"]["listForRef"]["response"]["data"]["check_runs"][number] } CheckRun
7- * @typedef {RestEndpointMethodTypes["actions"]["listWorkflowRunsForRepo"]["response"]["data"]["workflow_runs"][number] } WorkflowRun
8+ * @typedef {RestEndpointMethodTypes["checks"]["listForRef"]["response"]["data"]["check_runs"] } CheckRuns
9+ * @typedef {RestEndpointMethodTypes["actions"]["listWorkflowRunsForRepo"]["response"]["data"]["workflow_runs"] } WorkflowRuns
810 */
911
12+ /* v8 ignore start */
1013/**
1114 * Given the name of a completed check run name and a completed workflow, verify
1215 * that both have the same conclusion. If conclusions are different, fail the
@@ -24,41 +27,77 @@ export async function verifyRunStatus({ github, context, core }) {
2427 throw new Error ( "WORKFLOW_NAME is not set" ) ;
2528 }
2629
27- // Exit early when context is a check_run event and the check run does not
28- // match the checkRunName.
30+ if ( ! SUPPORTED_EVENTS . some ( ( e ) => e === context . eventName ) ) {
31+ throw new Error (
32+ `Unsupported event: ${ context . eventName } . Supported events: ${ SUPPORTED_EVENTS . join ( ", " ) } ` ,
33+ ) ;
34+ }
35+
36+ if ( context . eventName === "check_suite" && context . payload . check_suite . status !== "completed" ) {
37+ core . setFailed ( `Check suite ${ context . payload . check_suite . app . name } is not completed. Cannot evaluate incomplete check suite.` ) ;
38+ return ;
39+ }
40+
41+ return await verifyRunStatusImpl ( { github, context, core , checkRunName, workflowName} ) ;
42+ }
43+ /* v8 ignore stop */
44+
45+ /**
46+ * @param {Object } params
47+ * @param {import('github-script').AsyncFunctionArguments["github"] } params.github
48+ * @param {import('github-script').AsyncFunctionArguments["context"] } params.context
49+ * @param {import('github-script').AsyncFunctionArguments["core"] } params.core
50+ * @param {string } params.checkRunName
51+ * @param {string } params.workflowName
52+ */
53+ export async function verifyRunStatusImpl ( { github, context, core, checkRunName, workflowName} ) {
2954 if ( context . eventName == "check_run" ) {
3055 const contextRunName = context . payload . check_run . name ;
3156 if ( contextRunName !== checkRunName ) {
32- core . notice ( `Check run name (${ contextRunName } ) does not match input: ${ checkRunName } ` ) ;
57+ core . setFailed ( `Check run name (${ contextRunName } ) does not match input: ${ checkRunName } . Ensure job is filtering by github.event.check_run.name. ` ) ;
3358 return ;
3459 }
3560 }
3661
3762 const { head_sha } = await extractInputs ( github , context , core ) ;
3863
39- const checkRun =
40- context . eventName == "check_run"
41- ? context . payload . check_run
42- : await getCheckRun ( github , context , core , checkRunName , head_sha ) ;
64+ let checkRun ;
65+ if ( context . eventName == "check_run" ) {
66+ checkRun = context . payload . check_run ;
67+ } else {
68+ const checkRuns = await getCheckRuns ( github , context , checkRunName , head_sha ) ;
69+ if ( checkRuns . length === 0 ) {
70+ if ( context . eventName === "check_suite" ) {
71+ const message = `Could not locate check run ${ checkRunName } in check suite ${ context . payload . check_suite . app . name } . Ensure job is filtering by github.event.check_suite.app.name.` ;
72+ core . setFailed ( message ) ;
73+ return ;
74+ }
75+
76+ core . notice ( `No completed check run with name: ${ checkRunName } . Not enough information to judge success or failure. Ending with success status.` ) ;
77+ return ;
78+ }
4379
44- if ( ! checkRun ) {
45- core . notice ( `No completed check run with name: ${ checkRunName } ` ) ;
46- return ;
80+ // Use the most recent check run
81+ checkRun = checkRuns [ 0 ] ;
4782 }
4883
4984 core . info (
5085 `Check run name: ${ checkRun . name } , conclusion: ${ checkRun . conclusion } , URL: ${ checkRun . html_url } ` ,
5186 ) ;
5287 core . debug ( `Check run: ${ JSON . stringify ( checkRun ) } ` ) ;
5388
54- const workflowRun =
55- context . eventName == "workflow_run"
56- ? context . payload . workflow_run
57- : await getWorkflowRun ( github , context , core , workflowName , head_sha ) ;
89+ let workflowRun ;
90+ if ( context . eventName == "workflow_run" ) {
91+ workflowRun = context . payload . workflow_run ;
92+ } else {
93+ const workflowRuns = await getWorkflowRuns ( github , context , workflowName , head_sha ) ;
94+ if ( workflowRuns . length === 0 ) {
95+ core . notice ( `No completed workflow run with name: ${ workflowName } . Not enough information to judge success or failure. Ending with success status.` ) ;
96+ return ;
97+ }
5898
59- if ( ! workflowRun ) {
60- core . notice ( `No completed workflow run with name: ${ workflowName } ` ) ;
61- return ;
99+ // Use the most recent workflow run
100+ workflowRun = workflowRuns [ 0 ] ;
62101 }
63102
64103 core . info (
@@ -80,61 +119,44 @@ export async function verifyRunStatus({ github, context, core }) {
80119 * Returns the check with the given checkRunName for the given ref.
81120 * @param {import('github-script').AsyncFunctionArguments['github'] } github
82121 * @param {import('github-script').AsyncFunctionArguments['context'] } context
83- * @param {import('github-script').AsyncFunctionArguments['core'] } core
84122 * @param {string } checkRunName
85123 * @param {string } ref
86- * @returns {Promise<CheckRun | null > }
124+ * @returns {Promise<CheckRuns > }
87125 */
88- export async function getCheckRun (
126+ export async function getCheckRuns (
89127 github ,
90128 context ,
91- core ,
92129 checkRunName ,
93130 ref ,
94131) {
95- const checkRuns = await github . paginate ( github . rest . checks . listForRef , {
132+ const result = await github . paginate ( github . rest . checks . listForRef , {
96133 ...context . repo ,
97134 ref : ref ,
98135 check_name : checkRunName ,
99136 status : "completed" ,
100137 per_page : PER_PAGE_MAX ,
101138 } ) ;
102139
103- if ( checkRuns . length === 0 ) {
104- return null ;
105- }
106-
107- if ( checkRuns . length > 1 ) {
108- core . info ( `Multiple check runs:` ) ;
109- checkRuns . forEach ( ( cr ) => {
110- core . info ( `- ${ cr . name } : ${ cr . conclusion } ` ) ;
111- } ) ;
112-
113- const message = `Multiple completed check runs with name: ${ checkRunName } ` ;
114- core . setFailed ( message ) ;
115- throw new Error ( message ) ;
116- }
117-
118- return checkRuns [ 0 ] ;
140+ // a and b will never be null because status is "completed"
141+ /* v8 ignore next */
142+ return result . sort ( ( a , b ) => compareDatesDescending ( a . completed_at || '' , b . completed_at || '' ) ) ;
119143}
120144
121145/**
122146 * Returns the workflow run with the given workflowName for the given ref.
123147 * @param {import('github-script').AsyncFunctionArguments['github'] } github
124148 * @param {import('github-script').AsyncFunctionArguments['context'] } context
125- * @param {import('github-script').AsyncFunctionArguments['core'] } core
126149 * @param {string } workflowName
127150 * @param {string } ref
128- * @returns {Promise<WorkflowRun | null > }
151+ * @returns {Promise<WorkflowRuns > }
129152 */
130- export async function getWorkflowRun (
153+ export async function getWorkflowRuns (
131154 github ,
132155 context ,
133- core ,
134156 workflowName ,
135157 ref ,
136158) {
137- const workflowRuns = await github . paginate (
159+ const result = await github . paginate (
138160 github . rest . actions . listWorkflowRunsForRepo ,
139161 {
140162 ...context . repo ,
@@ -144,31 +166,15 @@ export async function getWorkflowRun(
144166 } ,
145167 ) ;
146168
147- if ( workflowRuns . length === 0 ) {
148- core . info ( `No completed workflow runs` ) ;
149- return null ;
150- }
151-
152- const matches = workflowRuns . filter ( ( run ) => run . name === workflowName ) ;
153-
154- if ( matches . length === 0 ) {
155- return null ;
156- }
157-
158- if ( matches . length > 1 ) {
159- core . warning (
160- `Multiple matching workflow runs, selecting the most recent run` ,
161- ) ;
162- matches . forEach ( ( wf ) =>
163- core . info ( `- ${ wf . name } : ${ wf . conclusion } ${ wf . html_url } ` ) ,
164- ) ;
165-
166- // Sort by "updated_at" descending, so most recent run is at index 0
167- matches . sort (
168- ( a , b ) =>
169- new Date ( b . updated_at ) . getTime ( ) - new Date ( a . updated_at ) . getTime ( ) ,
170- ) ;
171- }
172-
173- return matches [ 0 ] ;
169+ return result . filter ( ( run ) => run . name === workflowName ) . sort ( ( a , b ) => compareDatesDescending ( a . updated_at , b . updated_at ) ) ;
174170}
171+
172+ /**
173+ * Compares two date strings in descending order.
174+ * @param {string } a date string of the form "YYYY-MM-DDTHH:mm:ssZ"
175+ * @param {string } b date string of the form "YYYY-MM-DDTHH:mm:ssZ"
176+ * @returns
177+ */
178+ export function compareDatesDescending ( a , b ) {
179+ return new Date ( b ) . getTime ( ) - new Date ( a ) . getTime ( ) ;
180+ }
0 commit comments