From 377c5d30b03fec06e6ce51c1bd7427412937b0f4 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Mon, 7 Dec 2020 10:27:13 -0800 Subject: [PATCH] fix(rerun-ci): Fixes the rerun-ci tooling to request a rerun from CircleCI Previously, rerunning CI with the tooling would simply request a new run of the workflow on the branch. Instead, we should request a rerun via the API to allow only running the failing jobs rather than rerunning the successful jobs. --- functions/src/plugins/rerun-circleci.ts | 90 ++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/functions/src/plugins/rerun-circleci.ts b/functions/src/plugins/rerun-circleci.ts index a9ec332..38da121 100644 --- a/functions/src/plugins/rerun-circleci.ts +++ b/functions/src/plugins/rerun-circleci.ts @@ -50,25 +50,24 @@ export class RerunCircleCITask extends Task { const pullRequest: Github.PullRequestsGetResponse = context.payload.pull_request; const sender: Github.PullRequestsGetResponseUser = context.payload.sender; const {owner, repo} = context.repo(); - const circleCiUrl = `https://circleci.com/api/v2/project/gh/${owner}/${repo}/pipeline?circle-token=${CIRCLE_CI_TOKEN}`; + const id = this.getCircleCiWorkflowIdForPullRequest(context); + const url = `https://circleci.com/api/v2/workflow/${id}/rerun?circle-token=${CIRCLE_CI_TOKEN}`; try { - const response = await fetch(circleCiUrl, { + const response = await (await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - branch: `pull/${pullRequest.number}/head`, + // Always rerun only the steps which failed. + from_failed: true }) - }); - // Properly handled failures in the CircleCI requests are returned with an HTTP response code - // of 200 and json response with a `:message` key mapping to the failure message. If - // `:message` is not defined, the API request was successful. - const errMessage = (await response.json())[':message']; - if (errMessage) { - throw Error(errMessage); - } + })).json(); + + assertNoErrorsInCircleCiResponse(response); + } catch (err) { + this.logError(err); const error: TypeError = err; context.github.issues.createComment({ body: `@${sender.login} the CircleCI rerun you requested failed. See details below: @@ -98,4 +97,73 @@ ${error.message} const repositoryConfig = await this.getAppConfig(context); return repositoryConfig.rerunCircleCI; } + + /** + * Get the CircleCI workflow id of the first discovered CircleCI status in the statuses. Since + * only one workflow is run on each PR, all CircleCI statuses will track back to the same + * workflow id. + */ + private async getCircleCiWorkflowIdForPullRequest(context: Context) { + /** The target url of the discovered CircleCI status. */ + let targetUrl: string; + /** The pull request which triggered the bot action. */ + const pullRequest: Github.PullRequestsGetResponse = context.payload.pull_request; + /** The owner and repository name. */ + const {owner, repo} = context.repo(); + /** The list of statuses for the latest ref of the PR. */ + const {statuses} = (await context.github.repos.getCombinedStatusForRef({ + owner, repo, ref: pullRequest.head.ref + })).data; + + for (const status of statuses) { + if (status.context.startsWith('ci/circleci:')) { + targetUrl = status.target_url; + break; + } + } + + if (targetUrl === undefined) { + throw Error('No status for a CircleCI workflow was found on the pull request to be rerun.'); + } + + /** + * The matcher results of the regex to select the job ID of the job which the status represents. + */ + const jobIdMatcher = targetUrl.match(`https://circleci.com/gh/${owner}/${repo}/(\d+)\?`); + + if (jobIdMatcher === null) { + throw Error('Unable to determine the job ID for the CircleCI job creating the status'); + } + + /** The job ID. */ + const job = jobIdMatcher[0]; + /** The full url of the API request to CircleCI. */ + const url = `https://circleci.com/api/v2/project/gh/${owner}/${repo}/job/${job}?circle-token=${CIRCLE_CI_TOKEN}`; + /** The API response from the CircleCI request. */ + const response = (await (await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + })).json()); + + assertNoErrorsInCircleCiResponse(response); + + return response.latest_workflow.id; + } +} + + + +/** + * Checks the provided response from CircleCI's API to determine if it is an error message. + * + * Properly handled failures in the CircleCI requests are returned with an HTTP response code of 200 + * and json response with a `:message` key mapping to the failure message. If `:message` is not + * defined, the API request was successful. + */ +function assertNoErrorsInCircleCiResponse(response: any) { + if (response[':message'] !== undefined) { + throw Error(response[':message']); + } }