From 32d4b435d34bc2d60494448f98b12959da2cfc86 Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Thu, 9 Jan 2025 23:03:39 +0100 Subject: [PATCH 1/2] Generate a file hardis-report/apex-coverage-results.json with Apex code coverage details - Generate a file **hardis-report/apex-coverage-results.json** with Apex code coverage details for the following commands: - [hardis:project:deploy:smart](https://sfdx-hardis.cloudity.com/hardis/project/deploy/smart/) (only if `COVERAGE_FORMATTER_JSON=true` environment varriable is defined) - [hardis:org:test:apex](https://sfdx-hardis.cloudity.com/hardis/org/test/apex/) (always) - [SF Cli deployment wrapper commands](https://sfdx-hardis.cloudity.com/salesforce-deployment-assistant-setup/#using-custom-cicd-pipeline) --- CHANGELOG.md | 7 +++++ src/commands/hardis/org/test/apex.ts | 3 ++ src/common/utils/deployUtils.ts | 41 ++++++++++++++++++++++++++-- src/common/utils/wrapUtils.ts | 3 ++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70d2c797..3a6275b6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image `hardisgroupcom/sfdx-hardis@beta` +## [5.14.1] 2025-01-09 + +- Generate a file **hardis-report/apex-coverage-results.json** with Apex code coverage details for the following commands: + - [hardis:project:deploy:smart](https://sfdx-hardis.cloudity.com/hardis/project/deploy/smart/) (only if `COVERAGE_FORMATTER_JSON=true` environment variable is defined) + - [hardis:org:test:apex](https://sfdx-hardis.cloudity.com/hardis/org/test/apex/) (always) + - [SF Cli deployment wrapper commands](https://sfdx-hardis.cloudity.com/salesforce-deployment-assistant-setup/#using-custom-cicd-pipeline) + ## [5.14.0] 2025-01-09 - Add ability to replace ApiVersion on specific Metadata Types file using `sf hardis:project:audit:apiversion` diff --git a/src/commands/hardis/org/test/apex.ts b/src/commands/hardis/org/test/apex.ts index 12bccf74a..1247bda91 100644 --- a/src/commands/hardis/org/test/apex.ts +++ b/src/commands/hardis/org/test/apex.ts @@ -8,6 +8,7 @@ import { execCommand, extractRegexMatchesMultipleGroups, uxLog } from '../../../ import { getNotificationButtons, getOrgMarkdown } from '../../../../common/utils/notifUtils.js'; import { CONSTANTS, getConfig, getReportDirectory } from '../../../../config/index.js'; import { NotifProvider, NotifSeverity } from '../../../../common/notifProvider/index.js'; +import { generateApexCoverageOutputFile } from '../../../../common/utils/deployUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); @@ -145,6 +146,7 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales // Parse outcome value from logs with Regex this.testRunOutcome = (/Outcome *(.*) */.exec(execCommandRes.stdout + execCommandRes.stderr) || '')[1].trim(); this.testRunOutputString = execCommandRes.stdout + execCommandRes.stderr; + await generateApexCoverageOutputFile(this.testRunOutputString || ''); } catch (e) { // No Apex in the org if ( @@ -156,6 +158,7 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales // Failing Apex tests this.testRunOutputString = (e as Error).message; this.testRunOutcome = 'Failed'; + await generateApexCoverageOutputFile(e); } } } diff --git a/src/common/utils/deployUtils.ts b/src/common/utils/deployUtils.ts index 642f4ac40..24a16bf99 100644 --- a/src/common/utils/deployUtils.ts +++ b/src/common/utils/deployUtils.ts @@ -19,7 +19,7 @@ import { replaceJsonInString, uxLog, } from './index.js'; -import { CONSTANTS, getConfig, setConfig } from '../../config/index.js'; +import { CONSTANTS, getConfig, getReportDirectory, setConfig } from '../../config/index.js'; import { GitProvider } from '../gitProvider/index.js'; import { deployCodeCoverageToMarkdown } from '../gitProvider/utilsMarkdown.js'; import { MetadataUtils } from '../metadata-utils/index.js'; @@ -282,6 +282,7 @@ export async function smartDeploy( (options.postDestructiveChanges ? ` --post-destructive-changes ${options.postDestructiveChanges}` : '') + (options.targetUsername ? ` -o ${options.targetUsername}` : '') + (testlevel === 'NoTestRun' || branchConfig?.skipCodeCoverage === true ? '' : ' --coverage-formatters json-summary') + + ((testlevel === 'NoTestRun' || branchConfig?.skipCodeCoverage === true) && process.env?.COVERAGE_FORMATTER_JSON === "true" ? '' : ' --coverage-formatters json') + (debugMode ? ' --verbose' : '') + ` --wait ${process.env.SFDX_DEPLOY_WAIT_MINUTES || '120'}` + (process.env.SFDX_DEPLOY_DEV_DEBUG ? ' --dev-debug' : '') + @@ -289,17 +290,22 @@ export async function smartDeploy( let deployRes; try { deployRes = await execCommand(deployCommand, commandThis, { - output: true, + output: false, debug: debugMode, fail: true, retry: deployment.retry || null, }); - } catch (e) { + if (deployRes.status === 0) { + uxLog(commandThis, c.grey(shortenLogLines(deployRes.stdout + deployRes.stderr))); + } + } catch (e: any) { + await generateApexCoverageOutputFile(e); deployRes = await handleDeployError(e, check, branchConfig, commandThis, options, deployment); } if (typeof deployRes === 'object') { deployRes.stdout = JSON.stringify(deployRes); } + await generateApexCoverageOutputFile(deployRes.stdout + deployRes.stderr || ''); // Set deployment id await getDeploymentId(deployRes.stdout + deployRes.stderr || ''); @@ -1311,3 +1317,32 @@ async function updatePullRequestResultCoverage( } globalThis.pullRequestData = Object.assign(globalThis.pullRequestData || {}, prDataCodeCoverage); } + +export async function generateApexCoverageOutputFile(commandOutput: string | any): Promise { + try { + const outputString = + typeof commandOutput === 'string' ? commandOutput : + typeof commandOutput === 'object' && commandOutput.stdout && commandOutput.stderr ? commandOutput.stdout + commandOutput.stderr : + typeof commandOutput === 'object' && commandOutput.stdout ? commandOutput.stdout : + typeof commandOutput === 'object' && commandOutput.stderr ? commandOutput.stderr : + JSON.stringify(commandOutput); + const reportDir = await getReportDirectory(); + const coverageFileName = path.join(reportDir, "apex-coverage-results.json"); + let coverageObject: any = null; + const jsonLog = findJsonInString(outputString); + // Output from sf project deploy start or similar: extract from JSON + if (jsonLog && jsonLog?.result?.details?.runTestResult?.codeCoverage?.length > 0) { + coverageObject = jsonLog.result.details.runTestResult.codeCoverage; + } + // Output from apex run tests: get locally generated file + else if (fs.existsSync(path.join(reportDir, "test-result-codecoverage.json"))) { + coverageObject = JSON.parse(fs.readFileSync(path.join(reportDir, "test-result-codecoverage.json"), 'utf8')); + } + if (coverageObject !== null) { + await fs.writeFile(coverageFileName, JSON.stringify(coverageObject, null, 2), 'utf8'); + uxLog(this, c.cyan(`Written Apex coverage results in file ${coverageFileName}`)); + } + } catch (e: any) { + uxLog(this, c.red(`Error while generating Apex coverage output file: ${e.message}`)); + } +} \ No newline at end of file diff --git a/src/common/utils/wrapUtils.ts b/src/common/utils/wrapUtils.ts index 6f5aef402..762fe1e50 100644 --- a/src/common/utils/wrapUtils.ts +++ b/src/common/utils/wrapUtils.ts @@ -2,6 +2,7 @@ import { SfCommand } from "@salesforce/sf-plugins-core"; import c from "chalk"; import { execCommand, uxLog } from "./index.js"; import { analyzeDeployErrorLogs } from "./deployTips.js"; +import { generateApexCoverageOutputFile } from "./deployUtils.js"; export async function wrapSfdxCoreCommand(commandBase: string, argv: string[], commandThis: SfCommand, debug = false): Promise { const endArgs = [...argv].map((arg) => { @@ -44,7 +45,9 @@ export async function wrapSfdxCoreCommand(commandBase: string, argv: string[], c fail: true, }); process.exitCode = 0; + await generateApexCoverageOutputFile(deployRes); } catch (e) { + await generateApexCoverageOutputFile(e); // Add deployment tips in error logs const { errLog } = await analyzeDeployErrorLogs((e as any).stdout + (e as any).stderr, true, { check: endArgs.includes("--checkonly") }); uxLog(commandThis, c.red(c.bold("Sadly there has been error(s)"))); From 4919af127c70994ebd08dadadc6c28e97ae1aa66 Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Thu, 9 Jan 2025 23:23:18 +0100 Subject: [PATCH 2/2] - Do not display command output if execCommand has been called with `output: false` --- CHANGELOG.md | 1 + src/common/utils/index.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6275b6c..dca3102a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image - [hardis:project:deploy:smart](https://sfdx-hardis.cloudity.com/hardis/project/deploy/smart/) (only if `COVERAGE_FORMATTER_JSON=true` environment variable is defined) - [hardis:org:test:apex](https://sfdx-hardis.cloudity.com/hardis/org/test/apex/) (always) - [SF Cli deployment wrapper commands](https://sfdx-hardis.cloudity.com/salesforce-deployment-assistant-setup/#using-custom-cicd-pipeline) +- Do not display command output if execCommand has been called with `output: false` ## [5.14.0] 2025-01-09 diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index ef81c0103..a483bafc7 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -648,7 +648,9 @@ export async function execCommand( // Display error in red if not json if (!command.includes('--json') || options.fail) { const strErr = shortenLogLines(`${(e as any).stdout}\n${(e as any).stderr}`); - console.error(c.red(strErr)); + if (output) { + console.error(c.red(strErr)); + } (e as Error).message = (e as Error).message += '\n' + strErr; // Manage retry if requested if (options.retry != null) {