sfdx-hardis
Version:
Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards
261 lines (259 loc) • 13.1 kB
JavaScript
import { GitProviderRoot } from './gitProviderRoot.js';
import c from 'chalk';
import fs from "fs-extra";
import FormData from 'form-data';
import * as path from "path";
import { git, uxLog } from '../utils/index.js';
import bbPkg from 'bitbucket';
import { CONSTANTS } from '../../config/index.js';
const { Bitbucket } = bbPkg;
export class BitbucketProvider extends GitProviderRoot {
bitbucket;
serverUrl = 'https://bitbucket.org';
token;
constructor() {
super();
this.token = process.env.CI_SFDX_HARDIS_BITBUCKET_TOKEN || '';
const clientOptions = { auth: { token: this.token } };
this.bitbucket = new Bitbucket(clientOptions);
}
getLabel() {
return 'sfdx-hardis Bitbucket connector';
}
async getCurrentJobUrl() {
if (process.env.PIPELINE_JOB_URL) {
return process.env.PIPELINE_JOB_URL;
}
if (process.env.BITBUCKET_WORKSPACE && process.env.BITBUCKET_REPO_SLUG && process.env.BITBUCKET_BUILD_NUMBER) {
const jobUrl = `${this.serverUrl}/${process.env.BITBUCKET_WORKSPACE}/${process.env.BITBUCKET_REPO_SLUG}/pipelines/results/${process.env.BITBUCKET_BUILD_NUMBER}`;
return jobUrl;
}
uxLog(this, c.yellow(`[Bitbucket Integration] You need the following variables to be accessible to sfdx-hardis to build current job url:
- BITBUCKET_WORKSPACE
- BITBUCKET_REPO_SLUG
- BITBUCKET_BUILD_NUMBER`));
return null;
}
async getCurrentBranchUrl() {
if (process.env.BITBUCKET_WORKSPACE && process.env.BITBUCKET_REPO_SLUG && process.env.BITBUCKET_BRANCH) {
const currentBranchUrl = `${this.serverUrl}/${process.env.BITBUCKET_WORKSPACE}/${process.env.BITBUCKET_REPO_SLUG}/branch/${process.env.BITBUCKET_BRANCH}`;
return currentBranchUrl;
}
uxLog(this, c.yellow(`[Bitbucket Integration] You need the following variables to be accessible to sfdx-hardis to build current job url:
- BITBUCKET_WORKSPACE
- BITBUCKET_REPO_SLUG
- BITBUCKET_BRANCH`));
return null;
}
// Bitbucket does not supports mermaid in PR markdown
async supportsMermaidInPrMarkdown() {
return false;
}
// Find pull request info
async getPullRequestInfo() {
const pullRequestIdStr = process.env.BITBUCKET_PR_ID || null;
const repoSlug = process.env.BITBUCKET_REPO_SLUG || null;
const workspace = process.env.BITBUCKET_WORKSPACE || null;
// Case when PR is found in the context
if (pullRequestIdStr !== null) {
const pullRequestId = Number(pullRequestIdStr);
const pullRequest = await this.bitbucket.repositories.getPullRequest({
pull_request_id: pullRequestId,
repo_slug: repoSlug || '',
workspace: workspace || '',
});
if (pullRequest?.data.destination) {
// Add cross git provider properties used by sfdx-hardis
return this.completePullRequestInfo(pullRequest.data);
}
else {
uxLog(this, c.yellow(`[Bitbucket Integration] Warning: incomplete PR found (id: ${pullRequestIdStr})`));
uxLog(this, c.grey(JSON.stringify(pullRequest || {})));
}
}
// Case when we find PR from a commit
const sha = await git().revparse(['HEAD']);
const latestPullRequestsOnBranch = await this.bitbucket.repositories.listPullrequestsForCommit({
// cspell:disable-line
commit: sha,
repo_slug: repoSlug || '',
workspace: workspace || '',
});
const latestMergedPullRequestOnBranch = latestPullRequestsOnBranch?.data?.values?.filter((pr) => pr.state === 'MERGED' && pr.merge_commit?.hash === sha);
if (latestMergedPullRequestOnBranch?.length && latestMergedPullRequestOnBranch?.length > 0) {
const pullRequest = latestMergedPullRequestOnBranch[0];
// Add cross git provider properties used by sfdx-hardis
return this.completePullRequestInfo(pullRequest);
}
uxLog(this, c.grey(`[Bitbucket Integration] Unable to find related Pull Request Info`));
return null;
}
async getBranchDeploymentCheckId(gitBranch) {
let deploymentCheckId = null;
const repoSlug = process.env.BITBUCKET_REPO_SLUG || null;
const workspace = process.env.BITBUCKET_WORKSPACE || null;
const latestMergedPullRequestsOnBranch = await this.bitbucket.repositories.listPullRequests({
repo_slug: repoSlug || '',
workspace: workspace || '',
state: 'MERGED',
q: `destination.branch.name = "${gitBranch}"`,
sort: '-updated_on',
});
if (latestMergedPullRequestsOnBranch?.data?.values?.length &&
latestMergedPullRequestsOnBranch?.data?.values?.length > 0) {
const latestPullRequest = latestMergedPullRequestsOnBranch?.data?.values[0];
const latestPullRequestId = latestPullRequest.id;
deploymentCheckId = await this.getDeploymentIdFromPullRequest(latestPullRequestId || 0, repoSlug || '', workspace || '', deploymentCheckId, this.completePullRequestInfo(latestPullRequest));
}
return deploymentCheckId;
}
async getPullRequestDeploymentCheckId() {
const pullRequestInfo = await this.getPullRequestInfo();
if (pullRequestInfo) {
const repoSlug = process.env.BITBUCKET_REPO_SLUG || null;
const workspace = process.env.BITBUCKET_WORKSPACE || null;
return await this.getDeploymentIdFromPullRequest(pullRequestInfo.idNumber || 0, repoSlug || '', workspace || '', null, pullRequestInfo);
}
return null;
}
async getDeploymentIdFromPullRequest(latestPullRequestId, repoSlug, workspace, deploymentCheckId, latestPullRequest) {
const comments = await this.bitbucket.repositories.listPullRequestComments({
pull_request_id: latestPullRequestId,
repo_slug: repoSlug,
workspace: workspace,
});
for (const comment of comments?.data?.values || []) {
if ((comment?.content?.raw || '').includes(`<!-- sfdx-hardis deployment-id `)) {
const matches = /<!-- sfdx-hardis deployment-id (.*) -->/gm.exec(comment?.content?.raw || '');
if (matches) {
deploymentCheckId = matches[1];
uxLog(this, c.gray(`[Bitbucket Integration] Found deployment id ${deploymentCheckId} on PR #${latestPullRequestId} ${latestPullRequest.title}`));
break;
}
}
}
return deploymentCheckId;
}
async postPullRequestMessage(prMessage) {
const pullRequestIdStr = process.env.BITBUCKET_PR_ID || null;
const repoSlug = process.env.BITBUCKET_REPO_SLUG || null;
const workspace = process.env.BITBUCKET_WORKSPACE || null;
if (repoSlug == null || pullRequestIdStr == null) {
uxLog(this, c.grey('[Bitbucket integration] No repo and pull request, so no note posted...'));
return { posted: false, providerResult: { info: 'No related pull request' } };
}
const pullRequestId = Number(pullRequestIdStr);
const bitbucketBuildNumber = process.env.BITBUCKET_BUILD_NUMBER || null;
const bitbucketJobUrl = await this.getCurrentJobUrl();
const messageKey = `${prMessage.messageKey}-${pullRequestId}`;
let messageBody = `**${prMessage.title || ''}**
${prMessage.message}
\n_Powered by [sfdx-hardis](${CONSTANTS.DOC_URL_ROOT}) from job [${bitbucketBuildNumber}](${bitbucketJobUrl})_
\n<!-- sfdx-hardis message-key ${messageKey} -->
`;
// Add deployment id if present
if (globalThis.pullRequestDeploymentId) {
messageBody += `\n<!-- sfdx-hardis deployment-id ${globalThis.pullRequestDeploymentId} -->`;
}
messageBody = await this.uploadAndReplaceImageReferences(messageBody, prMessage.sourceFile || "");
const commentBody = {
content: {
raw: messageBody,
},
};
// Check for existing comment from a previous run
uxLog(this, c.grey('[Bitbucket integration] Listing comments of Pull Request...'));
const existingComments = await this.bitbucket.repositories.listPullRequestComments({
pull_request_id: pullRequestId,
repo_slug: repoSlug,
workspace: workspace || '',
});
let existingCommentId = null;
for (const existingComment of existingComments?.data?.values || []) {
if (existingComment?.content?.raw &&
existingComment?.content.raw?.includes(`<!-- sfdx-hardis message-key ${messageKey} -->`)) {
existingCommentId = existingComment.id || null;
}
}
// Create or update MR comment
if (existingCommentId) {
// Update existing comment
uxLog(this, c.grey('[Bitbucket integration] Updating Pull Request Comment on Bitbucket...'));
const pullRequestComment = await this.bitbucket.repositories.updatePullRequestComment({
workspace: workspace || '',
repo_slug: repoSlug,
pull_request_id: pullRequestId,
comment_id: existingCommentId,
_body: commentBody,
});
const prResult = {
posted: (pullRequestComment?.data?.id || -1) > 0,
providerResult: pullRequestComment,
};
uxLog(this, c.grey(`[Bitbucket integration] Updated Pull Request comment ${existingCommentId}`));
return prResult;
}
else {
// Create new comment if no existing comment was found
uxLog(this, c.grey('[Bitbucket integration] Adding Pull Request Comment on Bitbucket...'));
const pullRequestComment = await this.bitbucket.repositories.createPullRequestComment({
workspace: workspace || '',
repo_slug: repoSlug,
pull_request_id: pullRequestId,
_body: commentBody,
});
const prResult = {
posted: (pullRequestComment?.data?.id || -1) > 0,
providerResult: pullRequestComment,
};
if (prResult.posted) {
uxLog(this, c.grey(`[Bitbucket integration] Posted Pull Request comment on ${pullRequestId}`));
}
else {
uxLog(this, c.yellow(`[Bitbucket integration] Unable to post Pull Request comment on ${pullRequestId}:\n${JSON.stringify(pullRequestComment, null, 2)}`));
}
return prResult;
}
}
completePullRequestInfo(prData) {
const prInfo = {
idNumber: prData?.id || 0,
idStr: prData.id ? prData?.id?.toString() : '',
sourceBranch: prData?.source?.branch?.name || '',
targetBranch: prData?.destination?.branch?.name || '',
title: prData?.rendered?.title?.raw || prData?.rendered?.title?.markup || prData?.rendered?.title?.html || '',
description: prData?.rendered?.description?.raw || prData?.rendered?.description?.markup || prData?.rendered?.description?.html || '',
webUrl: prData?.links?.html?.href || '',
authorName: prData?.author?.display_name || '',
providerInfo: prData,
customBehaviors: {}
};
return this.completeWithCustomBehaviors(prInfo);
}
// Upload the image to Bitbucket
async uploadImage(localImagePath) {
try {
const imageName = path.basename(localImagePath);
const filesForm = new FormData();
filesForm.append("files", fs.createReadStream(localImagePath));
const attachmentResponse = await this.bitbucket.repositories.createDownload({
workspace: process.env.BITBUCKET_WORKSPACE || "",
repo_slug: process.env.BITBUCKET_REPO_SLUG || "",
_body: filesForm,
});
if (attachmentResponse) {
const imageRef = `${this.serverUrl}/${process.env.BITBUCKET_WORKSPACE}/${process.env.BITBUCKET_REPO_SLUG}/downloads/${imageName}`;
uxLog(this, c.grey(`[Bitbucket Integration] Image uploaded for comment: ${imageRef}`));
return imageRef;
}
else {
uxLog(this, c.yellow(`[Bitbucket Integration] Image uploaded but unable to get URL from response\n${JSON.stringify(attachmentResponse, null, 2)}`));
}
}
catch (e) {
uxLog(this, c.yellow(`[Bitbucket Integration] Error while uploading image in downloads section ${localImagePath}\n${e.message}`));
}
return null;
}
}
//# sourceMappingURL=bitbucket.js.map