diff --git a/dist/index.js b/dist/index.js index cf6190a9..df81edc2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6635,14 +6635,6 @@ async function createCheck(linterName, sha, context, lintResult, neutralCheckOnW ]; } - // Only use the first 50 annotations (limit for a single API request) - if (annotations.length > 50) { - core.info( - `There are more than 50 errors/warnings from ${linterName}. Annotations are created for the first 50 issues only.`, - ); - annotations = annotations.slice(0, 50); - } - let conclusion; if (lintResult.isSuccess) { if (annotations.length > 0 && neutralCheckOnWarning) { @@ -6654,50 +6646,95 @@ async function createCheck(linterName, sha, context, lintResult, neutralCheckOnW conclusion = "failure"; } - const body = { - name: linterName, - head_sha: sha, - conclusion, - output: { - title: capitalizeFirstLetter(summary), - summary: `${linterName} found ${summary}`, - annotations, - }, + const headers = { + "Content-Type": "application/json", + // "Accept" header is required to access Checks API during preview period + Accept: "application/vnd.github.antiope-preview+json", + Authorization: `Bearer ${context.token}`, + "User-Agent": actionName, }; - try { - core.info( - `Creating GitHub check with ${conclusion} conclusion and ${annotations.length} annotations for ${linterName}…`, - ); - await request(`${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/check-runs`, { - method: "POST", - headers: { - "Content-Type": "application/json", - // "Accept" header is required to access Checks API during preview period - Accept: "application/vnd.github.antiope-preview+json", - Authorization: `Bearer ${context.token}`, - "User-Agent": actionName, + + // GitHub only allows 50 annotations per request, chunk them and send multiple requests + const chunkSize = 50; + for (let i = 0; i < annotations.length; i += chunkSize) { + const annotationChunk = annotations.slice(i, i + chunkSize); + + const checkRuns = await request( + `${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/commits/${sha}/check-runs`, + { + method: "GET", + headers, }, - body, - }); - core.info(`${linterName} check created successfully`); - } catch (err) { - let errorMessage = err.message; - if (err.data) { - try { - const errorData = JSON.parse(err.data); - if (errorData.message) { - errorMessage += `. ${errorData.message}`; - } - if (errorData.documentation_url) { - errorMessage += ` ${errorData.documentation_url}`; + ); + const existingRun = checkRuns.data.check_runs.find((run) => run.name === linterName); + + try { + core.info( + `Creating GitHub check with ${conclusion} conclusion and ${annotations.length} annotations for ${linterName}…`, + ); + + if (existingRun == null) { + await request( + `${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/check-runs`, + { + method: "POST", + headers, + body: { + name: linterName, + conclusion, + head_sha: sha, + output: { + title: capitalizeFirstLetter(summary), + summary: `${linterName} found ${summary}`, + annotations: annotationChunk, + }, + }, + }, + ); + + core.info(`${linterName} check created successfully`); + } else { + const existingRunId = existingRun.id; + + await request( + `${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/check-runs/${existingRunId}`, + { + method: "PATCH", + headers, + body: { + name: linterName, + conclusion, + check_run_id: existingRunId, + output: { + title: capitalizeFirstLetter(summary), + summary: `${linterName} found ${summary}`, + annotations: annotationChunk, + }, + }, + }, + ); + + core.info(`${linterName} check updated successfully`); + } + } catch (err) { + let errorMessage = err.message; + if (err.data) { + try { + const errorData = JSON.parse(err.data); + if (errorData.message) { + errorMessage += `. ${errorData.message}`; + } + if (errorData.documentation_url) { + errorMessage += ` ${errorData.documentation_url}`; + } + } catch (e) { + // Ignore } - } catch (e) { - // Ignore } - } - core.error(errorMessage); + core.error(errorMessage); - throw new Error(`Error trying to create GitHub check for ${linterName}: ${errorMessage}`); + throw new Error(`Error trying to create GitHub check for ${linterName}: ${errorMessage}`); + } } } @@ -9381,9 +9418,10 @@ const https = __nccwpck_require__(5687); * Helper function for making HTTP requests * @param {string | URL} url - Request URL * @param {object} options - Request options + * @param {number} retryCount - Current attempted number of retries * @returns {Promise} - JSON response */ -function request(url, options) { +function request(url, options, retryCount = 0) { return new Promise((resolve, reject) => { const req = https .request(url, options, (res) => { @@ -9392,7 +9430,21 @@ function request(url, options) { data += chunk; }); res.on("end", () => { - if (res.statusCode >= 400) { + if (res.statusCode === 429) { + if (retryCount < 3) { + // max retries + const retryAfter = res.headers["retry-after"] + ? parseInt(res.headers["retry-after"], 10) * 1000 + : 5000 * retryCount; // default backoff time if 'retry-after' header is not present + setTimeout(() => { + request(url, options, retryCount + 1) + .then(resolve) + .catch(reject); + }, retryAfter); + } else { + reject(new Error(`Max retries reached. Status code: ${res.statusCode}`)); + } + } else if (res.statusCode >= 400) { const err = new Error(`Received status code ${res.statusCode}`); err.response = res; err.data = data; @@ -9403,6 +9455,7 @@ function request(url, options) { }); }) .on("error", reject); + if (options.body) { req.end(JSON.stringify(options.body)); } else { diff --git a/src/github/api.js b/src/github/api.js index c28b7a36..0a2d02a3 100644 --- a/src/github/api.js +++ b/src/github/api.js @@ -33,14 +33,6 @@ async function createCheck(linterName, sha, context, lintResult, neutralCheckOnW ]; } - // Only use the first 50 annotations (limit for a single API request) - if (annotations.length > 50) { - core.info( - `There are more than 50 errors/warnings from ${linterName}. Annotations are created for the first 50 issues only.`, - ); - annotations = annotations.slice(0, 50); - } - let conclusion; if (lintResult.isSuccess) { if (annotations.length > 0 && neutralCheckOnWarning) { @@ -52,50 +44,95 @@ async function createCheck(linterName, sha, context, lintResult, neutralCheckOnW conclusion = "failure"; } - const body = { - name: linterName, - head_sha: sha, - conclusion, - output: { - title: capitalizeFirstLetter(summary), - summary: `${linterName} found ${summary}`, - annotations, - }, + const headers = { + "Content-Type": "application/json", + // "Accept" header is required to access Checks API during preview period + Accept: "application/vnd.github.antiope-preview+json", + Authorization: `Bearer ${context.token}`, + "User-Agent": actionName, }; - try { - core.info( - `Creating GitHub check with ${conclusion} conclusion and ${annotations.length} annotations for ${linterName}…`, - ); - await request(`${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/check-runs`, { - method: "POST", - headers: { - "Content-Type": "application/json", - // "Accept" header is required to access Checks API during preview period - Accept: "application/vnd.github.antiope-preview+json", - Authorization: `Bearer ${context.token}`, - "User-Agent": actionName, + + // GitHub only allows 50 annotations per request, chunk them and send multiple requests + const chunkSize = 50; + for (let i = 0; i < annotations.length; i += chunkSize) { + const annotationChunk = annotations.slice(i, i + chunkSize); + + const checkRuns = await request( + `${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/commits/${sha}/check-runs`, + { + method: "GET", + headers, }, - body, - }); - core.info(`${linterName} check created successfully`); - } catch (err) { - let errorMessage = err.message; - if (err.data) { - try { - const errorData = JSON.parse(err.data); - if (errorData.message) { - errorMessage += `. ${errorData.message}`; - } - if (errorData.documentation_url) { - errorMessage += ` ${errorData.documentation_url}`; + ); + const existingRun = checkRuns.data.check_runs.find((run) => run.name === linterName); + + try { + core.info( + `Creating GitHub check with ${conclusion} conclusion and ${annotations.length} annotations for ${linterName}…`, + ); + + if (existingRun == null) { + await request( + `${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/check-runs`, + { + method: "POST", + headers, + body: { + name: linterName, + conclusion, + head_sha: sha, + output: { + title: capitalizeFirstLetter(summary), + summary: `${linterName} found ${summary}`, + annotations: annotationChunk, + }, + }, + }, + ); + + core.info(`${linterName} check created successfully`); + } else { + const existingRunId = existingRun.id; + + await request( + `${process.env.GITHUB_API_URL}/repos/${context.repository.repoName}/check-runs/${existingRunId}`, + { + method: "PATCH", + headers, + body: { + name: linterName, + conclusion, + check_run_id: existingRunId, + output: { + title: capitalizeFirstLetter(summary), + summary: `${linterName} found ${summary}`, + annotations: annotationChunk, + }, + }, + }, + ); + + core.info(`${linterName} check updated successfully`); + } + } catch (err) { + let errorMessage = err.message; + if (err.data) { + try { + const errorData = JSON.parse(err.data); + if (errorData.message) { + errorMessage += `. ${errorData.message}`; + } + if (errorData.documentation_url) { + errorMessage += ` ${errorData.documentation_url}`; + } + } catch (e) { + // Ignore } - } catch (e) { - // Ignore } - } - core.error(errorMessage); + core.error(errorMessage); - throw new Error(`Error trying to create GitHub check for ${linterName}: ${errorMessage}`); + throw new Error(`Error trying to create GitHub check for ${linterName}: ${errorMessage}`); + } } } diff --git a/src/utils/request.js b/src/utils/request.js index cb42007a..acd035a4 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -4,9 +4,10 @@ const https = require("https"); * Helper function for making HTTP requests * @param {string | URL} url - Request URL * @param {object} options - Request options + * @param {number} retryCount - Current attempted number of retries * @returns {Promise} - JSON response */ -function request(url, options) { +function request(url, options, retryCount = 0) { return new Promise((resolve, reject) => { const req = https .request(url, options, (res) => { @@ -15,7 +16,21 @@ function request(url, options) { data += chunk; }); res.on("end", () => { - if (res.statusCode >= 400) { + if (res.statusCode === 429) { + if (retryCount < 3) { + // max retries + const retryAfter = res.headers["retry-after"] + ? parseInt(res.headers["retry-after"], 10) * 1000 + : 5000 * retryCount; // default backoff time if 'retry-after' header is not present + setTimeout(() => { + request(url, options, retryCount + 1) + .then(resolve) + .catch(reject); + }, retryAfter); + } else { + reject(new Error(`Max retries reached. Status code: ${res.statusCode}`)); + } + } else if (res.statusCode >= 400) { const err = new Error(`Received status code ${res.statusCode}`); err.response = res; err.data = data; @@ -26,6 +41,7 @@ function request(url, options) { }); }) .on("error", reject); + if (options.body) { req.end(JSON.stringify(options.body)); } else {