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
283 lines (281 loc) • 15.3 kB
JavaScript
import { SfError } from '@salesforce/core';
import c from 'chalk';
import fs from 'fs-extra';
import * as path from 'path';
import { getConfig } from '../../config/index.js';
import { uxLog } from './index.js';
import { GitProvider } from '../gitProvider/index.js';
import { checkSfdxHardisTraceAvailable } from './orgConfigUtils.js';
import { soqlQuery } from './apiUtils.js';
// data import moved to DataAction class in actionsProvider
import { getPullRequestData, setPullRequestData } from './gitUtils.js';
import { ActionsProvider } from '../actionsProvider/actionsProvider.js';
import { getPullRequestScopedSfdxHardisConfig, listAllPullRequestsForCurrentScope } from './pullRequestUtils.js';
export async function executePrePostCommands(property, options) {
const actionLabel = property === 'commandsPreDeploy' ? 'Pre-deployment actions' : 'Post-deployment actions';
uxLog("action", this, c.cyan(`[DeploymentActions] Listing ${actionLabel}...`));
const branchConfig = await getConfig('branch');
const extraCommands = (options.extraCommands || []).filter(cmd => cmd.preOrPost === property);
const commands = [...(branchConfig[property] || []), ...(extraCommands || [])];
try {
await completeWithCommandsFromPullRequests(property, commands, options.checkOnly);
}
catch (e) {
uxLog("error", this, c.red(`[DeploymentActions] Error while retrieving commands from pull requests: ${e.message}\n ${e.stack}\n You might report the issue on sfdx-hardis GitHub repository.`));
}
if (commands.length === 0) {
uxLog("action", this, c.cyan(`[DeploymentActions] No ${actionLabel} defined in branch config or pull requests`));
uxLog("log", this, c.grey(`No ${property} found to run`));
return;
}
uxLog("action", this, c.cyan(`[DeploymentActions] Found ${commands.length} ${actionLabel} to run\n` +
commands.map(c => `- ${c.label} (${c.type || 'command'})`).join('\n')));
for (const cmd of commands) {
const actionsInstance = await ActionsProvider.buildActionInstance(cmd);
const actionsIssues = await actionsInstance.checkValidityIssues(cmd);
if (actionsIssues) {
cmd.result = actionsIssues;
uxLog("error", this, c.red(`[DeploymentActions] Action ${cmd.label} is not valid: ${actionsIssues.skippedReason}`));
continue;
}
// If if skipIfError is true and deployment failed
if (options.success === false && !(cmd?.skipIfError === false)) {
uxLog("action", this, c.yellow(`[DeploymentActions] Skipping ${cmd.label} (skipIfError=true) `));
cmd.result = {
statusCode: "skipped",
skippedReason: "skipIfError is true and deployment failed"
};
continue;
}
// Skip if we are in another context than the requested one
const cmdContext = cmd.context || "all";
if (cmdContext === "check-deployment-only" && options.checkOnly === false) {
uxLog("action", this, c.grey(`[DeploymentActions] Skipping ${cmd.label}: check-deployment-only action, and we are in process deployment mode`));
cmd.result = {
statusCode: "skipped",
skippedReason: "Action context is check-deployment-only but we are in process deployment mode"
};
continue;
}
if (cmdContext === "process-deployment-only" && options.checkOnly === true) {
uxLog("action", this, c.grey(`[DeploymentActions] Skipping ${cmd.label}: process-deployment-only action as we are in check deployment mode`));
cmd.result = {
statusCode: "skipped",
skippedReason: "Action context is process-deployment-only but we are in check deployment mode"
};
continue;
}
const runOnlyOnceByOrg = cmd.runOnlyOnceByOrg || false;
if (runOnlyOnceByOrg) {
await checkSfdxHardisTraceAvailable(options.conn);
const commandTraceQuery = `SELECT Id,CreatedDate FROM SfdxHardisTrace__c WHERE Type__c='${property}' AND Key__c='${cmd.id}' LIMIT 1`;
const commandTraceRes = await soqlQuery(commandTraceQuery, options.conn);
if (commandTraceRes?.records?.length > 0) {
uxLog("action", this, c.grey(`[DeploymentActions] Skipping ${cmd.label}: it has been defined with runOnlyOnceByOrg and has already been run on ${commandTraceRes.records[0].CreatedDate}`));
cmd.result = {
statusCode: "skipped",
skippedReason: "runOnlyOnceByOrg is true and command has already been run on this org"
};
continue;
}
}
// Run command
uxLog("action", this, c.cyan(`[DeploymentActions] Running action ${cmd.label}`));
await executeAction(cmd);
if (cmd.result?.statusCode === "success" && runOnlyOnceByOrg) {
const hardisTraceRecord = {
Name: property + "--" + cmd.id,
Type__c: property,
Key__c: cmd.id
};
const insertRes = await options.conn.insert("SfdxHardisTrace__c", [hardisTraceRecord]);
if (insertRes[0].success) {
uxLog("success", this, c.green(`[DeploymentActions] Stored SfdxHardisTrace__c entry ${insertRes[0].id} with command [${cmd.id}] so it is not run again in the future (runOnlyOnceByOrg: true)`));
}
else {
uxLog("error", this, c.red(`[DeploymentActions] Error storing SfdxHardisTrace__c entry :` + JSON.stringify(insertRes, null, 2)));
}
}
else if (cmd.result?.statusCode === "failed" && cmd.allowFailure !== true) {
uxLog("error", this, c.red(`[DeploymentActions] Action ${cmd.label} failed, stopping execution of further actions.`));
break;
}
}
manageResultMarkdownBody(property, commands, options.checkOnly);
// Check commands results
const failedCommands = commands.filter(c => c.result?.statusCode === "failed");
if (failedCommands.length > 0) {
uxLog("error", this, c.red(`[DeploymentActions] ${failedCommands.length} action(s) failed during ${actionLabel}:`));
// throw error if failed and allowFailure is not set
const failedAndNotAllowFailure = failedCommands.filter(c => c.allowFailure !== true);
if (failedAndNotAllowFailure.length > 0) {
let prData = getPullRequestData();
prData = Object.assign(prData, {
title: "❌ Error: Failed deployment actions",
messageKey: prData.messageKey ?? 'deployment',
});
setPullRequestData(prData);
await GitProvider.managePostPullRequestComment(options.checkOnly);
throw new SfError(`One or more ${actionLabel} have failed. See logs for more details.`);
}
}
}
async function completeWithCommandsFromPullRequests(property, commands, checkOnly) {
await checkForDraftCommandsFile(property, checkOnly);
const pullRequests = await listAllPullRequestsForCurrentScope(checkOnly);
for (const pr of pullRequests) {
// Check if there is a .sfdx-hardis.PULL_REQUEST_ID.yml file in the PR
const prConfigParsed = await getPullRequestScopedSfdxHardisConfig(pr);
if (prConfigParsed && prConfigParsed[property] && Array.isArray(prConfigParsed[property])) {
const prConfigCommands = prConfigParsed[property];
for (const cmd of prConfigCommands) {
cmd.pullRequest = pr;
commands.push(cmd);
}
}
}
}
async function checkForDraftCommandsFile(property, checkOnly) {
const prConfigFileName = path.join("scripts", "actions", `.sfdx-hardis.draft.yml`);
if (fs.existsSync(prConfigFileName)) {
let suggestedFileName = ".sfdx-hardis.PULL_REQUEST_ID.yml (ex: .sfdx-hardis.123.yml)";
const prInfo = await GitProvider.getPullRequestInfo();
if (prInfo && prInfo.idStr) {
suggestedFileName = `.sfdx-hardis.${prInfo.idStr}.yml`;
}
const errorMessage = `Draft deployment actions file ${prConfigFileName} found.
Please assign it to a Pull Request before proceeding, or delete the file it if you don't need it.
To assign it, rename .sfdx-hardis.draft.yml into ${suggestedFileName}.
`;
const propertyFormatted = property === 'commandsPreDeploy' ? 'preDeployCommandsResultMarkdownBody' : 'postDeployCommandsResultMarkdownBody';
let prData = getPullRequestData();
prData = Object.assign(prData, {
title: "❌ Error: Draft deployment actions file found",
messageKey: prData.messageKey ?? 'deployment',
[propertyFormatted]: errorMessage
});
setPullRequestData(prData);
await GitProvider.managePostPullRequestComment(checkOnly);
uxLog("error", this, c.red(`[DeploymentActions] ${errorMessage}`));
throw new SfError(`Draft commands file ${prConfigFileName} found. Please assign it to a Pull Request or delete it before proceeding.`);
}
}
async function executeAction(cmd) {
// Use ActionsProvider classes to execute actions
const actionInstance = await ActionsProvider.buildActionInstance(cmd);
try {
const res = await actionInstance.run(cmd);
cmd.result = res;
}
catch (e) {
uxLog("error", this, c.red(`[DeploymentActions] Exception while running action ${cmd.label}: ${e.message}`));
cmd.result = {
statusCode: 'failed',
output: e.message
};
}
}
function buildManualActionsSection(commands, isPreDeploy, checkOnly) {
if (isPreDeploy && !checkOnly) {
return '';
}
if (!isPreDeploy && checkOnly) {
return '';
}
const manualCommands = commands.filter(c => c.type === "manual");
if (manualCommands.length === 0) {
return '';
}
const title = isPreDeploy
? `#### Manual Actions to perform before proceeding with deployment:\n\n`
: `#### Manual Actions to perform after deployment:\n\n`;
let section = title;
for (const cmd of manualCommands) {
const labelCol = cmd.pullRequest
? `${cmd.label} ([${cmd.pullRequest.idStr || "?"}](${cmd.pullRequest.webUrl || ""}))`
: cmd.label;
section += `- [ ] ${labelCol}\n`;
}
section += `\n---\n\n`;
return section;
}
function manageResultMarkdownBody(property, commands, checkOnly) {
let markdownBody = `### ${property === 'commandsPreDeploy' ? 'Pre-deployment Actions' : 'Post-deployment Actions'} Results\n\n`;
// Add manual actions section
const isPreDeploy = property === 'commandsPreDeploy';
markdownBody += buildManualActionsSection(commands, isPreDeploy, checkOnly);
// Build markdown table
markdownBody += `| <!-- --> | Label | Type | Status | Details |\n`;
markdownBody += `|:--------:|-------|------|--------|---------|\n`;
for (const cmd of commands) {
const statusIcon = cmd.result?.statusCode === "manual" ?
"👋" :
cmd.result?.statusCode === "success" ? '✅' :
(cmd.result?.statusCode === "failed" && cmd.allowFailure === true) ? '⚠️' :
(cmd.result?.statusCode === "failed") ? '❌' :
cmd.result?.statusCode === "skipped" ? '⚪' : '❓';
const statusCol = `${cmd.result?.statusCode || 'not run'}`;
const detailCol = cmd.result?.statusCode === "skipped" ?
(cmd.result?.skippedReason || '<!-- -->') :
(cmd.result?.statusCode === "failed" && cmd.allowFailure === true) ?
(cmd.result.skippedReason ? `${cmd.result.skippedReason} (Allowed to fail)` : "(Allowed to fail)") :
(cmd.result?.statusCode === "failed" && cmd.result.skippedReason) ?
cmd.result.skippedReason :
"See details below";
const labelCol = cmd.pullRequest ?
`${cmd.label} ([${cmd.pullRequest.idStr || "?"}](${cmd.pullRequest.webUrl || ""}))` :
cmd.label;
markdownBody += `| ${statusIcon} | ${labelCol} | ${cmd.type || 'command'} | ${statusCol} | ${detailCol} |\n`;
}
// Add details in html <detail> blocks, embedded in a root <details> block to avoid markdown rendering issues
const commandsInResults = commands.filter(c => (c.result && c.result.output) || c.type === "manual");
if (commandsInResults.length > 0) {
markdownBody += `\n<details>\n<summary>Expand to see details for each action</summary>\n\n`;
for (const cmd of commands) {
if (cmd.result?.output) {
// Truncate output if too long: Either the last 2000 characters, either the last 50 lines (if they are not more than 2000 characters)
// Indicate when output has been truncated
const maxOutputLength = 2000;
let outputForMarkdown = cmd.result.output;
const outputLines = outputForMarkdown.split('\n');
if (outputForMarkdown.length > maxOutputLength) {
outputForMarkdown = outputForMarkdown.substring(outputForMarkdown.length - maxOutputLength);
}
if (outputLines.length > 50) {
const last50Lines = outputLines.slice(-50).join('\n');
if (last50Lines.length <= maxOutputLength) {
outputForMarkdown = last50Lines;
}
}
if (outputForMarkdown.length < cmd.result.output.length) {
outputForMarkdown = `... (output truncated, total length was ${cmd.result.output.length} characters)\n` + outputForMarkdown;
}
const labelTitle = cmd.pullRequest ?
`${cmd.label} (${cmd.pullRequest.idStr || "?"})` :
cmd.label;
markdownBody += `\n<details id="command-${cmd.id}">\n<summary>${labelTitle}</summary>\n\n`;
markdownBody += '```\n';
markdownBody += outputForMarkdown;
markdownBody += '\n```\n';
markdownBody += '</details>\n';
}
else if (cmd.type === "manual") {
const labelTitle = cmd.pullRequest ?
`${cmd.label} ([${cmd.pullRequest.idStr || "?"}](${cmd.pullRequest.webUrl || ""}))` :
cmd.label;
markdownBody += `\n<details id="command-${cmd.id}">\n<summary>${labelTitle}</summary>\n\n`;
markdownBody += '```\n';
markdownBody += cmd?.parameters?.instructions || "No instructions provided.";
markdownBody += '\n```\n';
markdownBody += '</details>\n';
}
}
markdownBody += `\n</details>\n`;
}
const propertyFormatted = property === 'commandsPreDeploy' ? 'preDeployCommandsResultMarkdownBody' : 'postDeployCommandsResultMarkdownBody';
const prData = {
[propertyFormatted]: markdownBody
};
setPullRequestData(prData);
}
//# sourceMappingURL=prePostCommandUtils.js.map