Skip to content
This repository was archived by the owner on Jan 19, 2024. It is now read-only.
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 79 additions & 11 deletions functions/src/plugins/rerun-circleci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
}
Comment on lines +118 to +123
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super-nit: This could be simplified as:

Suggested change
for (const status of statuses) {
if (status.context.startsWith('ci/circleci:')) {
targetUrl = status.target_url;
break;
}
}
const targetUrl = statuses.find(x => x.context.startsWith('ci/circleci:'))?.target_url;


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+)\?`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this string is converted to the RegExp you intend it to be converted 😁
What you have now will basically match https://circleci.com/gh/${owner}/${repo}/ followed by zero or more d letters 😁

You were probably going for something like:

Suggested change
const jobIdMatcher = targetUrl.match(`https://circleci.com/gh/${owner}/${repo}/(\d+)\?`);
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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't you mean:

Suggested change
const job = jobIdMatcher[0];
const job = jobIdMatcher[1];

/** 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']);
}
}