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

235 lines 11.1 kB
// Analyze deployment errors to provide tips to user 😊 import c from "chalk"; import format from "string-template"; import { getAllTips } from "./deployTipsList.js"; import { stripAnsi, uxLog } from "./index.js"; import { AiProvider } from "../aiProvider/index.js"; import { updatePullRequestResult } from "./deployTips.js"; import { shortenLogLines } from "./deployUtils.js"; export async function analyzeDeployErrorLogsJson(resultJson, log, includeInLog = true, options) { const allTips = getAllTips(); const tips = []; // Filter to keep only errors (we don't care about warnings) and build legacy message to match deploymentTips const errors = (resultJson?.result?.details?.componentFailures || []) .filter(error => error.success === false && error.problemType === "Error") .map(error => { error.messageInitial = `Error ${error.fullName} ${error.problem}`; error.messageInitialDisplay = `${error.componentType} ${error.fullName}: ${error.problem}`; error.tips = []; return error; }); // Collect errors & tips for (const error of errors) { for (const tipDefinition of allTips) { await matchesTip(tipDefinition, error); if (error.tips.length > 0) { tips.push(tipDefinition); } } // Add default tip if not found if (error.tips.length === 0) { error.message = stripAnsi(error.messageInitial); const errorBase = Object.assign({}, error); delete errorBase.tips; error.tips.push({ error: errorBase }); } } // Enrich with AI if applicable const alreadyProcessedErrors = []; for (const error of errors) { for (const errorTip of error.tips) { const aiTip = await findAiTip(errorTip.error, alreadyProcessedErrors); if (aiTip) { errorTip.tipFromAi = { promptResponse: aiTip.promptResponse, }; } } } // Gather failing tests const failedTests = extractFailedTestsInfo(resultJson?.result?.details?.runTestResult?.failures || []); // Build output list of errors & tips const errorsAndTips = []; for (const error of errors) { for (const errorTip of error.tips) errorsAndTips.push(errorTip); } const detailedErrorLines = []; // Fallback in case we have not been able to identify errors: Check if there are code coverage warnings if (errorsAndTips.length === 0 && failedTests.length === 0 && resultJson?.result?.details?.runTestResult?.codeCoverageWarnings?.length > 0) { for (const cvrgWarning of resultJson.result.details.runTestResult.codeCoverageWarnings) { const coverageErrorMsg = (cvrgWarning.name ? `${cvrgWarning.name} - ` : "") + cvrgWarning.message; errorsAndTips.push(({ error: { message: coverageErrorMsg }, tip: { label: "CodeCoverageWarning", message: "Please fix code coverage so your deployment can pass", docUrl: "https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_code_coverage_intro.htm", }, })); detailedErrorLines.push(...["", "⛔ " + c.red(c.bold("Coverage issue: " + coverageErrorMsg)), ""]); } } // Fallback : declare an error if we have not been able to identify errors if (errorsAndTips.length === 0 && failedTests.length === 0 && resultJson?.result?.errorMessage) { errorsAndTips.push(({ error: { message: resultJson.result.errorMessage }, tip: { label: resultJson.result.errorStatusCode || "UNKNOWN", message: "Please fix unknown errors (", }, })); detailedErrorLines.push(...["", "⛔ " + c.red(c.bold("Unknown issue: " + resultJson.result.errorMessage)), ""]); } // Fallback : declare an error if 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, please notify sfdx-hardis maintainers" }, tip: { label: "SfdxHardisInternalError", message: "Declare issue on https://github.com/hardisgroupcom/sfdx-hardis/issues", }, })); detailedErrorLines.push(...["", "⛔ " + c.red(c.bold("There has been an issue parsing errors, please notify sfdx-hardis maintainers")), ""]); } // Create output log for errors for (const error of errors) { detailedErrorLines.push(...["", "⛔ " + c.red(c.bold(error.messageInitialDisplay)), ""]); if (error.tips.length > 0 && error.tips.some(err => err.tip || err.tipFromAi)) { for (const errorTip of error.tips) { if (errorTip.tip) { detailedErrorLines.push(...[ c.yellow(c.italic("✏️ Error " + c.bold(errorTip.tip.label)) + ":"), c.yellow(errorTip.tip.messageConsole), c.yellow(`Documentation: ${errorTip.tip.docUrl}`) ]); } if (errorTip.tipFromAi) { detailedErrorLines.push(...[ c.yellow(c.italic("🤖 AI response:")), c.yellow(errorTip.tipFromAi.promptResponse) ]); } } } else { detailedErrorLines.push(...[c.yellow("No tip found for error. Try asking ChatGPT, Google or a Release Manager 😊")]); } } detailedErrorLines.push(""); // Create output log for test failures if (failedTests.length > 0) { detailedErrorLines.push(...["", c.red(c.bold("Test failures:"))], ""); for (const failedTest of failedTests) { detailedErrorLines.push(...[ c.red(`💥 ${c.bold(failedTest.class)}.${c.bold(failedTest.method)}: ${failedTest.error}`), c.grey(`Stack: ${failedTest.stack || "none"}`), "" ]); } } // Update data that will be used for Pull Request comment await updatePullRequestResult(errorsAndTips, failedTests, options); // Return results const newLog = includeInLog ? shortenLogLines(log) + "\n\n" + detailedErrorLines.join("\n") : shortenLogLines(log); return { tips, errorsAndTips, failedTests, errLog: newLog }; } async function matchesTip(tipDefinition, error) { matchStringBasedTip(tipDefinition, error); matchRegExpBasedTip(tipDefinition, error); } function matchStringBasedTip(tipDefinition, error) { if (tipDefinition.expressionString && tipDefinition.expressionString.filter((expressionString) => error.messageInitial.includes(expressionString)).length > 0) { error.message = stripAnsi(error.messageInitial); const errorBase = Object.assign({}, error); delete errorBase.tips; error.tips.push({ error: errorBase, tip: { label: tipDefinition.label, docUrl: tipDefinition.docUrl, message: tipDefinition.tip, messageConsole: tipDefinition.tip, }, }); } } function matchRegExpBasedTip(tipDefinition, error) { if (tipDefinition.expressionRegex && tipDefinition.expressionRegex.filter((expressionRegex) => { expressionRegex.lastIndex = 0; // reset regex last index to be able to reuse it return expressionRegex.test(error.messageInitial); }).length > 0) { const regex = tipDefinition.expressionRegex.filter((expressionRegex) => { expressionRegex.lastIndex = 0; // reset regex last index to be able to reuse it return expressionRegex.test(error.messageInitial); })[0]; regex.lastIndex = 0; // reset regex last index to be able to reuse it const matches = [...error.messageInitial.matchAll(regex)]; for (const m of matches) { const replacements = m.map((str) => c.bold(str.trim().replace(/'/gm, ""))); const replacementsMarkdown = m.map((str) => `**${str.trim().replace(/'/gm, "")}**`); error.message = stripAnsi(format(error.messageInitial, replacementsMarkdown)).replace(/\*\*.\*\*/gm, "."); const errorBase = Object.assign({}, error); delete errorBase.tips; error.tips.push({ error: errorBase, tip: { label: tipDefinition.label, docUrl: tipDefinition.docUrl, message: stripAnsi(format(tipDefinition.tip, replacementsMarkdown).replace(/\*\*.\*\*/gm, ".")), messageConsole: tipDefinition.tip.split(/\r?\n/).map((str) => format(str, replacements)).join("\n") }, }); } } } function extractFailedTestsInfo(failedTestsIn) { const failedTests = []; for (const failedTestIn of failedTestsIn || []) { const failedTestRes = { class: (failedTestIn.namespace ? failedTestIn.namespace + "__" : '') + failedTestIn.name, method: failedTestIn.methodName, error: failedTestIn.message, }; if (failedTestIn?.stackTrace) { failedTestRes.stack = failedTestIn.stackTrace; } failedTests.push(failedTestRes); } return failedTests; } async function findAiTip(error, alreadyProcessedErrors) { if (alreadyProcessedErrors.includes(error.message)) { return null; } alreadyProcessedErrors.push(error.message); if (AiProvider.isAiAvailable()) { if (alreadyProcessedErrors.length > parseInt(process.env.MAX_DEPLOYMENT_TIPS_AI_CALLS || "20")) { uxLog("warning", 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 = buildPrompt(error); try { const aiResponse = await AiProvider.promptAi(prompt, "PROMPT_SOLVE_DEPLOYMENT_ERROR"); return aiResponse; } catch (e) { uxLog("warning", this, c.yellow("[AI] Error while calling AI Provider: " + e.message)); } } return null; } function buildPrompt(error) { const prompt = `You are a Salesforce release manager using Salesforce CLI commands to perform deployments \n` + `How to solve the following Salesforce deployment error ?\n` + "- Please answer using sfdx source format, not metadata format. \n" + "- Please provide XML example if applicable. \n" + "- Please skip the part of the response about how to retrieve or deploy the changes with Salesforce CLI.\n" + `The error is: \n${JSON.stringify(error, null, 2)}`; return prompt; } //# sourceMappingURL=deployTipJson.js.map