UNPKG

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

305 lines 14.4 kB
// Analyze deployment errors to provide tips to user :) import c from "chalk"; import format from "string-template"; import { getAllTips } from "./deployTipsList.js"; import { deployErrorsToMarkdown, testFailuresToMarkdown } from "../gitProvider/utilsMarkdown.js"; import { findJsonInString, stripAnsi, uxLog } from "./index.js"; import { AiProvider } from "../aiProvider/index.js"; import { analyzeDeployErrorLogsJson } from "./deployTipJson.js"; let logRes = null; let errorsAndTips = []; let alreadyProcessedErrors = []; const firstYellowChar = c.yellow("*")[0]; // Checks for deploy tips in a log string // returns formatted and completed error log export async function analyzeDeployErrorLogs(log, includeInLog = true, options) { // New way using json: should be always be used const jsonResult = findJsonInString(log); if (jsonResult) { const resultsFromJson = await analyzeDeployErrorLogsJson(jsonResult, log, includeInLog, options); if (resultsFromJson && (resultsFromJson?.errorsAndTips.length > 0 || resultsFromJson?.failedTests?.length > 0)) { return resultsFromJson; } } errorsAndTips = []; // reset alreadyProcessedErrors = []; // reset logRes = returnErrorLines(log).join("\n"); // reset const tips = []; for (const tipDefinition of getAllTips()) { if (await matchesTip(tipDefinition, includeInLog)) { tips.push(tipDefinition); } } // Add default error messages for errors without tips const logResLines = []; const updatedLogLines = returnErrorLines(logRes); let index = 0; for (const logLine of updatedLogLines) { logResLines.push(logLine.trim()); if (logLine.includes("Deploy failed.")) { index++; continue; } if (isErrorLine(logLine) && (updatedLogLines[index + 1] && isErrorLine(updatedLogLines[index + 1]) || !updatedLogLines[index + 1])) { const aiTip = await findAiTip(logLine.trim()); // Complete with AI if possible if (aiTip && aiTip.success) { logResLines.push(c.yellow(`[AI] ${aiTip.model} suggested the following tip ${c.bold(c.bgRed("(can be good or stupid, this is AI !)"))}:`)); logResLines.push(c.magenta(c.italic(aiTip.promptResponse))); logResLines.push(c.yellow("")); errorsAndTips.push({ error: { message: stripAnsi(logLine.trim()) }, tipFromAi: { promptResponse: aiTip.promptResponse, }, }); } else { const promptText = AiProvider.buildPrompt("PROMPT_SOLVE_DEPLOYMENT_ERROR", { "ERROR": logLine.trim() }); // No tip found, give the user an AI prompt logResLines.push(c.yellow("No sfdx-hardis tip to solve this error. You can try the following prompt:")); logResLines.push(c.yellow(promptText)); logResLines.push(c.yellow("")); errorsAndTips.push({ error: { message: stripAnsi(logLine.trim()) }, tipFromAi: { promptText: promptText, }, }); } } index++; } // Extract failed test classes const logRaw = stripAnsi(log); const failedTests = []; // sf project deploy output extractFailedTestsInfoForSfCommand(logRaw, failedTests); if (failedTests.length === 0) { // Legacy sfdx force:source:deploy output extractFailedTestsInfoForSfdxCommand(logRaw, failedTests); } // Fallback in case we have not been able to identify errors if (errorsAndTips.length === 0 && failedTests.length === 0) { errorsAndTips.push(({ error: { message: "There has been an issue parsing errors, probably because of a SF CLI output format update. Please check console logs." }, tip: { label: "SfdxHardisParseError", message: "If you are in CI/CD, please check at the bottom of deployment check job logs. The issue will be fixed ASAP.", }, })); } await updatePullRequestResult(errorsAndTips, failedTests, options); return { tips, errorsAndTips, failedTests, errLog: logResLines.join("\n") }; } function isErrorLine(str) { const strTrim = str.trim(); if (strTrim.startsWith("Error") || strTrim.startsWith("| Error")) { return true; } return false; } function extractFailedTestsInfoForSfdxCommand(logRaw, failedTests) { const regexFailedTests = /Test Failures([\S\s]*?)Test Success/gm; if (logRaw.match(regexFailedTests)) { const failedTestsLines = (regexFailedTests .exec(logRaw) || [])[1] .split("\n") .map((s) => s.trim()); let failedTest = null; // Parse strings to extract main error line then stack for (const line of failedTestsLines) { const regex = /^(\w+[\d_]*)\s+(\w+[\d_]*)\s*(.*)$/; const match = line.match(regex); if (match) { if (match[1] === "Name") { // header column continue; } const errSplit = match[3].split("Class."); failedTest = { class: match[1], method: match[2], error: (errSplit.shift() || "").trim(), }; if (errSplit.length > 0) { failedTest.stack = "Class." + errSplit.join("\nClass."); } failedTests.push(failedTest); } } } } function extractFailedTestsInfoForSfCommand(logRaw, failedTests) { const regexFailedTests = /Test Failures([\S\s]*?)Test Success/gm; if (logRaw.match(regexFailedTests)) { const failedTestsString = (regexFailedTests.exec(logRaw) || [])[1].split(/\r?\n/).join("\n") + "\n•"; // Parse strings to extract main error line then stack // eslint-disable-next-line no-regex-spaces, no-useless-escape const regex = /^• (.*)\n message: (.*)\n stacktrace: ([\s\S]*?)(?=\n•|\z)/gm; const matches = [...failedTestsString.matchAll(regex)]; for (const match of matches || []) { const failedTest = { class: match[1].split(".")[0], method: match[1].split(".")[1], error: match[2].trim(), }; if (match[3]) { failedTest.stack = match[3]; } failedTests.push(failedTest); } } } // Checks if the error string or regex is found in the log // Adds the fix tip under the line if includeInLog is true async function matchesTip(tipDefinition, includeInLog = true) { const newLogLines = []; // string matching if (tipDefinition.expressionString && tipDefinition.expressionString.filter((expressionString) => { return (logRes || "").includes(expressionString); }).length > 0) { if (includeInLog) { const logLines = returnErrorLines(logRes); for (const line of logLines) { newLogLines.push(line); let found = false; for (const expressionString of tipDefinition.expressionString) { if (line.includes(expressionString)) { found = true; newLogLines.push(c.yellow(c.italic("Tip for " + tipDefinition.label + ":"))); newLogLines.push(...tipDefinition.tip.split(/\r?\n/).map((str) => c.yellow(str))); newLogLines.push(c.yellow(" ")); // Update output list errorsAndTips.push({ error: { message: stripAnsi(line) }, tip: { label: tipDefinition.label, message: tipDefinition.tip, docUrl: tipDefinition.docUrl }, }); } } if (found) { const aiTip = await findAiTip(line.trim()); // Complete with AI if possible if (aiTip && aiTip.success) { newLogLines.push(c.yellow(`[AI] ${aiTip.model} suggested the following tip ${c.bold(c.bgRed("(can be good or stupid, this is AI !)"))}:`)); newLogLines.push(c.magenta(c.italic(aiTip.promptResponse))); newLogLines.push(c.yellow("")); const lastErrorAndTip = errorsAndTips[errorsAndTips.length - 1]; lastErrorAndTip.tipFromAi = { promptResponse: aiTip.promptResponse, }; errorsAndTips[errorsAndTips.length - 1] = lastErrorAndTip; } } } logRes = newLogLines.join("\n"); } return true; } // regex matching /* jscpd:ignore-start */ if (tipDefinition.expressionRegex && tipDefinition.expressionRegex.filter((expressionRegex) => { return expressionRegex.test(logRes); }).length > 0) { if (includeInLog) { const newLogLines = []; const logLines = returnErrorLines(logRes); for (const line of logLines) { newLogLines.push(line); let found = false; for (const expressionRegex of tipDefinition.expressionRegex) { expressionRegex.lastIndex = 0; // reset regex last index to be able to reuse it const matches = [...line.matchAll(expressionRegex)]; for (const m of matches) { found = true; const replacements = m.map((str) => c.bold(str.trim().replace(/'/gm, ""))); const replacementsMarkdown = m.map((str) => `**${str.trim().replace(/'/gm, "")}**`); newLogLines.push(c.yellow(c.italic(format(tipDefinition.label, replacements)))); const tip = tipDefinition.tip; newLogLines.push(...tip.split(/\r?\n/).map((str) => c.yellow(format(str, replacements)))); newLogLines.push(c.yellow(" ")); // Update output list errorsAndTips.push({ error: { message: stripAnsi(format(line, replacementsMarkdown)).replace(/\*\*.\*\*/gm, ".") }, tip: { label: tipDefinition.label, docUrl: tipDefinition.docUrl, message: stripAnsi(format(tipDefinition.tip, replacementsMarkdown).replace(/\*\*.\*\*/gm, ".")), }, }); } if (found) { const aiTip = await findAiTip(line.trim()); // Complete with AI if possible if (aiTip && aiTip.success) { newLogLines.push(c.yellow(`[AI] ${aiTip.model} suggested the following tip ${c.bold(c.bgRed("(can be good or stupid, this is AI !)"))}:`)); newLogLines.push(c.magenta(c.italic(aiTip.promptResponse))); newLogLines.push(c.yellow("")); const lastErrorAndTip = errorsAndTips[errorsAndTips.length - 1]; lastErrorAndTip.tipFromAi = { promptResponse: aiTip.promptResponse, }; errorsAndTips[errorsAndTips.length - 1] = lastErrorAndTip; } } } } logRes = newLogLines.join("\n"); } return true; } return false; /* jscpd:ignore-end */ } function returnErrorLines(strIn) { return strIn.split(/\r?\n/).filter((str) => isErrorLine(str) || str.startsWith(firstYellowChar)); } // This data will be caught later to build a pull request message export async function updatePullRequestResult(errorsAndTips, failedTests, options) { const prData = { messageKey: "deployment", title: options.check ? "✅ Deployment check success" : "✅ Deployment success", deployErrorsMarkdownBody: "No error has been found during the deployment", deployStatus: "valid", }; if (errorsAndTips.length > 0) { prData.title = options.check ? "❌ Deployment check failure" : "❌ Deployment failure"; prData.deployErrorsMarkdownBody = deployErrorsToMarkdown(errorsAndTips); prData.status = "invalid"; } else if (failedTests.length > 0) { prData.title = options.check ? "❌ Deployment check ok, but failing test classes" : "❌ Deployment check ok, but failing test classes"; prData.deployErrorsMarkdownBody = testFailuresToMarkdown(failedTests); prData.status = "invalid"; } globalThis.pullRequestData = Object.assign(globalThis.pullRequestData || {}, prData); } async function findAiTip(errorLine) { if (alreadyProcessedErrors.includes(errorLine)) { return null; } alreadyProcessedErrors.push(errorLine); if (AiProvider.isAiAvailable()) { if (alreadyProcessedErrors.length > parseInt(process.env.MAX_DEPLOYMENT_TIPS_AI_CALLS || "20")) { uxLog(this, c.yellow(`[AI] Maximum number of AI calls for deployment tips reached. Increase with env var MAX_DEPLOYMENT_TIPS_AI_CALLS`)); return null; } const prompt = AiProvider.buildPrompt("PROMPT_SOLVE_DEPLOYMENT_ERROR", { "ERROR": errorLine }); try { const aiResponse = await AiProvider.promptAi(prompt, "PROMPT_SOLVE_DEPLOYMENT_ERROR"); return aiResponse; } catch (e) { uxLog(this, c.yellow("[AI] Error while calling OpenAI: " + e.message)); } } return null; } //# sourceMappingURL=deployTips.js.map