From 6c0b1a4be520565e1d31771e67832c4c39dcda35 Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Sun, 5 Jan 2025 23:16:31 +0100 Subject: [PATCH] Add branch & orgs strategy MermaidJS diagram in documentation --- CHANGELOG.md | 4 + config/sfdx-hardis.jsonschema.json | 17 ++ .../sfdx-hardis-json-schema-parameters.html | 40 ++- src/commands/hardis/doc/project2markdown.ts | 10 +- .../utils/branchStrategyMermaidBuilder.ts | 241 ++++++++++++++++++ src/common/utils/orgConfigUtils.ts | 74 +++++- 6 files changed, 379 insertions(+), 7 deletions(-) create mode 100644 src/common/utils/branchStrategyMermaidBuilder.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b284904..0f54b120c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image `hardisgroupcom/sfdx-hardis@beta` +## [5.13.0] 2025-01-05 + +- [hardis:doc:project2markdown](https://sfdx-hardis.cloudity.com/hardis/doc/project2markdown/) Add branch & orgs strategy MermaidJS diagram in documentation + ## [5.12.0] 2025-01-04 - New command [hardis:doc:mkdocs-to-salesforce](https://sfdx-hardis.cloudity.com/hardis/doc/mkdocs-to-salesforce/) to generate static HTML doc and host it in a Static Resource and a VisualForce page diff --git a/config/sfdx-hardis.jsonschema.json b/config/sfdx-hardis.jsonschema.json index 4f738c747..483249d07 100644 --- a/config/sfdx-hardis.jsonschema.json +++ b/config/sfdx-hardis.jsonschema.json @@ -991,6 +991,23 @@ "title": "Metadata to retrofit", "type": "array" }, + "mergeTargets": { + "$id": "#/properties/mergeTargets", + "description": "In branch-scoped config file, declares the list of branches that the current one can have as merge target. For example, integration will have mergeTargets [uat]", + "examples": [ + [ + "preprod" + ], + [ + "integration" + ] + ], + "items": { + "type": "string" + }, + "title": "Merge target branches", + "type": "array" + }, "monitoringCommands": { "$id": "#/properties/monitoringCommands", "description": "List of monitoring commands to run with command hardis:org:monitor:all", diff --git a/docs/schema/sfdx-hardis-json-schema-parameters.html b/docs/schema/sfdx-hardis-json-schema-parameters.html index 2db1ff115..e3e404816 100644 --- a/docs/schema/sfdx-hardis-json-schema-parameters.html +++ b/docs/schema/sfdx-hardis-json-schema-parameters.html @@ -3061,6 +3061,44 @@

+
+
+
+

+ +

+
+ +
+
+ +

Doc: Deploy to Salesforce Org

Type: boolean Default: false
+

Automatically deploy MkDocs HTML documentation from CI/CD Workflows to Salesforce org as static resource

+
+ + + + + +
+
Example:
+
true
+
+
+
+
+
+
@@ -5825,6 +5863,6 @@

