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
198 lines (191 loc) • 10.4 kB
JavaScript
/* jscpd:ignore-start */
import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import c from 'chalk';
import * as child from 'child_process';
import fs from 'fs-extra';
import * as path from 'path';
import { MetadataUtils } from '../../../../../common/metadata-utils/index.js';
import { ensureGitRepository, execCommand, isMonitoringJob, uxLog } from '../../../../../common/utils/index.js';
import LegacyApi from '../../diagnose/legacyapi.js';
import OrgTestApex from '../../test/apex.js';
import * as util from 'util';
import { PACKAGE_ROOT_DIR } from '../../../../../settings.js';
import { CONSTANTS } from '../../../../../config/index.js';
const exec = util.promisify(child.exec);
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
export default class DxSources extends SfCommand {
static title = 'Retrieve sfdx sources from org';
static description = `
## Command Behavior
**Retrieves Salesforce metadata from an org into a local directory, primarily for backup and monitoring purposes.**
This command is designed to pull metadata from any Salesforce org, providing a snapshot of its configuration. It's particularly useful in monitoring contexts where you need to track changes in an org's metadata over time.
Key functionalities:
- **Metadata Retrieval:** Connects to a target Salesforce org and retrieves metadata based on a specified \`package.xml\`.
- **Managed Package Filtering:** By default, it filters out metadata from managed packages to reduce the volume of retrieved data. This can be overridden with the \`--includemanaged\` flag.
- **Monitoring Integration:** Designed to be used within a monitoring CI/CD job, it performs additional post-retrieval actions like running Apex tests and checking for legacy API usage.
<details markdown="1">
<summary>Technical explanations</summary>
The command's technical implementation involves:
- **Git Repository Check:** Ensures the current directory is a Git repository and initializes it if necessary.
- **\`MetadataUtils.retrieveMetadatas\`:** This utility is the core of the retrieval process. It connects to the Salesforce org, retrieves metadata based on the provided \`package.xml\` and filtering options (e.g., \`filterManagedItems\`), and places the retrieved files in a specified folder.
- **File System Operations:** Uses \`fs-extra\` to manage directories and copy retrieved files to the target folder.
- **Post-Retrieval Actions (for Monitoring Jobs):** If the command detects it's running within a monitoring CI/CD job (\`isMonitoringJob()\`):
- It updates the \`.gitlab-ci.yml\` file if \`AUTO_UPDATE_GITLAB_CI_YML\` is set.
- It converts the retrieved metadata into SFDX format using \`sf project convert mdapi\`.
- It executes \`sf hardis:org:test:apex\` to run Apex tests.
- It executes \`sf hardis:org:diagnose:legacyapi\` to check for legacy API usage.
- It logs warnings if post-actions fail or if the monitoring version is deprecated.
- **Error Handling:** Includes robust error handling for retrieval failures and post-action execution.
</details>
`;
static examples = [
'$ sf hardis:org:retrieve:sources:metadata',
'$ SFDX_RETRIEVE_WAIT_MINUTES=200 sf hardis:org:retrieve:sources:metadata',
];
static flags = {
folder: Flags.string({
char: 'f',
default: '.',
description: messages.getMessage('folder'),
}),
packagexml: Flags.string({
char: 'p',
description: messages.getMessage('packageXml'),
}),
includemanaged: Flags.boolean({
default: false,
description: 'Include items from managed packages',
}),
instanceurl: Flags.string({
char: 'r',
description: messages.getMessage('instanceUrl'),
}),
debug: Flags.boolean({
char: 'd',
default: false,
description: messages.getMessage('debugMode'),
}),
websocket: Flags.string({
description: messages.getMessage('websocket'),
}),
skipauth: Flags.boolean({
description: 'Skip authentication check when a default username is required',
}),
'target-org': requiredOrgFlagWithDeprecations,
};
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = false;
// Trigger notification(s) to MsTeams channel
static triggerNotification = true;
debugMode = false;
/* jscpd:ignore-end */
async run() {
const { flags } = await this.parse(DxSources);
const folder = path.resolve(flags.folder || '.');
const packageXml = path.resolve(flags.packagexml || 'package.xml');
const includeManaged = flags.includemanaged || false;
this.debugMode = flags.debug || false;
// Check required pre-requisites
await ensureGitRepository({ init: true });
const isMonitoring = await isMonitoringJob();
// Retrieve metadatas
let message = '';
try {
const filterManagedItems = includeManaged === false;
await MetadataUtils.retrieveMetadatas(packageXml, folder, false, [], { filterManagedItems: filterManagedItems }, this, flags['target-org'].getUsername(), this.debugMode);
// Copy to destination
await fs.copy(path.join(folder, 'unpackaged'), path.resolve(folder));
// Remove temporary files
await fs.rm(path.join(folder, 'unpackaged'), { recursive: true });
message = `[sfdx-hardis] Successfully retrieved metadatas in ${folder}`;
uxLog("other", this, message);
}
catch (e) {
if (!isMonitoring) {
throw e;
}
message = '[sfdx-hardis] Error retrieving metadatas';
}
// Post actions for monitoring CI job
if (isMonitoring) {
try {
return await this.processPostActions(message, flags);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
uxLog("warning", this, c.yellow('Post actions have failed !'));
}
uxLog("warning", this, c.yellow(c.bold('This version of sfdx-hardis monitoring is deprecated and will not be maintained anymore')));
uxLog("warning", this, c.yellow(c.bold('Switch to new sfdx-hardis monitoring that is enhanced !')));
uxLog("warning", this, c.yellow(c.bold(`Info: ${CONSTANTS.DOC_URL_ROOT}/salesforce-monitoring-home/`)));
}
return { orgId: flags['target-org'].getOrgId(), outputString: message };
}
async processPostActions(message, flags) {
uxLog("action", this, c.cyan('Monitoring repo detected'));
// Update default .gitlab-ci.yml within the monitoring repo
const localGitlabCiFile = path.join(process.cwd(), '.gitlab-ci.yml');
if (fs.existsSync(localGitlabCiFile) && process.env?.AUTO_UPDATE_GITLAB_CI_YML) {
const localGitlabCiContent = await fs.readFile(localGitlabCiFile, 'utf8');
const latestGitlabCiFile = path.join(PACKAGE_ROOT_DIR, 'defaults/monitoring/.gitlab-ci.yml');
const latestGitlabCiContent = await fs.readFile(latestGitlabCiFile, 'utf8');
if (localGitlabCiContent !== latestGitlabCiContent) {
await fs.writeFile(localGitlabCiFile, latestGitlabCiContent);
uxLog("action", this, c.cyan('Updated .gitlab-ci.yml file'));
}
}
// Also trace updates with sfdx sources, for better readability
uxLog("action", this, c.cyan('Convert into sfdx format...'));
if (fs.existsSync('metadatas')) {
// Create sfdx project if not existing yet
if (!fs.existsSync('sfdx-project')) {
const createCommand = 'sf project generate' + ` --name "sfdx-project"`;
uxLog("action", this, c.cyan('Creating sfdx-project...'));
await execCommand(createCommand, this, {
output: true,
fail: true,
debug: this.debugMode,
});
}
// Convert metadatas into sfdx sources
const mdapiConvertCommand = `sf project convert mdapi --root-dir "../metadatas"`;
uxLog("action", this, c.cyan('Converting metadata to source formation into sfdx-project...'));
uxLog("other", this, `[command] ${c.bold(c.grey(mdapiConvertCommand))}`);
const prevCwd = process.cwd();
process.chdir(path.join(process.cwd(), './sfdx-project'));
try {
const convertRes = await exec(mdapiConvertCommand, {
maxBuffer: 10000 * 10000,
});
uxLog("other", this, convertRes.stdout + convertRes.stderr);
}
catch (e) {
uxLog("warning", this, c.yellow('Error while converting metadatas to sources:\n' + e.message));
}
process.chdir(prevCwd);
}
let orgTestRes = null;
let legacyApiRes = null;
const prevExitCode = process.exitCode || 0;
try {
// Run test classes
uxLog("action", this, c.cyan('Running Apex tests...'));
orgTestRes = await new OrgTestApex([], this.config)._run();
// Check usage of Legacy API versions
uxLog("action", this, c.cyan('Running Legacy API Use checks...'));
legacyApiRes = await new LegacyApi([], this.config)._run();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
uxLog("warning", this, c.yellow('Issues found when running Apex tests or Legacy API, please check messages'));
}
process.exitCode = prevExitCode;
// Delete report files
//const reportFiles = await glob("**/hardis-report/**", { cwd: process.cwd() });
//reportFiles.map(async (file) => await fs.remove(file));
return { orgId: flags['target-org'].getOrgId(), outputString: message, orgTestRes, legacyApiRes };
}
}
//# sourceMappingURL=metadata.js.map