Skip to content

Commit

Permalink
Add branch & orgs strategy MermaidJS diagram in documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
nvuillam committed Jan 5, 2025
1 parent c1fec4c commit cd5c85e
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions config/sfdx-hardis.jsonschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions docs/salesforce-project-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ With a single command, you can generate a Web Site documenting your Salesforce m

![](assets/images/project-documentation.gif)

If it is a sfdx-hardis CI/CD project, a diagram of the branches and orgs strategy will be generated.

![](assets/images/screenshot-doc-branches-strategy.jpg)

## How To generate

- Use the Git repository containing your SFDX project, or create it easily using [sfdx-hardis Monitoring](salesforce-monitoring-home.md), or simply calling [BackUp command](hardis/org/monitor/backup.md)
Expand Down
40 changes: 39 additions & 1 deletion docs/schema/sfdx-hardis-json-schema-parameters.html
Original file line number Diff line number Diff line change
Expand Up @@ -3061,6 +3061,44 @@ <h2 class="mb-0">
</div>
</div>
</div>
<div class="accordion" id="accordiondocDeployToOrg">
<div class="card">
<div class="card-header" id="headingdocDeployToOrg">
<h2 class="mb-0">
<button class="btn btn-link property-name-button" type="button" data-toggle="collapse" data-target="#docDeployToOrg"
aria-expanded="" aria-controls="docDeployToOrg" onclick="setAnchor('#docDeployToOrg')"><span class="property-name">docDeployToOrg</span></button>
</h2>
</div>

<div id="docDeployToOrg"
class="collapse property-definition-div" aria-labelledby="headingdocDeployToOrg"
data-parent="#accordiondocDeployToOrg">
<div class="card-body pl-5">

<div class="breadcrumbs">root
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-arrow-right-short" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8z"
/>
</svg>
<a href="#docDeployToOrg" onclick="anchorLink('docDeployToOrg')">docDeployToOrg</a></div><h4>Doc: Deploy to Salesforce Org</h4><span class="badge badge-dark value-type">Type: boolean</span> <span class="badge badge-success default-value">Default: false</span><br/>
<span class="description"><p>Automatically deploy MkDocs HTML documentation from CI/CD Workflows to Salesforce org as static resource</p>
</span>





<br/>
<div class="badge badge-secondary">Example:</div>
<br/><div id="docDeployToOrg_ex1" class="jumbotron examples"><div class="highlight"><pre><span></span><span class="kc">true</span>
</pre></div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion" id="accordionextends">
<div class="card">
<div class="card-header" id="headingextends">
Expand Down Expand Up @@ -5825,6 +5863,6 @@ <h2 class="mb-0">
</div>

<footer>
<p class="generated-by-footer">Generated using <a href="https://github.com/coveooss/json-schema-for-humans">json-schema-for-humans</a> on 2025-01-03 at 11:36:24 +0100</p>
<p class="generated-by-footer">Generated using <a href="https://github.com/coveooss/json-schema-for-humans">json-schema-for-humans</a> on 2025-01-04 at 17:30:53 +0100</p>
</footer></body>
</html>
16 changes: 15 additions & 1 deletion src/commands/hardis/doc/project2markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -69,6 +70,12 @@ If Flow history doc always display a single state, you probably need to update y
![Screenshot project documentation](https://github.com/hardisgroupcom/sfdx-hardis/raw/main/docs/assets/images/screenshot-project-doc-2.jpg)
If it is a sfdx-hardis CI/CD project, a diagram of the branches and orgs strategy will be generated.
![](https://github.com/hardisgroupcom/sfdx-hardis/raw/main/docs/assets/images/screenshot-doc-branches-strategy.jpg)
If you have a complex strategy, you might need to input property **mergeTargets** in branch-scoped sfdx-hardis.yml file to have a correct diagram.
${this.htmlInstructions}
`;

Expand Down Expand Up @@ -417,8 +424,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 |",
"| :--------- | :------------- | :------------------ |"
Expand Down
241 changes: 241 additions & 0 deletions src/common/utils/branchStrategyMermaidBuilder.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit cd5c85e

Please sign in to comment.