From 0ee4dbd452341997272464493ee302ba2f38d8e0 Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Sun, 3 Nov 2024 22:56:35 +0100 Subject: [PATCH] Generate project documentation (#866) * hardis:doc:packagexml2markdown: Generate markdown documentation from a package.xml file * PackageXml2markdown * fix * hardis:org:retrieve:packageconfig: Ignore standard Salesforce packages + doc * project config * Add installed packages * installed packages for monitoring * Update CI/CD home documentation * Call during backup + update README * cspell * [Mega-Linter] Apply linters fixes :) * changelog --------- Co-authored-by: nvuillam --- .github/linters/.cspell.json | 1 + CHANGELOG.md | 8 + docs/salesforce-ci-cd-home.md | 15 +- .../hardis/doc/packagexml2markdown.ts | 67 +++++ src/commands/hardis/doc/project2markdown.ts | 275 ++++++++++++++++++ src/commands/hardis/org/monitor/backup.ts | 8 + .../hardis/org/retrieve/packageconfig.ts | 2 +- src/common/utils/docUtils.ts | 80 +++++ src/common/utils/index.ts | 4 + src/common/utils/orgConfigUtils.ts | 35 ++- src/common/utils/orgUtils.ts | 18 +- src/config/index.ts | 2 +- 12 files changed, 505 insertions(+), 10 deletions(-) create mode 100644 src/commands/hardis/doc/packagexml2markdown.ts create mode 100644 src/commands/hardis/doc/project2markdown.ts create mode 100644 src/common/utils/docUtils.ts diff --git a/.github/linters/.cspell.json b/.github/linters/.cspell.json index c113e7240..b3b67b157 100644 --- a/.github/linters/.cspell.json +++ b/.github/linters/.cspell.json @@ -514,6 +514,7 @@ "includeprofiles", "initial", "initialisation", + "inputfile", "inputfolder", "installable", "installationkey", diff --git a/CHANGELOG.md b/CHANGELOG.md index 468a65577..aea9cfcd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image `hardisgroupcom/sfdx-hardis@beta` +## [5.5.0] 2024-11-03 + +- hardis:doc:packagexml2markdown: Generate markdown documentation from a package.xml file +- hardis:doc:project2markdown: Generate markdown documentation from any SFDX project (CI/CD, monitoring, projects not using sfdx-hardis...) in `docs` folder and add a link in README.md if existing. +- hardis:org:monitor:backup: Call hardis:doc:project2markdown after backup +- hardis:org:retrieve:packageconfig: Ignore standard Salesforce packages +- Update CI/CD home documentation + ## [5.4.1] 2024-11-02 - hardis:org:multi-org-query enhancements diff --git a/docs/salesforce-ci-cd-home.md b/docs/salesforce-ci-cd-home.md index 4ad8c210d..fa3bdac75 100644 --- a/docs/salesforce-ci-cd-home.md +++ b/docs/salesforce-ci-cd-home.md @@ -19,12 +19,15 @@ There are many ways to do DevOps with Salesforce, each of them have their advant You can setup and use a full CI/CD pipeline for your Salesforce projects using sfdx-hardis, with advanced features: -- [Overwrite Management](salesforce-ci-cd-config-overwrite.md) -- [Delta Deployments](salesforce-ci-cd-config-delta-deployment.md) -- [Automated sources cleaning](salesforce-ci-cd-config-cleaning.md) -- [Messaging platforms integrations](salesforce-ci-cd-setup-integrations-home.md) (Slack, Microsoft Teams) - -We provide ready to use CI/CD pipelines for the following Git platforms: +- **Admins** are autonomous to [build their pull requests](https://sfdx-hardis.cloudity.com/salesforce-ci-cd-publish-task/) **with clicks on VsCode Extension, no command lines** +- [Delta Deployments](salesforce-ci-cd-config-delta-deployment.md): Improve performances by deploying only updated metadatas +- [Overwrite Management](salesforce-ci-cd-config-overwrite.md): Define which metadatas will never be overwritten if they are already existing in the target org of a deployment +- [Smart Apex Test Runs](https://sfdx-hardis.cloudity.com/hardis/project/deploy/smart/#smart-deployments-tests): If your Pull Request to a sandbox can not break Apex Tests, just don't run them to improve performances. +- [Automated sources cleaning](salesforce-ci-cd-config-cleaning.md): Clean profiles from attributes existing on permission sets, clean flow positions... +- [Integration with Messaging platforms](salesforce-ci-cd-setup-integrations-home.md): Receive detailed deployment notifications on Slack, Microsoft Teams and Emails +- Integration with **ticketing systems**: [JIRA](https://sfdx-hardis.cloudity.com/salesforce-ci-cd-setup-integration-jira/), [Azure Boards](https://sfdx-hardis.cloudity.com/salesforce-ci-cd-setup-integration-azure-boards/), or [any other tool](https://sfdx-hardis.cloudity.com/salesforce-ci-cd-setup-integration-generic-ticketing/) + +We provide ready to use CI/CD pipelines for the following Git platforms, with results of Deployment simulation jobs as comments on Pull Requests: - [Gitlab](https://github.com/hardisgroupcom/sfdx-hardis/blob/main/defaults/ci/.gitlab-ci.yml) - [Azure](https://github.com/hardisgroupcom/sfdx-hardis/blob/main/defaults/ci/azure-pipelines-checks.yml) diff --git a/src/commands/hardis/doc/packagexml2markdown.ts b/src/commands/hardis/doc/packagexml2markdown.ts new file mode 100644 index 000000000..8f4971fc9 --- /dev/null +++ b/src/commands/hardis/doc/packagexml2markdown.ts @@ -0,0 +1,67 @@ +/* jscpd:ignore-start */ +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core'; +import { AnyJson } from '@salesforce/ts-types'; +import { WebSocketClient } from '../../../common/websocketClient.js'; +import { generatePackageXmlMarkdown } from '../../../common/utils/docUtils.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('sfdx-hardis', 'org'); + +export default class PackageXml2Markdown extends SfCommand { + public static title = 'PackageXml to Markdown'; + + public static description = `Generates a markdown documentation from a package.xml file`; + + public static examples = [ + '$ sf hardis:doc:packagexml2markdown', + '$ sf hardis:doc:packagexml2markdown --inputfile manifest/package-all.xml' + ]; + + public static flags: any = { + inputfile: Flags.string({ + char: 'x', + description: 'Path to package.xml file. If not specified, the command will look in manifest folder', + }), + outputfile: Flags.string({ + char: 'f', + description: 'Force the path and name of output report file. Must end with .md', + }), + 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', + }), + }; + + // Set this to true if your command requires a project workspace; 'requiresProject' is false by default + public static requiresProject = false; + + protected inputFile; + protected outputFile; + protected debugMode = false; + /* jscpd:ignore-end */ + + public async run(): Promise { + const { flags } = await this.parse(PackageXml2Markdown); + this.inputFile = flags.inputfile || null; + this.outputFile = flags.outputfile || null; + this.debugMode = flags.debug || false; + + // Generate markdown for package.xml + this.outputFile = await generatePackageXmlMarkdown(this.inputFile, this.outputFile); + + // Open file in a new VsCode tab if available + WebSocketClient.requestOpenFile(this.outputFile); + + // Return an object to be displayed with --json + return { outputFile: this.outputFile }; + } + +} diff --git a/src/commands/hardis/doc/project2markdown.ts b/src/commands/hardis/doc/project2markdown.ts new file mode 100644 index 000000000..4ae12fdbc --- /dev/null +++ b/src/commands/hardis/doc/project2markdown.ts @@ -0,0 +1,275 @@ +/* jscpd:ignore-start */ +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import fs, { ensureDir } from 'fs-extra'; +import c from "chalk"; +import * as path from "path"; +import sortArray from 'sort-array'; +import { Messages } from '@salesforce/core'; +import { AnyJson } from '@salesforce/ts-types'; +import { WebSocketClient } from '../../../common/websocketClient.js'; +import { generatePackageXmlMarkdown } from '../../../common/utils/docUtils.js'; +import { countPackageXmlItems } from '../../../common/utils/xmlUtils.js'; +import { bool2emoji, execSfdxJson, getCurrentGitBranch, getGitRepoName, uxLog } from '../../../common/utils/index.js'; +import { CONSTANTS, getConfig } from '../../../config/index.js'; +import { listMajorOrgs } from '../../../common/utils/orgConfigUtils.js'; +import { glob } from 'glob'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('sfdx-hardis', 'org'); + +export default class Project2Markdown extends SfCommand { + public static title = 'SFDX Project to Markdown'; + + public static description = `Generates a markdown documentation from a SFDX project`; + + public static examples = [ + '$ sf hardis:doc:project2markdown', + ]; + + public static flags: any = { + 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', + }), + }; + + // Set this to true if your command requires a project workspace; 'requiresProject' is false by default + public static requiresProject = true; + + protected packageXmlCandidates: any[]; + protected outputMarkdownIndexFile = "docs/index.md" + protected mdLines: string[] = []; + protected sfdxHardisConfig: any = {}; + protected outputPackageXmlMarkdownFiles: any[] = []; + protected debugMode = false; + /* jscpd:ignore-end */ + + public async run(): Promise { + const { flags } = await this.parse(Project2Markdown); + this.debugMode = flags.debug || false; + this.packageXmlCandidates = this.listPackageXmlCandidates(); + + if (fs.existsSync("config/.sfdx-hardis.yml")) { + this.sfdxHardisConfig = await getConfig("project"); + this.mdLines.push(...[ + `## ${this.sfdxHardisConfig?.projectName?.toUpperCase() || "SFDX Project"} CI/CD configuration`, + ""]); + this.buildSfdxHardisParams(); + await this.buildMajorBranchesAndOrgs(); + } + else { + const repoName = (await getGitRepoName() || "").replace(".git", ""); + const branchName = await getCurrentGitBranch() || "" + this.mdLines.push(...[ + `## ${repoName}/${branchName} SFDX Project Content`, + "", + ]); + } + + // List SFDX packages and generate a manifest for each of them, except if there is only force-app with a package.xml + await this.manageLocalPackages(); + // List all packageXml files and generate related markdown + await this.generatePackageXmlMarkdown(this.packageXmlCandidates); + await this.writePackagesInIndex(); + + // List managed packages + await this.writeInstalledPackages(); + + // Footer + this.mdLines.push(`_Documentation generated with [sfdx-hardis](${CONSTANTS.DOC_URL_ROOT}) command [\`sf hardis:doc:project2markdown\`](https://sfdx-hardis.cloudity.com/hardis/doc/project2markdown/)_`); + // Write output index file + await fs.ensureDir(path.dirname(this.outputMarkdownIndexFile)); + await fs.writeFile(this.outputMarkdownIndexFile, this.mdLines.join("\n") + "\n"); + uxLog(this, c.green(`Successfully generated doc index at ${this.outputMarkdownIndexFile}`)); + + const readmeFile = path.join(process.cwd(), "README.md"); + if (fs.existsSync(readmeFile)) { + let readme = await fs.readFile(readmeFile, "utf8"); + if (!readme.includes("docs/index.md")) { + readme += "\n\n## Documentation\n\n[Read auto-generated documentation of the SFDX project](docs/index.md)\n"; + await fs.writeFile(readmeFile, readme); + uxLog(this, c.green(`Updated README.md to add link to docs/index.md`)); + } + } + + + // Open file in a new VsCode tab if available + WebSocketClient.requestOpenFile(this.outputMarkdownIndexFile); + + return { outputPackageXmlMarkdownFiles: this.outputPackageXmlMarkdownFiles }; + } + + private async writeInstalledPackages() { + // CI/CD context + const packages = this.sfdxHardisConfig.installedPackages || []; + // Monitoring context + const packageFolder = path.join(process.cwd(), 'installedPackages'); + if (packages.length === 0 && fs.existsSync(packageFolder)) { + const findManagedPattern = "**/*.json"; + const matchingPackageFiles = await glob(findManagedPattern, { cwd: packageFolder }); + for (const packageFile of matchingPackageFiles) { + const packageFileFull = path.join(packageFolder, packageFile); + if (!fs.existsSync(packageFileFull)) { + continue; + } + const pckg = await fs.readJSON(packageFileFull); + packages.push(pckg); + } + } + // Write packages table + if (packages && packages.length > 0) { + this.mdLines.push(...[ + "## Installed packages", + "", + "| Name | Namespace | Version | Version Name |", + "| :---- | :-------- | :------ | :----------: | " + ]); + for (const pckg of sortArray(packages, { by: ['SubscriberPackageNamespace', 'SubscriberPackageName'], order: ['asc', 'asc'] }) as any[]) { + this.mdLines.push(...[ + `| ${pckg.SubscriberPackageName} | ${pckg.SubscriberPackageNamespace || ""} | [${pckg.SubscriberPackageVersionNumber}](https://test.salesforce.com/packaging/installPackage.apexp?p0=${pckg.SubscriberPackageVersionId}) | ${pckg.SubscriberPackageVersionName} |` + ]); + } + this.mdLines.push(""); + this.mdLines.push("___"); + this.mdLines.push(""); + } + } + + private buildSfdxHardisParams() { + this.mdLines.push(...[ + "| Sfdx-hardis Parameter | Value | Description & doc link |", + "| :--------- | :---- | :---------- |" + ]); + const installPackagesDuringCheckDeploy = this.sfdxHardisConfig?.installPackagesDuringCheckDeploy ?? false; + this.mdLines.push(`| installPackagesDuringCheckDeploy | ${bool2emoji(installPackagesDuringCheckDeploy)} ${installPackagesDuringCheckDeploy} | [Install 1GP & 2GP packages during deployment check CI/CD job](https://sfdx-hardis.cloudity.com/hardis/project/deploy/smart/#packages-installation) |`); + const useDeltaDeployment = this.sfdxHardisConfig?.useDeltaDeployment ?? false; + this.mdLines.push(`| useDeltaDeployment | ${bool2emoji(useDeltaDeployment)} ${useDeltaDeployment} | [Deploys only updated metadatas , only when a MR/PR is from a minor branch to a major branch](https://sfdx-hardis.cloudity.com/salesforce-ci-cd-config-delta-deployment/#delta-mode) |`); + const useSmartDeploymentTests = this.sfdxHardisConfig?.useSmartDeploymentTests ?? false; + this.mdLines.push(`| useSmartDeploymentTests | ${bool2emoji(useSmartDeploymentTests)} ${useSmartDeploymentTests} | [Skip Apex test cases if delta metadatas can not impact them, only when a MR/PR is from a minor branch to a major branch](https://sfdx-hardis.cloudity.com/hardis/project/deploy/smart/#smart-deployments-tests) |`); + this.mdLines.push(""); + this.mdLines.push("___"); + this.mdLines.push(""); + } + + private async buildMajorBranchesAndOrgs() { + const majorOrgs = await listMajorOrgs(); + if (majorOrgs.length > 0) { + this.mdLines.push(...[ + "## Major branches and orgs", + "", + "| Git branch | Salesforce Org | Deployment Username |", + "| :--------- | :------------- | :------------------ |" + ]); + for (const majorOrg of majorOrgs) { + const majorOrgLine = `| ${majorOrg.branchName} | ${majorOrg.instanceUrl} | ${majorOrg.targetUsername} |`; + this.mdLines.push(majorOrgLine); + } + this.mdLines.push(""); + this.mdLines.push("___"); + this.mdLines.push(""); + } + } + + private async manageLocalPackages() { + const packageDirs = this.project?.getPackageDirectories(); + if (!(packageDirs?.length === 1 && packageDirs[0].name === "force-app" && fs.existsSync("manifest/package.xml"))) { + for (const packageDir of packageDirs || []) { + // Generate manifest from package folder + const packageManifestFile = path.join("manifest", packageDir.name + '-package.xml'); + await ensureDir(path.dirname(packageManifestFile)); + await execSfdxJson("sf project generate manifest" + + ` --source-dir ${packageDir.path}` + + ` --name ${packageManifestFile}`, this, + { + fail: true, + output: true, + debug: this.debugMode, + } + ); + // Add package in available packages list + this.packageXmlCandidates.push({ + path: packageManifestFile, + name: packageDir.name, + description: `Package.xml generated from content of SFDX package ${packageDir.name} (folder ${packageDir.path})` + }); + } + } + } + + private async writePackagesInIndex() { + this.mdLines.push(...[ + "## Package XML files", + "", + "| Package name | Description |", + "| :----------- | :---------- |" + ]); + + for (const outputPackageXmlDef of this.outputPackageXmlMarkdownFiles) { + const metadataNb = await countPackageXmlItems(outputPackageXmlDef.path); + const packageMdFile = path.basename(outputPackageXmlDef.path) + ".md"; + const label = outputPackageXmlDef.name ? `Package folder: ${outputPackageXmlDef.name}` : path.basename(outputPackageXmlDef.path); + const packageTableLine = `| [${label}](${packageMdFile}) (${metadataNb}) | ${outputPackageXmlDef.description} |`; + this.mdLines.push(packageTableLine); + } + this.mdLines.push(""); + this.mdLines.push("___"); + this.mdLines.push(""); + } + + private async generatePackageXmlMarkdown(packageXmlCandidates) { + // Generate packageXml doc when found + for (const packageXmlCandidate of packageXmlCandidates) { + if (fs.existsSync(packageXmlCandidate.path)) { + // Generate markdown for package.xml + const packageMarkdownFile = await generatePackageXmlMarkdown(packageXmlCandidate.path, null, packageXmlCandidate); + // Open file in a new VsCode tab if available + WebSocketClient.requestOpenFile(packageMarkdownFile); + packageXmlCandidate.markdownFile = packageMarkdownFile; + this.outputPackageXmlMarkdownFiles.push(packageXmlCandidate); + } + } + } + + private listPackageXmlCandidates(): any[] { + return [ + // CI/CD package files + { + path: "manifest/package.xml", + description: "Contains all deployable metadatas of the SFDX project" + }, + { + path: "manifest/packageDeployOnce.xml", + description: "Contains all metadatas that will never be overwritten during deployment if they are already existing in the target org" + }, + { + path: "manifest/package-no-overwrite.xml", + description: "Contains all metadatas that will never be overwritten during deployment if they are already existing in the target org" + }, + { + path: "manifest/destructiveChanges.xml", + description: "Contains all metadatas that will be deleted during deployment, in case they are existing in the target org" + }, + // Monitoring package files + { + path: "manifest/package-all-org-items.xml", + description: "Contains the entire list of metadatas that are present in the monitored orgs (not all of them are in the git backup)" + }, + { + path: "manifest/package-backup-items.xml", + description: "Contains the list of metadatas that are in the git backup" + }, + { + path: "manifest/package-skip-items.xml", + description: "Contains the list of metadatas that are excluded from the backup.
Other metadata types might be skipped using environment variable MONITORING_BACKUP_SKIP_METADATA_TYPES" + }, + ]; + } + +} diff --git a/src/commands/hardis/org/monitor/backup.ts b/src/commands/hardis/org/monitor/backup.ts index 19965a894..011ccf4c8 100644 --- a/src/commands/hardis/org/monitor/backup.ts +++ b/src/commands/hardis/org/monitor/backup.ts @@ -14,6 +14,7 @@ import { MessageAttachment } from '@slack/web-api'; import { getNotificationButtons, getOrgMarkdown, getSeverityIcon } from '../../../../common/utils/notifUtils.js'; import { generateCsvFile, generateReportPath } from '../../../../common/utils/filesUtils.js'; import { countPackageXmlItems, parsePackageXmlFile, writePackageXmlFile } from '../../../../common/utils/xmlUtils.js'; +import Project2Markdown from '../../doc/project2markdown.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); @@ -258,6 +259,13 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales }, }); + // Run project documentation generation + try { + await Project2Markdown.run(); + } catch (e: any) { + uxLog(this, c.yellow("Error while generating project documentation " + e.message)); + } + return { outputString: 'BackUp processed on org ' + flags['target-org'].getConnection().instanceUrl }; } diff --git a/src/commands/hardis/org/retrieve/packageconfig.ts b/src/commands/hardis/org/retrieve/packageconfig.ts index 47f82e8d4..75c369480 100644 --- a/src/commands/hardis/org/retrieve/packageconfig.ts +++ b/src/commands/hardis/org/retrieve/packageconfig.ts @@ -58,7 +58,7 @@ export default class RetrievePackageConfig extends SfCommand { message: c.cyanBright('Do you want to update your project configuration with this list of packages ?'), }); if (updateConfigRes.value === true) { - await managePackageConfig(installedPackages, installedPackages); + await managePackageConfig(installedPackages, installedPackages, true); } const message = `[sfdx-hardis] Successfully retrieved package config`; diff --git a/src/common/utils/docUtils.ts b/src/common/utils/docUtils.ts new file mode 100644 index 000000000..b04d01f09 --- /dev/null +++ b/src/common/utils/docUtils.ts @@ -0,0 +1,80 @@ +import * as path from "path"; +import c from 'chalk'; +import fs from 'fs-extra'; +import { uxLog } from "./index.js"; +import { countPackageXmlItems, parsePackageXmlFile } from "./xmlUtils.js"; +import { CONSTANTS } from "../../config/index.js"; +import { SfError } from "@salesforce/core"; + +export async function generatePackageXmlMarkdown(inputFile: string|null, outputFile: string|null= null, packageXmlDefinition: any = null) { + // Find packageXml to parse if not defined + if (inputFile == null) { + inputFile = path.join(process.cwd(), "manifest", "package.xml"); + if (!fs.existsSync(inputFile)) { + throw new SfError("No package.xml found. You need to send the path to a package.xml file in --inputfile option"); + } + } + // Build output file if not defined + if (outputFile == null) { + const packageXmlFileName = path.basename(inputFile); + outputFile = path.join(process.cwd(), "docs", `${packageXmlFileName}.md`); + } + await fs.ensureDir(path.dirname(outputFile)); + + uxLog(this, c.cyan(this, `Generating markdown doc from ${inputFile} to ${outputFile}...`)); + + // Read content + const packageXmlContent = await parsePackageXmlFile(inputFile); + const metadataTypes = Object.keys(packageXmlContent); + metadataTypes.sort(); + const nbItems = await countPackageXmlItems(inputFile); + + const mdLines: string[] = [] + +if (packageXmlDefinition && packageXmlDefinition.description) { + // Header + mdLines.push(...[ + `## Content of ${path.basename(inputFile)}`, + '', + packageXmlDefinition.description, + '', + `Metadatas: ${nbItems}`, + '' + ]); +} +else { + // Header + mdLines.push(...[ + `## Content of ${path.basename(inputFile)}`, + '', + `Metadatas: ${nbItems}`, + '' + ]); +} + + // Generate package.xml markdown + for (const metadataType of metadataTypes) { + const members = packageXmlContent[metadataType]; + members.sort(); + const memberLengthLabel = members.length === 1 && members[0] === "*" ? "*" : members.length; + mdLines.push(`
${metadataType} (${memberLengthLabel})`); + for (const member of members) { + const memberLabel = member === "*" ? "ALL (wildcard *)" : member; + mdLines.push(` • ${memberLabel}
`); + } + mdLines.push("
"); + mdLines.push(""); + mdLines.push("
"); + } + mdLines.push(""); + + // Footer + mdLines.push(`_Documentation generated with [sfdx-hardis](${CONSTANTS.DOC_URL_ROOT})_`); + + // Write output file + await fs.writeFile(outputFile, mdLines.join("\n") + "\n"); + + uxLog(this, c.green(`Successfully generated ${path.basename(inputFile)} documentation into ${outputFile}`)); + + return outputFile; +} \ No newline at end of file diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 563fb0c1f..60051f295 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1040,6 +1040,10 @@ export function uxLog(commandThis: any, text: string) { } } +export function bool2emoji(bool: boolean): string { + return bool ? "✅" : "⬜" +} + // Caching methods const SFDX_LOCAL_FOLDER = '/root/.sfdx'; const TMP_COPY_FOLDER = '.cache/sfdx-hardis/.sfdx'; diff --git a/src/common/utils/orgConfigUtils.ts b/src/common/utils/orgConfigUtils.ts index c6066317f..468e872f0 100644 --- a/src/common/utils/orgConfigUtils.ts +++ b/src/common/utils/orgConfigUtils.ts @@ -2,6 +2,7 @@ import c from 'chalk'; import fs from 'fs-extra'; import { glob } from 'glob'; import puppeteer, { Browser } from 'puppeteer-core'; +import sortArray from 'sort-array'; import * as chromeLauncher from 'chrome-launcher'; import * as yaml from 'js-yaml'; import { uxLog } from './index.js'; @@ -171,7 +172,39 @@ export async function listMajorOrgs() { } majorOrgs.push(props); } - return majorOrgs; + // Clumsy sorting but not other way :/ + const majorOrgsSorted: any = []; + // Main + for (const majorOrg of majorOrgs) { + if (majorOrg?.branchName?.toLowerCase().startsWith("main") || majorOrg?.branchName?.toLowerCase().startsWith("prod")) { + majorOrgsSorted.push(majorOrg); + } + } + // Preprod + for (const majorOrg of majorOrgs) { + if (majorOrg?.branchName?.toLowerCase().startsWith("preprod") || majorOrg?.branchName?.toLowerCase().startsWith("staging")) { + majorOrgsSorted.push(majorOrg); + } + } + // uat + for (const majorOrg of majorOrgs) { + if (majorOrg?.branchName?.toLowerCase().startsWith("uat") || majorOrg?.branchName?.toLowerCase().startsWith("recette")) { + majorOrgsSorted.push(majorOrg); + } + } + // integration + for (const majorOrg of majorOrgs) { + if (majorOrg?.branchName?.toLowerCase().startsWith("integ")) { + majorOrgsSorted.push(majorOrg); + } + } + // Add remaining major branches + for (const majorOrg of sortArray(majorOrgs, { by: ['branchName'], order: ['asc'] }) as any[]) { + if (majorOrgsSorted.filter(org => org.branchName === majorOrg.branchName).length === 0) { + majorOrgsSorted.push(majorOrg); + } + } + return majorOrgsSorted; } export async function checkSfdxHardisTraceAvailable(conn: Connection) { diff --git a/src/common/utils/orgUtils.ts b/src/common/utils/orgUtils.ts index 0112cd7a7..c30c684c5 100644 --- a/src/common/utils/orgUtils.ts +++ b/src/common/utils/orgUtils.ts @@ -356,11 +356,12 @@ export async function authenticateWithSfdxUrlStore(org: any) { } // Add package installation to project .sfdx-hardis.yml -export async function managePackageConfig(installedPackages, packagesToInstallCompleted) { +export async function managePackageConfig(installedPackages, packagesToInstallCompleted, filterStandard = false) { const config = await getConfig('project'); let projectPackages = config.installedPackages || []; let updated = false; for (const installedPackage of installedPackages) { + // Filter standard packages const matchInstalled = packagesToInstallCompleted.filter( (pckg) => pckg.SubscriberPackageId === installedPackage.SubscriberPackageId ); @@ -389,6 +390,21 @@ export async function managePackageConfig(installedPackages, packagesToInstallCo ); updated = true; } else if (matchInstalled.length > 0 && matchLocal.length === 0) { + // Check if not filtered package + if (filterStandard && + ["Salesforce Connected Apps", + "Salesforce Mobile Apps", + "Trail Tracker", + "SalesforceA Connected Apps", + "Salesforce Adoption Dashboards", + "Salesforce.com CRM Dashboards", + "Sales Insights" + ].includes(installedPackage.SubscriberPackageName) + ) { + uxLog(this, c.grey(`Skip ${installedPackage.SubscriberPackageName} as it is a Salesforce standard package`)) + continue; + } + // Request user about automatic installation during scratch orgs and deployments const installResponse = await prompts({ type: 'select', diff --git a/src/config/index.ts b/src/config/index.ts index 416effbea..7fdb12d14 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -73,7 +73,7 @@ async function getBranchConfigFiles() { return branchConfigFiles; } -export const getConfig = async (layer = 'user'): Promise => { +export const getConfig = async (layer: "project" | "branch" | "user" = 'user'): Promise => { const defaultConfig = await loadFromConfigFile(projectConfigFiles); if (layer === 'project') { return defaultConfig;