Skip to content
Open
Show file tree
Hide file tree
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
28 changes: 14 additions & 14 deletions lib/machine/configurers/prCloser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { GoalConfigurer } from "@atomist/sdm-core";
import { MyGoals } from "../goals";
import { GetPrsForBranch } from "../../typings/types";
import { execPromise, slackInfoMessage } from "@atomist/sdm";
import { logger, TokenCredentials } from "@atomist/automation-client";
import { logger } from "@atomist/automation-client";
import * as _ from "lodash";
import * as GithubApi from "@octokit/rest";
import {
BitBucketPrData,
createBitbucketPrComment,
declineBitBucketPr,
deleteBitbucketBranch,
} from "../../support/bitbucket/utils";

export const PrCloserConfigurator: GoalConfigurer<MyGoals> = async (sdm, goals) => {
goals.pushImpact.withListener(
Expand Down Expand Up @@ -101,22 +106,17 @@ export const PrCloserConfigurator: GoalConfigurer<MyGoals> = async (sdm, goals)
*/
logger.debug(`Found the following PR numbers to close ${JSON.stringify(closeThesePrs)}`);
for (const closePr of closeThesePrs) {
const gh = new GithubApi({
auth: `token ${(i.credentials as TokenCredentials).token}`,
});
const data = {
const data: BitBucketPrData = {
owner: i.push.repo.owner,
repo: i.push.repo.name,
number: closePr.number,
branch: closePr.branch,
};

await gh.issues.createComment({
...data,
issue_number: closePr.number,
body: `Atomist closed this PR because it no longer contains any valid changes.`,
});

await gh.pulls.update({ ...data, pull_number: closePr.number, state: "closed"});
await gh.git.deleteRef({ ...data, ref: `heads/${closePr.branch}` });
await createBitbucketPrComment(data,
{text: `Atomist closed this PR because it no longer contains any valid changes.`});
await declineBitBucketPr(data);
await deleteBitbucketBranch(data);

await i.addressChannels(slackInfoMessage(
`Closed PR#${closePr.number} and deleted branch ${closePr.branch}`,
Expand Down
133 changes: 133 additions & 0 deletions lib/support/bitbucket/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
configurationValue,
DefaultHttpClientFactory,
HttpClientFactory,
HttpClientOptions,
HttpMethod,
HttpResponse,
logger,
} from "@atomist/automation-client";
import { bitBucketCredentials } from "./auth";
import {
BasicAuthCredentials,
} from "@atomist/automation-client/lib/operations/common/BasicAuthCredentials";
import * as _ from "lodash";

/**
* Stores the required information for mutating PRs
*/
export interface BitBucketPrData {
owner: string;
repo: string;
branch: string;
number: number;
}

/**
* Stores the required information to mutate Branches
*/
export interface BitBucketBranchData {
owner: string;
repo: string;
branch: string;
}

/**
* Simple utility functino to return an authorization header
* @param {BasicAuthCredentials} creds
*/
export function usernameColonPassword(creds: BasicAuthCredentials): { Authorization: string } {
return {
Authorization: `Basic ${Buffer.from(creds.username + ":" + creds.password).toString("base64")}`,
};
}

/**
* Generic function for sending API requests to the Bitbucket instance
* - Supply the URI beyond the base URL (from sdm.git.url)
* - Authentication (basic) is handled automatically
* - Optionally supply a response type
*
* @param uri
* @param method
* @param body
* @param headers Optional
*/
export async function sendBitbucketApiRequest<R = any>(
uri: string,
method: HttpMethod,
body?: any,
headers?: HttpClientOptions["headers"],
): Promise<HttpResponse<R>> {
const baseUrl = configurationValue<string>("sdm.git.url");
const targetUrl = `${baseUrl}${uri.startsWith("/") ? "" : "/"}${uri}`;

const initHeaders = headers ? headers : {
"Accept": "application/json",
"Content-Type": "application/json",
};
const authHeaders = usernameColonPassword(bitBucketCredentials());
const finalHeaders = _.merge(initHeaders, {...authHeaders});

logger.debug(`sendBitbucketApiRequest: Sending request to ${targetUrl}`);
try {
const result = await configurationValue<HttpClientFactory>("http.client.factory", DefaultHttpClientFactory)
.create(targetUrl)
.exchange<R>(targetUrl, {
method,
body,
headers: finalHeaders,
});

logger.debug(`sendBitbucketApiRequest: Successfully sent request to ${targetUrl}`);
return result;
} catch (e) {
logger.error(`sendBitbucketApiRequest: Failed to send request to ${targetUrl}`);
throw e;
}
}

/**
* This function can be used to add comments to an existing PR. See details in the API docs located here
* https://docs.atlassian.com/bitbucket-server/rest/5.5.1/bitbucket-rest.html#idm139496951085440 in the /comments
* section for valid comment data structure.
*
* @param {BitBucketPrData} data
* @param {Object} comment
*/
export const createBitbucketPrComment = async (data: BitBucketPrData, comment: any): Promise<void> => {
await sendBitbucketApiRequest(
`/rest/api/1.0/projects/${data.owner}/repos/${data.repo}/pull-requests/${data.number}/comments`,
HttpMethod.Post,
comment,
);
};

/**
* This function can be used to decline an existing PR
* @param {BitBucketPrData} data
*/
export const declineBitBucketPr = async (data: BitBucketPrData): Promise<void> => {
const version = await sendBitbucketApiRequest<{version: number}>(
`/rest/api/1.0/projects/${data.owner}/repos/${data.repo}/pull-requests/${data.number}`,
HttpMethod.Get,
);

await sendBitbucketApiRequest(
`/rest/api/1.0/projects/${data.owner}/repos/${data.repo}/pull-requests/${data.number}/decline`,
HttpMethod.Post,
{version: version.body.version},
);
};

/**
* This function can be used to delete a branch
* @param {BitBucketBranchData} data
*/
export const deleteBitbucketBranch = async (data: BitBucketBranchData): Promise<void> => {
await sendBitbucketApiRequest(
`/rest/branch-utils/1.0/projects/${data.owner}/repos/${data.repo}/branches`,
HttpMethod.Delete,
{name: `refs/heads/${data.branch}`, dryRun: false},
);
};