From 7903503c3a5d258690e8d8656ca5e1afcba88d16 Mon Sep 17 00:00:00 2001 From: ipcrm Date: Wed, 31 Jul 2019 21:03:55 -0400 Subject: [PATCH 1/2] Adding PR Closer customized for BitBucket --- lib/machine/configurers/prCloser.ts | 28 +++--- lib/support/bitbucket/utils.ts | 133 ++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 lib/support/bitbucket/utils.ts diff --git a/lib/machine/configurers/prCloser.ts b/lib/machine/configurers/prCloser.ts index b7f2dcf..9cfa546 100644 --- a/lib/machine/configurers/prCloser.ts +++ b/lib/machine/configurers/prCloser.ts @@ -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 = async (sdm, goals) => { goals.pushImpact.withListener( @@ -101,22 +106,17 @@ export const PrCloserConfigurator: GoalConfigurer = 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}`, diff --git a/lib/support/bitbucket/utils.ts b/lib/support/bitbucket/utils.ts new file mode 100644 index 0000000..bb821a3 --- /dev/null +++ b/lib/support/bitbucket/utils.ts @@ -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( + uri: string, + method: HttpMethod, + body?: any, + headers?: HttpClientOptions["headers"], +): Promise> { + const baseUrl = configurationValue("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("http.client.factory", DefaultHttpClientFactory) + .create(targetUrl) + .exchange(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. + * + * @param {BitBucketPrData} data + * @param {string} comment + */ +export const createBitbucketPrComment = async (data: BitBucketPrData, comment: any): Promise => { + 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 => { + 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 => { + await sendBitbucketApiRequest( + `/rest/branch-utils/1.0/projects/${data.owner}/repos/${data.repo}/branches`, + HttpMethod.Delete, + {name: `refs/heads/${data.branch}`, dryRun: false}, + ); +}; From 20df08f28f0949781a10c85bff23f433f09b7ff8 Mon Sep 17 00:00:00 2001 From: ipcrm Date: Wed, 31 Jul 2019 21:26:48 -0400 Subject: [PATCH 2/2] Fix doc mistake --- lib/support/bitbucket/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/support/bitbucket/utils.ts b/lib/support/bitbucket/utils.ts index bb821a3..22e48ca 100644 --- a/lib/support/bitbucket/utils.ts +++ b/lib/support/bitbucket/utils.ts @@ -90,10 +90,10 @@ export async function sendBitbucketApiRequest( /** * 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. + * section for valid comment data structure. * * @param {BitBucketPrData} data - * @param {string} comment + * @param {Object} comment */ export const createBitbucketPrComment = async (data: BitBucketPrData, comment: any): Promise => { await sendBitbucketApiRequest(