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
344 lines • 14.9 kB
JavaScript
import { Version3Client } from "jira.js";
import { TicketProviderRoot } from "./ticketProviderRoot.js";
import c from "chalk";
import sortArray from "sort-array";
import { getBranchMarkdown, getOrgMarkdown } from "../utils/notifUtils.js";
import { extractRegexMatches, uxLog } from "../utils/index.js";
import { SfError } from "@salesforce/core";
import { getConfig, getEnvVar } from "../../config/index.js";
export class JiraProvider extends TicketProviderRoot {
jiraClient = null;
jiraHost = null;
constructor(config) {
super();
const rawHost = getEnvVar("JIRA_HOST") || config.jiraHost || "";
const sanitizedHost = rawHost.startsWith("http") ? rawHost : `https://${rawHost}`;
this.jiraHost = sanitizedHost.replace(/\/$/, "");
const jiraOptions = {
host: this.jiraHost || '',
};
// Basic Auth
if (getEnvVar("JIRA_EMAIL") && getEnvVar("JIRA_TOKEN")) {
jiraOptions.authentication = {
basic: {
email: getEnvVar("JIRA_EMAIL") || "",
apiToken: getEnvVar("JIRA_TOKEN") || "",
},
};
this.isActive = true;
uxLog("log", this, c.grey("[JiraProvider] Using JIRA_EMAIL and JIRA_TOKEN for authentication"));
}
// Personal access token
else if (getEnvVar("JIRA_PAT")) {
jiraOptions.authentication = {
oauth2: {
accessToken: getEnvVar("JIRA_PAT") || "",
},
};
this.isActive = true;
uxLog("log", this, c.grey("[JiraProvider] Using JIRA_PAT for authentication"));
}
if (this.isActive) {
this.jiraClient = new Version3Client(jiraOptions);
}
}
static isAvailable(config) {
if (
// Basic auth
(getEnvVar("JIRA_HOST") || config.jiraHost) &&
getEnvVar("JIRA_TOKEN") &&
getEnvVar("JIRA_EMAIL")) {
return true;
}
if (
// Personal Access Token
(getEnvVar("JIRA_HOST") || config.jiraHost) &&
getEnvVar("JIRA_PAT")) {
return true;
}
return false;
}
getLabel() {
return "sfdx-hardis JIRA connector";
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static async getTicketsFromString(text, options = {}) {
const tickets = [];
// Extract JIRA tickets using URL references
const jiraUrlRegex = /(https:\/\/.*(jira|atlassian\.net).*\/[A-Z0-9]+-\d+\b)/g;
const jiraMatches = await extractRegexMatches(jiraUrlRegex, text);
for (const jiraTicketUrl of jiraMatches) {
const pattern = /https:\/\/.*\/([A-Z0-9]+-\d+\b)/;
const match = jiraTicketUrl.match(pattern);
if (match) {
const ticketId = match[1];
if (!tickets.some((ticket) => ticket.url === jiraTicketUrl || ticket.id === ticketId)) {
tickets.push({
provider: "JIRA",
url: jiraTicketUrl,
id: ticketId,
});
}
}
}
// Extract JIRA tickets using Identifiers
const config = await getConfig("project");
const jiraBaseUrl = getEnvVar("JIRA_HOST") || config.jiraHost || "https://define.JIRA_HOST.in.cicd.variables/";
const jiraRegex = getEnvVar("JIRA_TICKET_REGEX") || config.jiraTicketRegex || "(?<=[^a-zA-Z0-9_-]|^)([A-Za-z0-9]{2,10}-\\d{1,6})(?=[^a-zA-Z0-9_-]|$)";
const jiraRefRegex = new RegExp(jiraRegex, "gm");
const jiraRefs = await extractRegexMatches(jiraRefRegex, text);
const jiraBaseUrlBrowse = jiraBaseUrl.replace(/\/$/, "") + "/browse/";
for (const jiraRef of jiraRefs) {
const jiraTicketUrl = jiraBaseUrlBrowse + jiraRef;
if (!tickets.some((ticket) => ticket.url === jiraTicketUrl || ticket.id === jiraRef)) {
tickets.push({
provider: "JIRA",
url: jiraTicketUrl,
id: jiraRef,
});
}
}
const ticketsSorted = sortArray(tickets, { by: ["id"], order: ["asc"] });
return ticketsSorted;
}
async collectTicketsInfo(tickets) {
if (!this.jiraClient) {
return tickets;
}
const jiraTicketsNumber = tickets.filter((ticket) => ticket.provider === "JIRA").length;
if (jiraTicketsNumber > 0) {
uxLog("action", this, c.cyan(`[JiraProvider] Now trying to collect ${jiraTicketsNumber} tickets infos from JIRA server ` + this.jiraHost + " ..."));
}
for (const ticket of tickets) {
if (ticket.provider === "JIRA") {
let ticketInfo = null;
try {
ticketInfo = await this.jiraClient.issues.getIssue({ issueIdOrKey: ticket.id });
}
catch (e) {
uxLog("warning", this, c.yellow(`[JiraApi] Error while trying to get ${ticket.id} information: ${e.message}`));
}
if (ticketInfo) {
const body = this.getPlainTextFromDescription(ticketInfo?.fields?.description);
ticket.foundOnServer = true;
ticket.subject = ticketInfo?.fields?.summary || "";
ticket.body = body;
ticket.status = ticketInfo.fields?.status?.id || "";
ticket.statusLabel = ticketInfo.fields?.status?.name || "";
const assignee = ticketInfo.fields?.assignee;
const reporter = ticketInfo.fields?.reporter;
if (assignee) {
ticket.assignee = assignee.accountId || assignee.name || "";
ticket.assigneeLabel = assignee.displayName || "";
}
if (reporter) {
ticket.reporter = reporter.accountId || reporter.name || "";
ticket.reporterLabel = reporter.displayName || "";
}
const preferredOwner = assignee || reporter;
if (preferredOwner) {
ticket.author = preferredOwner.accountId || preferredOwner.name || "";
ticket.authorLabel = preferredOwner.displayName || "";
}
if (ticket.subject === "") {
uxLog("warning", this, c.yellow("[JiraProvider] Unable to collect JIRA ticket info for " + ticket.id));
if (JSON.stringify(ticketInfo).includes("<!DOCTYPE html>")) {
uxLog("log", this, c.grey("[JiraProvider] This is probably a JIRA auth config issue, as HTML is returned"));
}
else {
uxLog("log", this, c.grey(JSON.stringify(ticketInfo)));
}
ticket.foundOnServer = false;
}
uxLog("log", this, c.grey("[JiraProvider] Collected data for ticket " + ticket.id));
}
else {
uxLog("warning", this, c.yellow("[JiraProvider] Unable to get JIRA issue " + ticket.id));
}
}
}
return tickets;
}
async postDeploymentComments(tickets, org, pullRequestInfo) {
if (!this.jiraClient) {
return tickets;
}
uxLog("action", this, c.cyan(`[JiraProvider] Try to post comments on ${tickets.length} tickets...`));
const genericHtmlResponseError = "Probably config/access error since response is HTML";
const orgMarkdown = JSON.parse(await getOrgMarkdown(org, "jira"));
const branchMarkdown = JSON.parse(await getBranchMarkdown("jira"));
const tag = await this.getDeploymentTag();
const commentedTickets = [];
const taggedTickets = [];
for (const ticket of tickets) {
if (ticket.foundOnServer) {
// Build comment
let prTitle = "";
let prUrl = "";
let prAuthor = "";
if (pullRequestInfo) {
prUrl = pullRequestInfo.webUrl;
if (prUrl) {
prTitle = pullRequestInfo.title;
prAuthor = pullRequestInfo?.authorName;
}
}
const jiraComment = this.getJiraDeploymentCommentAdf(orgMarkdown.label, orgMarkdown.url, branchMarkdown.label, branchMarkdown.url || "", prTitle, prUrl, prAuthor);
// Post comment
try {
const commentPostRes = await this.jiraClient.issueComments.addComment({ issueIdOrKey: ticket.id, comment: jiraComment });
if (JSON.stringify(commentPostRes).includes("<!DOCTYPE html>")) {
throw new SfError(genericHtmlResponseError);
}
commentedTickets.push(ticket);
}
catch (e6) {
uxLog("warning", this, c.yellow(`[JiraProvider] Error while posting comment on ${ticket.id}: ${e6.message}`));
}
// Add deployment label to JIRA ticket
try {
await this.jiraClient.issues.editIssue({
issueIdOrKey: ticket.id,
update: {
labels: [{ add: tag }],
},
});
taggedTickets.push(ticket);
}
catch (e6) {
if (e6.message != null && e6.message.includes("<!doctype html>")) {
e6.message = genericHtmlResponseError;
}
uxLog("warning", this, c.yellow(`[JiraProvider] Error while adding label ${tag} on ${ticket.id}: ${e6.message}`));
}
}
}
// Summary
if (commentedTickets.length > 0 || taggedTickets.length > 0) {
uxLog("log", this, c.grey(`[JiraProvider] Posted comments on ${commentedTickets.length} ticket(s): ` + commentedTickets.map((ticket) => ticket.id).join(", ")));
uxLog("log", this, c.grey(`[JiraProvider] Added label ${tag} on ${taggedTickets.length} ticket(s): ` + taggedTickets.map((ticket) => ticket.id).join(", ")));
}
return tickets;
}
getJiraDeploymentCommentAdf(orgName, orgUrl, branchName, branchUrl, prTitle, prUrl, prAuthor) {
const comment = {
version: 1,
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: "Deployed by ",
},
{
type: "text",
text: "sfdx-hardis",
marks: [
{
type: "link",
attrs: {
href: "${CONSTANTS.DOC_URL_ROOT}/",
},
},
],
},
{
type: "text",
text: " in ",
},
{
type: "text",
text: orgName,
marks: [
{
type: "link",
attrs: {
href: orgUrl,
},
},
{
type: "strong",
},
],
},
{
type: "text",
text: " from branch ",
},
{
type: "text",
text: branchName,
marks: [
{
type: "link",
attrs: {
href: branchUrl,
},
},
{
type: "strong",
},
],
},
],
},
{
type: "paragraph",
content: [
{
type: "text",
text: "Related PR: ",
},
{
type: "text",
text: prTitle,
marks: [
{
type: "link",
attrs: {
href: prUrl,
},
},
],
},
{
type: "text",
text: `, by ${prAuthor}`,
},
],
},
],
};
return comment;
}
getPlainTextFromDescription(description) {
if (!description) {
return "";
}
if (typeof description === "string") {
return description;
}
const segments = [];
const visitNode = (node) => {
if (!node) {
return;
}
if (typeof node.text === "string") {
segments.push(node.text);
}
if (Array.isArray(node.content)) {
for (const child of node.content) {
visitNode(child);
}
if (node.type === "paragraph") {
segments.push("\n");
}
}
};
visitNode(description);
return segments.join("").replace(/\n{3,}/g, "\n\n").trim();
}
}
//# sourceMappingURL=jiraProvider.js.map