\ No newline at end of file diff --git a/src/commands/hardis/doc/project2markdown.ts b/src/commands/hardis/doc/project2markdown.ts index e02958815..5577f265d 100644 --- a/src/commands/hardis/doc/project2markdown.ts +++ b/src/commands/hardis/doc/project2markdown.ts @@ -17,6 +17,7 @@ import { listFlowFiles } from '../../../common/utils/projectUtils.js'; import { generateFlowMarkdownFile, generateHistoryDiffMarkdown, generateMarkdownFileWithMermaid } from '../../../common/utils/mermaidUtils.js'; import { MetadataUtils } from '../../../common/metadata-utils/index.js'; import { PACKAGE_ROOT_DIR } from '../../../settings.js'; +import { BranchStrategyMermaidBuilder } from '../../../common/utils/branchStrategyMermaidBuilder.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); @@ -417,8 +418,15 @@ ${Project2Markdown.htmlInstructions} const branchesOrgsLines: string[] = []; const majorOrgs = await listMajorOrgs(); if (majorOrgs.length > 0) { + + branchesOrgsLines.push(...[ + "## Branches & Orgs strategy", + "", + ]); + const mermaidLines = new BranchStrategyMermaidBuilder(majorOrgs).build({ withMermaidTag: true, format: "list" }); + branchesOrgsLines.push(...mermaidLines); + branchesOrgsLines.push(...[ - "## Major branches and orgs", "", "| Git branch | Salesforce Org | Deployment Username |", "| :--------- | :------------- | :------------------ |" diff --git a/src/common/utils/branchStrategyMermaidBuilder.ts b/src/common/utils/branchStrategyMermaidBuilder.ts new file mode 100644 index 000000000..68329ba4d --- /dev/null +++ b/src/common/utils/branchStrategyMermaidBuilder.ts @@ -0,0 +1,241 @@ +import sortArray from "sort-array"; +import { prettifyFieldName } from "./flowVisualiser/nodeFormatUtils.js"; +import { isIntegration, isPreprod, isProduction } from "./orgConfigUtils.js"; + + +export class BranchStrategyMermaidBuilder { + private branchesAndOrgs: any[]; + private gitBranches: any[]; + private salesforceOrgs: any[] = []; + private gitLinks: any[] = []; + private deployLinks: any[] = []; + private sbDevLinks: any[] = []; + private retrofitLinks: any[] = []; + private mermaidLines: string[] = []; + + constructor(branchesAndOrgs: any[]) { + this.branchesAndOrgs = branchesAndOrgs; + } + + public build(options: { format: "list" | "string", withMermaidTag: boolean }): string | string[] { + this.listGitBranchesAndLinks(); + this.listSalesforceOrgsAndLinks(); + this.generateMermaidLines(); + if (options.withMermaidTag) { + this.mermaidLines.unshift("```mermaid"); + this.mermaidLines.push("```"); + } + return options.format === "list" ? this.mermaidLines : this.mermaidLines.join("\n"); + } + + private listGitBranchesAndLinks(): void { + const branchesWhoAreMergeTargets: string[] = []; + const branchesMergingInPreprod: string[] = []; + this.gitBranches = this.branchesAndOrgs.map((branchAndOrg) => { + const nodeName = branchAndOrg.branchName + "Branch" + for (const mergeTarget of branchAndOrg.mergeTargets || []) { + if (!branchesWhoAreMergeTargets.includes(mergeTarget)) { + branchesWhoAreMergeTargets.push(mergeTarget); + } + if (isPreprod(mergeTarget)) { + branchesMergingInPreprod.push(branchAndOrg.branchName); + } + this.gitLinks.push({ + source: nodeName, + target: mergeTarget + "Branch", + type: "gitMerge", + label: "Merge" + }); + } + return { + name: branchAndOrg.branchName, + nodeName: nodeName, + label: branchAndOrg.branchName, + class: isProduction(branchAndOrg.branchName) ? "gitMain" : "gitMajor", + level: branchAndOrg.level + }; + }); + // Create feature branches for branches that are not merge targets + const noMergeTargetBranchAndOrg = this.branchesAndOrgs.filter((branchAndOrg) => !branchesWhoAreMergeTargets.includes(branchAndOrg.branchName)); + if (branchesMergingInPreprod.length < 2 && !noMergeTargetBranchAndOrg.find((branchAndOrg) => isPreprod(branchAndOrg.branchName))) { + noMergeTargetBranchAndOrg.push(this.branchesAndOrgs.find((branchAndOrg) => isPreprod(branchAndOrg.branchName))); + } + for (const branchAndOrg of noMergeTargetBranchAndOrg) { + const nameBase = isPreprod(branchAndOrg.branchName) ? "hotfix" : "feature"; + const level = branchAndOrg.level - 1 + const nameBase1 = nameBase + "1"; + const nodeName1 = nameBase + "Branch" + "1" + this.gitBranches.push({ + name: nameBase1, + nodeName: nodeName1, + label: nameBase1, + class: "gitFeature", + level: level + }); + this.gitLinks.push({ + source: nodeName1, + target: this.gitBranches.find((gitBranch) => gitBranch.name === branchAndOrg.branchName)?.nodeName || "ERROR", + type: "gitMerge", + label: "Merge" + }); + const nameBase2 = nameBase + "2"; + const nodeName2 = nameBase + "Branch" + "2" + this.gitBranches.push({ + name: nameBase2, + nodeName: nodeName2, + label: nameBase2, + class: "gitFeature", + level: level + }); + this.gitLinks.push({ + source: nodeName2, + target: this.gitBranches.find((gitBranch) => gitBranch.name === branchAndOrg.branchName)?.nodeName || "ERROR", + type: "gitMerge", + label: "Merge", + level: level + }); + } + const mainBranch = this.branchesAndOrgs.find((branchAndOrg) => isProduction(branchAndOrg.branchName)); + const preprodBranch = this.branchesAndOrgs.find((branchAndOrg) => isPreprod(branchAndOrg.branchName)); + const integrationBranch = this.branchesAndOrgs.find((branchAndOrg) => isIntegration(branchAndOrg.branchName)); + if (mainBranch && preprodBranch && integrationBranch) { + this.retrofitLinks.push({ + source: mainBranch.branchName + "Branch", + target: integrationBranch.branchName + "Branch", + type: "gitMerge", + label: "Retrofit from RUN to BUILD" + }); + } + // Sort branches & links + this.gitBranches = sortArray(this.gitBranches, { by: ['level', 'name'], order: ['asc', 'asc'] }); + this.gitLinks = sortArray(this.gitLinks, { by: ['level', 'source'], order: ['asc', 'asc'] }); + } + + private listSalesforceOrgsAndLinks(): any { + for (const gitBranch of this.gitBranches) { + const branchAndOrg = this.branchesAndOrgs.find((branchAndOrg) => branchAndOrg.branchName === gitBranch.name); + if (branchAndOrg) { + // Major org + const nodeName = branchAndOrg.branchName + "Org"; + this.salesforceOrgs.push({ + name: branchAndOrg.branchName, + nodeName: branchAndOrg.branchName + "Org", + label: isProduction(branchAndOrg.branchName) ? "Production Org" : prettifyFieldName(branchAndOrg.branchName) + " Org", + class: gitBranch.class === "gitMain" ? "salesforceProd" : gitBranch.class === "gitMajor" ? "salesforceMajor" : "salesforceDev", + level: branchAndOrg.level + }); + this.deployLinks.push({ + source: gitBranch.nodeName, + target: nodeName, + type: "sfDeploy", + label: "Deploy to Org", + level: branchAndOrg.level + }); + } + else { + const nodeName = gitBranch.name + "Org"; + this.salesforceOrgs.push({ + name: gitBranch.name, + nodeName: nodeName, + label: "Dev" + prettifyFieldName(gitBranch.name), + class: "salesforceDev", + level: gitBranch.level + }); + this.sbDevLinks.push({ + source: nodeName, + target: gitBranch.nodeName, + type: "sfPushPull", + label: "Push / Pull", + level: gitBranch.level + }); + } + } + // Sort orgs & links + this.salesforceOrgs = sortArray(this.salesforceOrgs, { by: ['level', 'name'], order: ['desc', 'asc'] }); + this.deployLinks = sortArray(this.deployLinks, { by: ['level', 'source'], order: ['desc', 'asc'] }); + this.sbDevLinks = sortArray(this.sbDevLinks, { by: ['level', 'source'], order: ['asc', 'asc'] }); + } + + private generateMermaidLines() { + this.mermaidLines.push("flowchart LR"); + this.mermaidLines.push(""); + + // Git branches + this.mermaidLines.push(this.indent("subgraph GitBranches [Git Branches]", 1)); + this.mermaidLines.push(this.indent("direction TB", 2)); + for (const gitBranch of this.gitBranches) { + this.mermaidLines.push(this.indent(`${gitBranch.nodeName}["${gitBranch.label}"]:::${gitBranch.class}`, 2)); + } + this.mermaidLines.push(this.indent("end", 1)); + this.mermaidLines.push(""); + + // Salesforce orgs + this.mermaidLines.push(this.indent("subgraph SalesforceOrgs [Salesforce Orgs]", 1)); + this.mermaidLines.push(this.indent("direction TB", 2)); + for (const salesforceOrg of this.salesforceOrgs.filter((salesforceOrg) => ["salesforceProd", "salesforceMajor"].includes(salesforceOrg.class))) { + this.mermaidLines.push(this.indent(`${salesforceOrg.nodeName}(["${salesforceOrg.label}"]):::${salesforceOrg.class}`, 2)); + } + this.mermaidLines.push(this.indent("end", 1)); + this.mermaidLines.push(""); + + // Salesforce dev orgs + this.mermaidLines.push(this.indent("subgraph SalesforceDevOrgs [Salesforce Orgs BUILD]", 1)); + this.mermaidLines.push(this.indent("direction TB", 2)); + for (const salesforceOrg of this.salesforceOrgs.filter((salesforceOrg) => salesforceOrg.name.startsWith("feature"))) { + this.mermaidLines.push(this.indent(`${salesforceOrg.nodeName}(["${salesforceOrg.label}"]):::${salesforceOrg.class}`, 2)); + } + this.mermaidLines.push(this.indent("end", 1)); + this.mermaidLines.push(""); + + // Salesforce dev orgs run + this.mermaidLines.push(this.indent("subgraph SalesforceDevOrgsRun [Salesforce Orgs RUN]", 1)); + this.mermaidLines.push(this.indent("direction TB", 2)); + for (const salesforceOrg of this.salesforceOrgs.filter((salesforceOrg) => salesforceOrg.name.startsWith("hotfix"))) { + this.mermaidLines.push(this.indent(`${salesforceOrg.nodeName}(["${salesforceOrg.label}"]):::${salesforceOrg.class}`, 2)); + } + this.mermaidLines.push(this.indent("end", 1)); + this.mermaidLines.push(""); + + // Links + this.addLinks(this.gitLinks); + this.addLinks(this.deployLinks); + this.addLinks(this.sbDevLinks); + this.addLinks(this.retrofitLinks); + + // Classes and styles + this.mermaidLines.push(...this.listClassesAndStyles()); + } + + private addLinks(links) { + for (const link of links) { + if (link.type === "gitMerge") { + this.mermaidLines.push(this.indent(`${link.source} -->|"${link.label}"| ${link.target}`, 1)); + } else if (link.type === "sfDeploy") { + this.mermaidLines.push(this.indent(`${link.source} -. ${link.label} .-> ${link.target}`, 1)); + } else if (link.type === "sfPushPull") { + this.mermaidLines.push(this.indent(`${link.source} <-. ${link.label} .-> ${link.target}`, 1)); + } + } + this.mermaidLines.push(""); + } + + listClassesAndStyles(): string[] { + const classesAndStyles = ` classDef salesforceDev fill:#A9E8F8,stroke:#004E8A,stroke-width:2px,color:black,font-weight:bold,border-radius:10px; + classDef salesforceMajor fill:#0088CE,stroke:#004E8A,stroke-width:2px,color:white,font-weight:bold,border-radius:10px; + classDef salesforceProd fill:blue,stroke:#004E8A,stroke-width:2px,color:white,font-weight:bold,border-radius:10px; + classDef gitMajor fill:#FFC107,stroke:#D84315,stroke-width:2px,color:black,font-weight:bold,border-radius:10px; + classDef gitMain fill:#FF6F61,stroke:#FF6F00,stroke-width:2px,color:black,font-weight:bold,border-radius:10px; + classDef gitFeature fill:#B5EAD7,stroke:#2E7D32,stroke-width:2px,color:black,font-weight:bold,border-radius:10px; + + style GitBranches fill:#F4F4F9,stroke:#7C4DFF,stroke-width:1px; + style SalesforceOrgs fill:#E8F5E9,stroke:#1B5E20,stroke-width:1px; + style SalesforceDevOrgs fill:#E1F5FE,stroke:#0288D1,stroke-width:1px; + style SalesforceDevOrgsRun fill:#F3E5F5,stroke:#6A1B9A,stroke-width:1px; +` + return classesAndStyles.split("\n"); + } + + private indent(str: string, number: number): string { + return ' '.repeat(number) + str; + } +} \ No newline at end of file diff --git a/src/common/utils/orgConfigUtils.ts b/src/common/utils/orgConfigUtils.ts index 468e872f0..651718bfb 100644 --- a/src/common/utils/orgConfigUtils.ts +++ b/src/common/utils/orgConfigUtils.ts @@ -176,35 +176,99 @@ export async function listMajorOrgs() { const majorOrgsSorted: any = []; // Main for (const majorOrg of majorOrgs) { - if (majorOrg?.branchName?.toLowerCase().startsWith("main") || majorOrg?.branchName?.toLowerCase().startsWith("prod")) { + if (isProduction(majorOrg?.branchName || "")) { + majorOrg.level = majorOrg.level || 100; majorOrgsSorted.push(majorOrg); } } // Preprod for (const majorOrg of majorOrgs) { - if (majorOrg?.branchName?.toLowerCase().startsWith("preprod") || majorOrg?.branchName?.toLowerCase().startsWith("staging")) { + if (isPreprod(majorOrg?.branchName || "")) { + majorOrg.level = majorOrg.level || 90; + majorOrgsSorted.push(majorOrg); + } + } + // uat run + for (const majorOrg of majorOrgs) { + if (isUatRun(majorOrg?.branchName || "")) { + majorOrg.level = majorOrg.level || 80; majorOrgsSorted.push(majorOrg); } } // uat for (const majorOrg of majorOrgs) { - if (majorOrg?.branchName?.toLowerCase().startsWith("uat") || majorOrg?.branchName?.toLowerCase().startsWith("recette")) { + if (isUat(majorOrg?.branchName || "")) { + majorOrg.level = majorOrg.level || 70; majorOrgsSorted.push(majorOrg); } } // integration for (const majorOrg of majorOrgs) { - if (majorOrg?.branchName?.toLowerCase().startsWith("integ")) { + if (isIntegration(majorOrg?.branchName || "")) { + majorOrg.level = majorOrg.level || 50; 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) { + majorOrg.level = majorOrg.level || 40; majorOrgsSorted.push(majorOrg); } } - return majorOrgsSorted; + const completedMajorOrgs = majorOrgsSorted.map((majorOrg: any) => { + if (majorOrg?.mergeTargets?.length > 0) { + return majorOrg; + } + majorOrg.mergeTargets = guessMatchingMergeTargets(majorOrg.branchName, majorOrgs); + return majorOrg; + }); + return completedMajorOrgs; +} + +function guessMatchingMergeTargets(branchName: string, majorOrgs: any[]): string[] { + if (isProduction(branchName)) { + return []; + } + else if (isPreprod(branchName)) { + return majorOrgs.filter(org => isProduction(org.branchName)).map(org => org.branchName); + } + else if (isUat(branchName)) { + return majorOrgs.filter(org => isPreprod(org.branchName)).map(org => org.branchName); + } + else if (isUatRun(branchName)) { + return majorOrgs.filter(org => isPreprod(org.branchName)).map(org => org.branchName); + } + else if (isIntegration(branchName)) { + return majorOrgs.filter(org => isUat(org.branchName)).map(org => org.branchName); + } + uxLog(this, c.yellow(`Unable to guess merge targets for ${branchName}. +Please set them manually in config/branches/.sfdx-hardis.${branchName}.yml +Example: +mergeTargets: + - preprod +`)); + return []; +} + +export function isProduction(branchName) { + return branchName.toLowerCase().startsWith("prod") || branchName.toLowerCase().startsWith("main"); +} + +export function isPreprod(branchName) { + return branchName.toLowerCase().startsWith("preprod") || branchName.toLowerCase().startsWith("staging"); +} + +export function isUat(branchName) { + return (branchName.toLowerCase().startsWith("uat") || branchName.toLowerCase().startsWith("recette")) && !branchName.toLowerCase().includes("run"); +} + +export function isIntegration(branchName) { + return branchName.toLowerCase().startsWith("integ"); +} + +export function isUatRun(branchName) { + return (branchName.toLowerCase().startsWith("uat") || branchName.toLowerCase().startsWith("recette")) && branchName.toLowerCase().includes("run"); } export async function checkSfdxHardisTraceAvailable(conn: Connection) {