Skip to content

Commit 51ed6b4

Browse files
authored
Merge pull request #1177 from hardisgroupcom/features/md-status
hardis:lint:metadatastatus
2 parents 788f5ae + 9bcebee commit 51ed6b4

File tree

5 files changed

+269
-15
lines changed

5 files changed

+269
-15
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44

55
Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image `hardisgroupcom/sfdx-hardis@beta`
66

7+
## [5.28.0] 2025-04-23
8+
9+
- [hardis:lint:metadatastatus](https://sfdx-hardis.cloudity.com/hardis/lint/metadatastatus/): Detect more inactive elements that are technical debt to be cleaned
10+
- Approval Processes
11+
- Assignment Rules
12+
- Auto Response Rules
13+
- Escalation Rules
14+
- Forecasting Types
15+
- Record Types
16+
- Workflow Rules
17+
718
## [5.27.0] 2025-04-18
819

920
- [hardis:doc:project2markdown](https://sfdx-hardis.cloudity.com/hardis/doc/project2markdown/) new features
Loading

docs/salesforce-monitoring-inactive-metadata.md

+16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ And what about this **deactivated Validation** Rule ?
1212

1313
Maybe it's time to remove them !
1414

15+
Full list of metadata types that are checked:
16+
17+
- Approval Processes
18+
- Assignment Rules
19+
- Auto Response Rules
20+
- Escalation Rules
21+
- Flows
22+
- Forecasting Types
23+
- Record Types
24+
- Validation Rules
25+
- Workflow Rules
26+
1527
Sfdx-hardis command: [sf hardis:lint:metadatastatus](https://sfdx-hardis.cloudity.com/hardis/lint/metadatastatus/)
1628

1729
Key: **METADATA_STATUS**
@@ -23,3 +35,7 @@ Key: **METADATA_STATUS**
2335
### Slack example
2436

2537
![](assets/images/screenshot-monitoring-inactive-metadata.jpg)
38+
39+
### Local example
40+
41+
![](assets/images/detect-inactive-metadata.gif)

src/commands/hardis/lint/metadatastatus.ts

+241-14
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,19 @@ const messages = Messages.loadMessages('sfdx-hardis', 'org');
2424
/* jscpd:ignore-end */
2525
export default class LintMetadataStatus extends SfCommand<any> {
2626
public static title = 'check inactive metadatas';
27-
public static description = `Check if elements (flows and validation rules) are inactive in the project
27+
public static description = `Check if elements are inactive in the project:
28+
29+
- Approval Processes
30+
- Assignment Rules
31+
- Auto Response Rules
32+
- Escalation Rules
33+
- Flows
34+
- Forecasting Types
35+
- Record Types
36+
- Validation Rules
37+
- Workflow Rules
38+
39+
![](https://github.com/hardisgroupcom/sfdx-hardis/raw/main/docs/assets/images/detect-inactive-metadata.gif)
2840
2941
This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/salesforce-monitoring-inactive-metadata/) and can output Grafana, Slack and MsTeams Notifications.
3042
`;
@@ -62,30 +74,85 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
6274

6375
public async run(): Promise<AnyJson> {
6476
const { flags } = await this.parse(LintMetadataStatus);
77+
78+
const inactiveApprovalProcesses = await this.verifyApprovalProcesses();
79+
const inactiveAssignmentRules = await this.verifyAssignmentRules();
80+
const inactiveAutoResponseRules = await this.verifyAutoResponseRules();
81+
const inactiveEscalationRules = await this.verifyEscalationRules();
6582
const draftFlows = await this.verifyFlows();
83+
const inactiveForecastingTypes = await this.verifyForecastingTypes();
84+
const inactiveRecordTypes = await this.verifyRecordTypes();
6685
const inactiveValidationRules = await this.verifyValidationRules();
86+
const inactiveWorkflows = await this.verifyWorkflowRules();
6787

88+
this.inactiveItems = [
89+
...inactiveApprovalProcesses,
90+
...inactiveAssignmentRules,
91+
...inactiveAutoResponseRules,
92+
...draftFlows,
93+
...inactiveEscalationRules,
94+
...inactiveForecastingTypes,
95+
...inactiveRecordTypes,
96+
...inactiveValidationRules,
97+
...inactiveWorkflows,
98+
];
6899
// Prepare notifications
69100
const branchMd = await getBranchMarkdown();
70101
const notifButtons = await getNotificationButtons();
71102
let notifSeverity: NotifSeverity = 'log';
72103
let notifText = `No inactive configuration elements has been found in ${branchMd}`;
73104
const attachments: MessageAttachment[] = [];
74-
if (draftFlows.length > 0 || inactiveValidationRules.length > 0) {
105+
if (this.inactiveItems.length > 0) {
75106
notifSeverity = 'warning';
107+
if (inactiveApprovalProcesses.length > 0) {
108+
attachments.push({
109+
text: `*Inactive Approval Processes*\n${inactiveApprovalProcesses.map((file) => `• ${file.name}`).join('\n')}`,
110+
});
111+
}
112+
if (inactiveAssignmentRules.length > 0) {
113+
attachments.push({
114+
text: `*Inactive Assignment Rules*\n${inactiveAssignmentRules.map((file) => `• ${file.name}`).join('\n')}`,
115+
});
116+
}
117+
if (inactiveAutoResponseRules.length > 0) {
118+
attachments.push({
119+
text: `*Inactive Auto Response Rules*\n${inactiveAutoResponseRules.map((file) => `• ${file.name}`).join('\n')}`,
120+
});
121+
}
122+
if (inactiveEscalationRules.length > 0) {
123+
attachments.push({
124+
text: `*Inactive Escalation Rules*\n${inactiveEscalationRules.map((file) => `• ${file.name}`).join('\n')}`,
125+
});
126+
}
76127
if (draftFlows.length > 0) {
77128
attachments.push({
78129
text: `*Inactive Flows*\n${draftFlows.map((file) => `• ${file.name}`).join('\n')}`,
79130
});
80131
}
132+
if (inactiveForecastingTypes.length > 0) {
133+
attachments.push({
134+
text: `*Inactive Forecasting Types*\n${inactiveForecastingTypes.map((file) => `• ${file.name}`).join('\n')}`,
135+
});
136+
}
137+
if (inactiveRecordTypes.length > 0) {
138+
attachments.push({
139+
text: `*Inactive Record Types*\n${inactiveRecordTypes.map((file) => `• ${file.name}`).join('\n')}`,
140+
});
141+
}
81142
if (inactiveValidationRules.length > 0) {
82143
attachments.push({
83144
text: `*Inactive Validation Rules*\n${inactiveValidationRules.map((file) => `• ${file.name}`).join('\n')}`,
84145
});
85146
}
86-
const numberInactive = draftFlows.length + inactiveValidationRules.length;
87-
notifText = `${numberInactive} inactive configuration elements have been found in ${branchMd}`;
88-
await this.buildCsvFile(draftFlows, inactiveValidationRules);
147+
if (inactiveWorkflows.length > 0) {
148+
attachments.push({
149+
text: `*Inactive Workflow Rules*\n${inactiveWorkflows.map((file) => `• ${file.name}`).join('\n')}`,
150+
});
151+
}
152+
153+
notifText = `${this.inactiveItems.length} inactive configuration elements have been found in ${branchMd}`;
154+
// Build result file
155+
await this.buildCsvFile();
89156
} else {
90157
uxLog(this, 'No draft flow or validation rule files detected.');
91158
}
@@ -124,11 +191,11 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
124191
const flowContent: string = await fs.readFile(file, 'utf-8');
125192
if (flowContent.includes('<status>Draft</status>')) {
126193
const fileName = path.basename(file, '.flow-meta.xml');
127-
draftFiles.push({ type: 'Draft Flow', name: fileName, severity: 'warning', severityIcon: severityIcon });
194+
draftFiles.push({ type: 'Flow (draft)', name: fileName, severity: 'warning', severityIcon: severityIcon });
128195
}
129196
}
130197

131-
return draftFiles;
198+
return draftFiles.sort((a, b) => a.name.localeCompare(b.name));
132199
}
133200

134201
/**
@@ -152,15 +219,179 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
152219
const ruleName = path.basename(file, '.validationRule-meta.xml');
153220
const objectName = path.basename(path.dirname(path.dirname(file)));
154221
inactiveRules.push({
155-
type: 'Inactive VR',
222+
type: 'Validation Rule (inactive)',
156223
name: `${objectName} - ${ruleName}`,
157224
severity: 'warning',
158225
severityIcon: severityIcon,
159226
});
160227
}
161228
}
162229

163-
return inactiveRules;
230+
return inactiveRules.sort((a, b) => a.name.localeCompare(b.name));
231+
}
232+
233+
private async verifyRecordTypes(): Promise<any[]> {
234+
const inactiveRecordTypes: any[] = [];
235+
const recordTypeFiles: string[] = await glob('**/objects/**/recordTypes/*.recordType-meta.xml', {
236+
ignore: this.ignorePatterns,
237+
});
238+
const severityIcon = getSeverityIcon('warning');
239+
for (const file of recordTypeFiles) {
240+
const recordTypeName = path.basename(file, '.recordType-meta.xml');
241+
const objectName = path.basename(path.dirname(path.dirname(file)));
242+
// Skip if record type is from a managed package
243+
if (path.basename(recordTypeName).includes('__')) {
244+
continue;
245+
}
246+
const recordTypeXml: string = await fs.readFile(file, 'utf-8');
247+
if (recordTypeXml.includes('<active>false</active>')) {
248+
inactiveRecordTypes.push({
249+
type: 'Record Type (inactive)',
250+
name: `${objectName} - ${recordTypeName}`,
251+
severity: 'warning',
252+
severityIcon: severityIcon,
253+
});
254+
}
255+
}
256+
257+
return inactiveRecordTypes.sort((a, b) => a.name.localeCompare(b.name));
258+
}
259+
260+
private async verifyApprovalProcesses(): Promise<any[]> {
261+
const inactiveApprovalProcesses: any[] = [];
262+
const approvalProcessFiles: string[] = await glob('**/approvalProcesses/**/*.approvalProcess-meta.xml', {
263+
ignore: this.ignorePatterns,
264+
});
265+
const severityIcon = getSeverityIcon('warning');
266+
for (const file of approvalProcessFiles) {
267+
const approvalProcessFullName = path.basename(file, '.approvalProcess-meta.xml');
268+
const [objectName, approvalProcessName] = approvalProcessFullName.split('.');
269+
// Skip if approval process is from a managed package
270+
if (path.basename(approvalProcessName).includes('__')) {
271+
continue;
272+
}
273+
const approvalProcessXml: string = await fs.readFile(file, 'utf-8');
274+
if (approvalProcessXml.includes('<active>false</active>')) {
275+
inactiveApprovalProcesses.push({
276+
type: 'Approval Process (inactive)',
277+
name: `${objectName} - ${approvalProcessName}`,
278+
severity: 'warning',
279+
severityIcon: severityIcon,
280+
});
281+
}
282+
}
283+
284+
return inactiveApprovalProcesses.sort((a, b) => a.name.localeCompare(b.name));
285+
}
286+
287+
private async verifyForecastingTypes(): Promise<any[]> {
288+
const inactiveForecastTypes: any[] = [];
289+
const forecastTypeFiles: string[] = await glob('**/forecastingTypes/**/*.forecastingType-meta.xml', {
290+
ignore: this.ignorePatterns,
291+
});
292+
const severityIcon = getSeverityIcon('warning');
293+
for (const file of forecastTypeFiles) {
294+
const forecastingTypeName = path.basename(file, '.forecastingType-meta.xml');
295+
const forecastTypeXml: string = await fs.readFile(file, 'utf-8');
296+
if (forecastTypeXml.includes('<active>false</active>')) {
297+
inactiveForecastTypes.push({
298+
type: 'Forecasting Type (inactive)',
299+
name: forecastingTypeName,
300+
severity: 'warning',
301+
severityIcon: severityIcon,
302+
});
303+
}
304+
}
305+
306+
return inactiveForecastTypes.sort((a, b) => a.name.localeCompare(b.name));
307+
}
308+
309+
private async verifyWorkflowRules(): Promise<any[]> {
310+
const inactiveWorkflowRules: any[] = [];
311+
const workflowRuleFiles: string[] = await glob('**/workflows/**/*.workflow-meta.xml', {
312+
ignore: this.ignorePatterns,
313+
});
314+
const severityIcon = getSeverityIcon('warning');
315+
for (const file of workflowRuleFiles) {
316+
const workflowRuleName = path.basename(file, '.workflow-meta.xml');
317+
const workflowRuleXml: string = await fs.readFile(file, 'utf-8');
318+
if (workflowRuleXml.includes('<active>false</active>')) {
319+
inactiveWorkflowRules.push({
320+
type: 'Workflow Rule (inactive)',
321+
name: workflowRuleName,
322+
severity: 'warning',
323+
severityIcon: severityIcon,
324+
});
325+
}
326+
}
327+
328+
return inactiveWorkflowRules.sort((a, b) => a.name.localeCompare(b.name));
329+
}
330+
331+
private async verifyAssignmentRules(): Promise<any[]> {
332+
const inactiveAssignmentRules: any[] = [];
333+
const assignmentRuleFiles: string[] = await glob('**/assignmentRules/**/*.assignmentRules-meta.xml', {
334+
ignore: this.ignorePatterns,
335+
});
336+
const severityIcon = getSeverityIcon('warning');
337+
for (const file of assignmentRuleFiles) {
338+
const assignmentRuleName = path.basename(file, '.assignmentRules-meta.xml');
339+
const assignmentRuleXml: string = await fs.readFile(file, 'utf-8');
340+
if (assignmentRuleXml.includes('<active>false</active>')) {
341+
inactiveAssignmentRules.push({
342+
type: 'Assignment Rule (inactive)',
343+
name: assignmentRuleName,
344+
severity: 'warning',
345+
severityIcon: severityIcon,
346+
});
347+
}
348+
}
349+
350+
return inactiveAssignmentRules.sort((a, b) => a.name.localeCompare(b.name));
351+
}
352+
353+
private async verifyAutoResponseRules(): Promise<any[]> {
354+
const inactiveAutoResponseRules: any[] = [];
355+
const autoResponseRuleFiles: string[] = await glob('**/autoResponseRules/**/*.autoResponseRules-meta.xml', {
356+
ignore: this.ignorePatterns,
357+
});
358+
const severityIcon = getSeverityIcon('warning');
359+
for (const file of autoResponseRuleFiles) {
360+
const autoResponseRuleName = path.basename(file, '.autoResponseRules-meta.xml');
361+
const autoResponseRuleXml: string = await fs.readFile(file, 'utf-8');
362+
if (autoResponseRuleXml.includes('<active>false</active>')) {
363+
inactiveAutoResponseRules.push({
364+
type: 'Auto Response Rule (inactive)',
365+
name: autoResponseRuleName,
366+
severity: 'warning',
367+
severityIcon: severityIcon,
368+
});
369+
}
370+
}
371+
372+
return inactiveAutoResponseRules.sort((a, b) => a.name.localeCompare(b.name));
373+
}
374+
375+
private async verifyEscalationRules(): Promise<any[]> {
376+
const inactiveEscalationRules: any[] = [];
377+
const escalationRuleFiles: string[] = await glob('**/escalationRules/**/*.escalationRules-meta.xml', {
378+
ignore: this.ignorePatterns,
379+
});
380+
const severityIcon = getSeverityIcon('warning');
381+
for (const file of escalationRuleFiles) {
382+
const escalationRuleName = path.basename(file, '.escalationRules-meta.xml');
383+
const escalationRuleXml: string = await fs.readFile(file, 'utf-8');
384+
if (escalationRuleXml.includes('<active>false</active>')) {
385+
inactiveEscalationRules.push({
386+
type: 'Escalation Rule (inactive)',
387+
name: escalationRuleName,
388+
severity: 'warning',
389+
severityIcon: severityIcon,
390+
});
391+
}
392+
}
393+
394+
return inactiveEscalationRules.sort((a, b) => a.name.localeCompare(b.name));
164395
}
165396

166397
/**
@@ -169,14 +400,10 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
169400
* It then maps the draft flows and inactive validation rules into an array of objects, each with a 'type' property set to either "Draft Flow" or "Inactive VR" and a 'name' property set to the file or rule name.
170401
* Finally, it generates a CSV file from this array and writes it to the output file.
171402
*
172-
* @param {string[]} draftFlows - An array of draft flow names.
173-
* @param {string[]} inactiveValidationRules - An array of inactive validation rule names.
174403
* @returns {Promise<void>} - A Promise that resolves when the CSV file has been successfully generated.
175404
*/
176-
private async buildCsvFile(draftFlows: string[], inactiveValidationRules: string[]): Promise<void> {
405+
private async buildCsvFile(): Promise<void> {
177406
this.outputFile = await generateReportPath('lint-metadatastatus', this.outputFile);
178-
this.inactiveItems = [...draftFlows, ...inactiveValidationRules];
179-
180407
this.outputFilesRes = await generateCsvFile(this.inactiveItems, this.outputFile);
181408
}
182409
}

src/common/gitProvider/utilsMarkdown.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function mdTableCell(str: string) {
8181
if (!str) {
8282
return "<!-- -->"
8383
}
84-
return str.replace(/\n/gm, "<br/>".replace(/\|/gm, ""))
84+
return str.replace(/\n/gm, "<br/>").replace(/\|/gm, "");
8585
}
8686

8787
export async function flowDiffToMarkdownForPullRequest(flowNames: string[], fromCommit: string, toCommit: string, truncatedNb: number = 0): Promise<any> {

0 commit comments

Comments
 (0)