sfdx-gatekeeper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/bin/cli.js +102 -0
- package/lib/output.js +190 -0
- package/package.json +49 -0
- package/src/index.js +440 -0
- package/validations/branch-naming.js +47 -0
- package/validations/commit-message.js +93 -0
- package/validations/component-prefix.js +124 -0
- package/validations/package-xml.js +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# SFDX Gatekeeper
|
|
2
|
+
|
|
3
|
+
A lightweight pre-commit validation framework for Salesforce projects.
|
|
4
|
+
|
|
5
|
+
**Author:** Milanjeet Singh
|
|
6
|
+
**Created:** March 2026
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/sfdx-gatekeeper)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Easy Configuration** - Simple YAML config file
|
|
14
|
+
- **Extensible** - Add your own validations
|
|
15
|
+
- **Beautiful Output** - Formatted, colorful terminal output
|
|
16
|
+
- **Minimal Dependencies** - Only `js-yaml` for config parsing
|
|
17
|
+
- **Salesforce Focused** - Built specifically for SFDX projects
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install sfdx-gatekeeper --save-dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Initialize Configuration
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx sfdx-gatekeeper init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This creates `sfdx-gatekeeper.yml` in your project root.
|
|
34
|
+
|
|
35
|
+
### 2. Install Git Hooks
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx sfdx-gatekeeper install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Done!
|
|
42
|
+
|
|
43
|
+
Your commits will now be validated automatically.
|
|
44
|
+
|
|
45
|
+
## Sample Output
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
┌──────────────────────────────────────────────────┐
|
|
49
|
+
│ SFDX Gatekeeper Validations │
|
|
50
|
+
├──────────────────────────────────────────────────┤
|
|
51
|
+
│ Commit Message Standard ✔ [Pass] │
|
|
52
|
+
│ Component Prefix (CODVR_) ✔ [Pass] │
|
|
53
|
+
│ Branch Naming Convention ✔ [Pass] │
|
|
54
|
+
├──────────────────────────────────────────────────┤
|
|
55
|
+
│ Result: 3/3 checks passed │
|
|
56
|
+
└──────────────────────────────────────────────────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
| Command | Description |
|
|
62
|
+
|---------|-------------|
|
|
63
|
+
| `npx sfdx-gatekeeper init` | Create config file |
|
|
64
|
+
| `npx sfdx-gatekeeper install` | Install git hooks |
|
|
65
|
+
| `npx sfdx-gatekeeper uninstall` | Remove git hooks |
|
|
66
|
+
| `npx sfdx-gatekeeper run <hook>` | Run validations manually |
|
|
67
|
+
| `npx sfdx-gatekeeper list` | List available validations |
|
|
68
|
+
| `npx sfdx-gatekeeper test` | Test output formatting |
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
Edit `sfdx-gatekeeper.yml` to customize validations:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
settings:
|
|
76
|
+
name: "SFDX Gatekeeper Validations"
|
|
77
|
+
boxWidth: 50
|
|
78
|
+
|
|
79
|
+
validations:
|
|
80
|
+
enabled:
|
|
81
|
+
- commit-message
|
|
82
|
+
- component-prefix
|
|
83
|
+
- branch-naming
|
|
84
|
+
|
|
85
|
+
commit-message:
|
|
86
|
+
prefix: "Commit for"
|
|
87
|
+
issuePattern: "[IS]-[0-9]+"
|
|
88
|
+
blockedPhrases:
|
|
89
|
+
- "initial commit"
|
|
90
|
+
- "dummy"
|
|
91
|
+
- "wip"
|
|
92
|
+
requireDescription: true
|
|
93
|
+
|
|
94
|
+
component-prefix:
|
|
95
|
+
prefix: "CODVR_"
|
|
96
|
+
checkTypes:
|
|
97
|
+
- lwc
|
|
98
|
+
- aura
|
|
99
|
+
- classes
|
|
100
|
+
|
|
101
|
+
branch-naming:
|
|
102
|
+
pattern: "^(feature|bugfix|hotfix|release)/"
|
|
103
|
+
exempt:
|
|
104
|
+
- main
|
|
105
|
+
- master
|
|
106
|
+
- develop
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Built-in Validations
|
|
110
|
+
|
|
111
|
+
| Validation | Description |
|
|
112
|
+
|------------|-------------|
|
|
113
|
+
| `commit-message` | Validates commit message format |
|
|
114
|
+
| `component-prefix` | Checks SF components have required prefix |
|
|
115
|
+
| `branch-naming` | Enforces branch naming convention |
|
|
116
|
+
| `package-xml` | Checks package.xml is in sync |
|
|
117
|
+
|
|
118
|
+
## Adding Custom Validations
|
|
119
|
+
|
|
120
|
+
Create a new file in `node_modules/sfdx-gatekeeper/validations/` or your project's validations folder:
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// validations/my-custom-check.js
|
|
124
|
+
|
|
125
|
+
class MyCustomValidation {
|
|
126
|
+
constructor(validator) {
|
|
127
|
+
this.validator = validator;
|
|
128
|
+
this.displayName = 'My Custom Check';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async run() {
|
|
132
|
+
// Your validation logic here
|
|
133
|
+
|
|
134
|
+
if (someCondition) {
|
|
135
|
+
return { status: 'pass', details: '' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
status: 'fail',
|
|
140
|
+
details: 'Reason for failure'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = MyCustomValidation;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Enable it in `sfdx-gatekeeper.yml`:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
validations:
|
|
152
|
+
enabled:
|
|
153
|
+
- commit-message
|
|
154
|
+
- my-custom-check
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Commit Message Format
|
|
158
|
+
|
|
159
|
+
Valid commit messages:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Commit for I-12345 - Fixed login issue
|
|
163
|
+
Commit for S-67890 - Added new feature
|
|
164
|
+
Commit for codvr_myComponent - Updated styles
|
|
165
|
+
Commit for Apex CODVR_Controller - Refactored query
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Requirements
|
|
169
|
+
|
|
170
|
+
- Node.js >= 14.0.0
|
|
171
|
+
- Git
|
|
172
|
+
|
|
173
|
+
## Contributing
|
|
174
|
+
|
|
175
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
176
|
+
|
|
177
|
+
## Author
|
|
178
|
+
|
|
179
|
+
**Milanjeet Singh**
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* =============================================================================
|
|
4
|
+
* SFDX Gatekeeper - CLI Entry Point
|
|
5
|
+
*
|
|
6
|
+
* Author: Milanjeet Singh
|
|
7
|
+
* Created: March 2026
|
|
8
|
+
* =============================================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { SFDXGatekeeper } = require('../src/index.js');
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args[0] || 'help';
|
|
15
|
+
|
|
16
|
+
const validator = new SFDXGatekeeper();
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
try {
|
|
20
|
+
switch (command) {
|
|
21
|
+
case 'init':
|
|
22
|
+
await validator.init();
|
|
23
|
+
break;
|
|
24
|
+
|
|
25
|
+
case 'install':
|
|
26
|
+
await validator.installHooks();
|
|
27
|
+
break;
|
|
28
|
+
|
|
29
|
+
case 'uninstall':
|
|
30
|
+
await validator.uninstallHooks();
|
|
31
|
+
break;
|
|
32
|
+
|
|
33
|
+
case 'run':
|
|
34
|
+
const hook = args[1] || 'pre-commit';
|
|
35
|
+
const hookArg = args[2];
|
|
36
|
+
const exitCode = await validator.run(hook, hookArg);
|
|
37
|
+
process.exit(exitCode);
|
|
38
|
+
break;
|
|
39
|
+
|
|
40
|
+
case 'list':
|
|
41
|
+
validator.listValidations();
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case 'test':
|
|
45
|
+
validator.testOutput();
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case 'help':
|
|
49
|
+
case '--help':
|
|
50
|
+
case '-h':
|
|
51
|
+
printHelp();
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case 'version':
|
|
55
|
+
case '--version':
|
|
56
|
+
case '-v':
|
|
57
|
+
const pkg = require('../package.json');
|
|
58
|
+
console.log(`mtx-validator v${pkg.version}`);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
console.error(`Unknown command: ${command}`);
|
|
63
|
+
printHelp();
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Error:', error.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function printHelp() {
|
|
73
|
+
console.log(`
|
|
74
|
+
SFDX Gatekeeper - Pre-commit validation framework for Salesforce
|
|
75
|
+
Author: Milanjeet Singh
|
|
76
|
+
|
|
77
|
+
Usage: sfdx-gatekeeper <command> [options]
|
|
78
|
+
|
|
79
|
+
Commands:
|
|
80
|
+
init Create sfdx-gatekeeper.yml config in current project
|
|
81
|
+
install Install git hooks in current project
|
|
82
|
+
uninstall Remove git hooks from current project
|
|
83
|
+
run <hook> Run validations for a specific hook
|
|
84
|
+
list List available validations
|
|
85
|
+
test Test validator output (dry run)
|
|
86
|
+
version Show version number
|
|
87
|
+
help Show this help message
|
|
88
|
+
|
|
89
|
+
Hooks:
|
|
90
|
+
pre-commit Run before commit is created
|
|
91
|
+
commit-msg <file> Validate commit message
|
|
92
|
+
pre-push Run before push
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
npx sfdx-gatekeeper init
|
|
96
|
+
npx sfdx-gatekeeper install
|
|
97
|
+
npx sfdx-gatekeeper run pre-commit
|
|
98
|
+
npx sfdx-gatekeeper run commit-msg .git/COMMIT_EDITMSG
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main();
|
package/lib/output.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* SFDX Gatekeeper - Output Formatting Library
|
|
4
|
+
*
|
|
5
|
+
* Author: Milanjeet Singh
|
|
6
|
+
* Created: March 2026
|
|
7
|
+
* =============================================================================
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class Output {
|
|
11
|
+
constructor(boxWidth = 50) {
|
|
12
|
+
this.boxWidth = boxWidth;
|
|
13
|
+
this.supportsColor = process.stdout.isTTY;
|
|
14
|
+
|
|
15
|
+
// Colors
|
|
16
|
+
this.colors = {
|
|
17
|
+
red: this.supportsColor ? '\x1b[31m' : '',
|
|
18
|
+
green: this.supportsColor ? '\x1b[32m' : '',
|
|
19
|
+
yellow: this.supportsColor ? '\x1b[33m' : '',
|
|
20
|
+
blue: this.supportsColor ? '\x1b[34m' : '',
|
|
21
|
+
cyan: this.supportsColor ? '\x1b[36m' : '',
|
|
22
|
+
bold: this.supportsColor ? '\x1b[1m' : '',
|
|
23
|
+
dim: this.supportsColor ? '\x1b[2m' : '',
|
|
24
|
+
reset: this.supportsColor ? '\x1b[0m' : ''
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Symbols
|
|
28
|
+
this.symbols = {
|
|
29
|
+
pass: '✔',
|
|
30
|
+
fail: '✘',
|
|
31
|
+
warn: '⚠',
|
|
32
|
+
info: 'ℹ',
|
|
33
|
+
arrow: '→'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Box drawing
|
|
37
|
+
this.box = {
|
|
38
|
+
tl: '┌', tr: '┐', bl: '└', br: '┘',
|
|
39
|
+
h: '─', v: '│', ml: '├', mr: '┤'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Print header with title
|
|
45
|
+
*/
|
|
46
|
+
printHeader(title) {
|
|
47
|
+
const { cyan, bold, reset } = this.colors;
|
|
48
|
+
const { tl, tr, h, v, ml, mr } = this.box;
|
|
49
|
+
const width = this.boxWidth;
|
|
50
|
+
|
|
51
|
+
const padding = Math.floor((width - title.length) / 2);
|
|
52
|
+
const paddingStr = ' '.repeat(padding);
|
|
53
|
+
const extraSpace = (width - title.length) % 2 === 1 ? ' ' : '';
|
|
54
|
+
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(`${cyan}${tl}${h.repeat(width)}${tr}${reset}`);
|
|
57
|
+
console.log(`${cyan}${v}${reset}${bold}${paddingStr}${title}${paddingStr}${extraSpace}${reset}${cyan}${v}${reset}`);
|
|
58
|
+
console.log(`${cyan}${ml}${h.repeat(width)}${mr}${reset}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Print footer
|
|
63
|
+
*/
|
|
64
|
+
printFooter() {
|
|
65
|
+
const { cyan, reset } = this.colors;
|
|
66
|
+
const { bl, br, h } = this.box;
|
|
67
|
+
|
|
68
|
+
console.log(`${cyan}${bl}${h.repeat(this.boxWidth)}${br}${reset}`);
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Print divider
|
|
74
|
+
*/
|
|
75
|
+
printDivider() {
|
|
76
|
+
const { cyan, reset } = this.colors;
|
|
77
|
+
const { ml, mr, h } = this.box;
|
|
78
|
+
|
|
79
|
+
console.log(`${cyan}${ml}${h.repeat(this.boxWidth)}${mr}${reset}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Print result line
|
|
84
|
+
*/
|
|
85
|
+
printResultLine(name, status) {
|
|
86
|
+
const { cyan, green, red, yellow, dim, reset } = this.colors;
|
|
87
|
+
const { v } = this.box;
|
|
88
|
+
const { pass, fail, warn } = this.symbols;
|
|
89
|
+
|
|
90
|
+
let statusText, statusColor, symbol;
|
|
91
|
+
|
|
92
|
+
switch (status) {
|
|
93
|
+
case 'pass':
|
|
94
|
+
statusText = 'Pass';
|
|
95
|
+
statusColor = green;
|
|
96
|
+
symbol = pass;
|
|
97
|
+
break;
|
|
98
|
+
case 'fail':
|
|
99
|
+
statusText = 'Failed';
|
|
100
|
+
statusColor = red;
|
|
101
|
+
symbol = fail;
|
|
102
|
+
break;
|
|
103
|
+
case 'warn':
|
|
104
|
+
statusText = 'Warning';
|
|
105
|
+
statusColor = yellow;
|
|
106
|
+
symbol = warn;
|
|
107
|
+
break;
|
|
108
|
+
case 'skip':
|
|
109
|
+
statusText = 'Skipped';
|
|
110
|
+
statusColor = dim;
|
|
111
|
+
symbol = '-';
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
statusText = status;
|
|
115
|
+
statusColor = '';
|
|
116
|
+
symbol = '?';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resultStr = `${symbol} [${statusText}]`;
|
|
120
|
+
const nameWidth = this.boxWidth - resultStr.length - 3;
|
|
121
|
+
const paddedName = name.substring(0, nameWidth).padEnd(nameWidth);
|
|
122
|
+
|
|
123
|
+
console.log(`${cyan}${v}${reset} ${paddedName}${statusColor}${resultStr}${reset} ${cyan}${v}${reset}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Print detail line
|
|
128
|
+
*/
|
|
129
|
+
printDetail(message) {
|
|
130
|
+
const { cyan, dim, reset } = this.colors;
|
|
131
|
+
const { v, arrow } = this.box;
|
|
132
|
+
|
|
133
|
+
const indent = ` ${this.symbols.arrow} `;
|
|
134
|
+
const msgWidth = this.boxWidth - indent.length - 1;
|
|
135
|
+
const truncatedMsg = message.substring(0, msgWidth).padEnd(msgWidth);
|
|
136
|
+
|
|
137
|
+
console.log(`${cyan}${v}${reset}${dim}${indent}${truncatedMsg}${reset}${cyan}${v}${reset}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Print summary
|
|
142
|
+
*/
|
|
143
|
+
printSummary(passed, total) {
|
|
144
|
+
const { cyan, green, red, yellow, bold, reset } = this.colors;
|
|
145
|
+
const { v } = this.box;
|
|
146
|
+
|
|
147
|
+
this.printDivider();
|
|
148
|
+
|
|
149
|
+
const summary = `Result: ${passed}/${total} checks passed`;
|
|
150
|
+
const padding = Math.floor((this.boxWidth - summary.length) / 2);
|
|
151
|
+
const paddingStr = ' '.repeat(padding);
|
|
152
|
+
const extraSpace = (this.boxWidth - summary.length) % 2 === 1 ? ' ' : '';
|
|
153
|
+
|
|
154
|
+
let color;
|
|
155
|
+
if (passed === total) {
|
|
156
|
+
color = green;
|
|
157
|
+
} else if (passed === 0) {
|
|
158
|
+
color = red;
|
|
159
|
+
} else {
|
|
160
|
+
color = yellow;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`${cyan}${v}${reset}${paddingStr}${color}${bold}${summary}${reset}${paddingStr}${extraSpace}${cyan}${v}${reset}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Log helpers
|
|
168
|
+
*/
|
|
169
|
+
logInfo(message) {
|
|
170
|
+
const { blue, reset } = this.colors;
|
|
171
|
+
console.log(`${blue}${this.symbols.info}${reset} ${message}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
logSuccess(message) {
|
|
175
|
+
const { green, reset } = this.colors;
|
|
176
|
+
console.log(`${green}${this.symbols.pass}${reset} ${message}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
logError(message) {
|
|
180
|
+
const { red, reset } = this.colors;
|
|
181
|
+
console.log(`${red}${this.symbols.fail}${reset} ${message}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
logWarn(message) {
|
|
185
|
+
const { yellow, reset } = this.colors;
|
|
186
|
+
console.log(`${yellow}${this.symbols.warn}${reset} ${message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = Output;
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sfdx-gatekeeper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight pre-commit validation framework for Salesforce projects",
|
|
5
|
+
"author": "Milanjeet Singh",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"git",
|
|
9
|
+
"hooks",
|
|
10
|
+
"pre-commit",
|
|
11
|
+
"commit-msg",
|
|
12
|
+
"salesforce",
|
|
13
|
+
"sfdx",
|
|
14
|
+
"lwc",
|
|
15
|
+
"apex",
|
|
16
|
+
"validation",
|
|
17
|
+
"linter",
|
|
18
|
+
"gatekeeper"
|
|
19
|
+
],
|
|
20
|
+
"main": "src/index.js",
|
|
21
|
+
"bin": {
|
|
22
|
+
"sfdx-gatekeeper": "./bin/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"src",
|
|
27
|
+
"lib",
|
|
28
|
+
"validations",
|
|
29
|
+
"templates"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node bin/cli.js test",
|
|
33
|
+
"lint": "eslint src/**/*.js"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"js-yaml": "^4.1.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=14.0.0"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://gitlab.com/milanjeet-singh/sfdx-gatekeeper.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://gitlab.com/milanjeet-singh/sfdx-gatekeeper/-/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://gitlab.com/milanjeet-singh/sfdx-gatekeeper#readme"
|
|
49
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* SFDX Gatekeeper - Main Module
|
|
4
|
+
*
|
|
5
|
+
* Author: Milanjeet Singh
|
|
6
|
+
* Created: March 2026
|
|
7
|
+
* =============================================================================
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
14
|
+
const Output = require('../lib/output.js');
|
|
15
|
+
|
|
16
|
+
class SFDXGatekeeper {
|
|
17
|
+
constructor(projectRoot = process.cwd()) {
|
|
18
|
+
this.projectRoot = projectRoot;
|
|
19
|
+
this.configPath = path.join(projectRoot, 'sfdx-gatekeeper.yml');
|
|
20
|
+
this.config = this.loadConfig();
|
|
21
|
+
this.output = new Output(this.config.settings?.boxWidth || 50);
|
|
22
|
+
this.results = [];
|
|
23
|
+
this.hookType = '';
|
|
24
|
+
this.commitMsgFile = '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load configuration from mtx-validator.yml
|
|
29
|
+
*/
|
|
30
|
+
loadConfig() {
|
|
31
|
+
const defaultConfig = this.getDefaultConfig();
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(this.configPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const userConfig = yaml.load(fs.readFileSync(this.configPath, 'utf8'));
|
|
36
|
+
return this.mergeConfig(defaultConfig, userConfig);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn(`Warning: Could not parse config file: ${error.message}`);
|
|
39
|
+
return defaultConfig;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return defaultConfig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get default configuration
|
|
48
|
+
*/
|
|
49
|
+
getDefaultConfig() {
|
|
50
|
+
return {
|
|
51
|
+
settings: {
|
|
52
|
+
name: 'SFDX Gatekeeper Validations',
|
|
53
|
+
boxWidth: 50
|
|
54
|
+
},
|
|
55
|
+
validations: {
|
|
56
|
+
enabled: ['commit-message', 'component-prefix', 'branch-naming'],
|
|
57
|
+
'commit-message': {
|
|
58
|
+
prefix: 'Commit for',
|
|
59
|
+
issuePattern: '[IS]-[0-9]+',
|
|
60
|
+
blockedPhrases: ['initial commit', 'dummy', 'wip', 'fixup', 'squash'],
|
|
61
|
+
requireDescription: true,
|
|
62
|
+
validComponents: ['codvr', 'c_', 'lwc', 'aura', 'apex', 'flow', 'trigger', 'class', 'test', 'batch', 'scheduler', 'controller', 'handler', 'helper', 'service', 'util']
|
|
63
|
+
},
|
|
64
|
+
'component-prefix': {
|
|
65
|
+
prefix: 'CODVR_',
|
|
66
|
+
checkTypes: ['lwc', 'aura', 'classes', 'triggers'],
|
|
67
|
+
paths: {
|
|
68
|
+
lwc: 'force-app/main/default/lwc',
|
|
69
|
+
aura: 'force-app/main/default/aura',
|
|
70
|
+
classes: 'force-app/main/default/classes',
|
|
71
|
+
triggers: 'force-app/main/default/triggers'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
'branch-naming': {
|
|
75
|
+
pattern: '^(feature|bugfix|hotfix|release|develop|main|master)/',
|
|
76
|
+
exempt: ['main', 'master', 'develop']
|
|
77
|
+
},
|
|
78
|
+
'package-xml': {
|
|
79
|
+
path: 'manifest/package.xml',
|
|
80
|
+
enabled: false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Deep merge configurations
|
|
88
|
+
*/
|
|
89
|
+
mergeConfig(defaultConfig, userConfig) {
|
|
90
|
+
const merged = { ...defaultConfig };
|
|
91
|
+
|
|
92
|
+
if (userConfig.settings) {
|
|
93
|
+
merged.settings = { ...defaultConfig.settings, ...userConfig.settings };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (userConfig.validations) {
|
|
97
|
+
merged.validations = { ...defaultConfig.validations };
|
|
98
|
+
|
|
99
|
+
if (userConfig.validations.enabled) {
|
|
100
|
+
merged.validations.enabled = userConfig.validations.enabled;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const key of Object.keys(userConfig.validations)) {
|
|
104
|
+
if (key !== 'enabled' && defaultConfig.validations[key]) {
|
|
105
|
+
merged.validations[key] = {
|
|
106
|
+
...defaultConfig.validations[key],
|
|
107
|
+
...userConfig.validations[key]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return merged;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Initialize config file in project
|
|
118
|
+
*/
|
|
119
|
+
async init() {
|
|
120
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'sfdx-gatekeeper.yml');
|
|
121
|
+
|
|
122
|
+
if (fs.existsSync(this.configPath)) {
|
|
123
|
+
console.log('⚠ sfdx-gatekeeper.yml already exists');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let template;
|
|
128
|
+
if (fs.existsSync(templatePath)) {
|
|
129
|
+
template = fs.readFileSync(templatePath, 'utf8');
|
|
130
|
+
} else {
|
|
131
|
+
template = this.generateConfigTemplate();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fs.writeFileSync(this.configPath, template);
|
|
135
|
+
console.log('✔ Created sfdx-gatekeeper.yml');
|
|
136
|
+
console.log(' Edit this file to customize validations');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate config template
|
|
141
|
+
*/
|
|
142
|
+
generateConfigTemplate() {
|
|
143
|
+
return `# =============================================================================
|
|
144
|
+
# SFDX Gatekeeper Configuration
|
|
145
|
+
# Author: Milanjeet Singh
|
|
146
|
+
# =============================================================================
|
|
147
|
+
|
|
148
|
+
settings:
|
|
149
|
+
name: "SFDX Gatekeeper Validations"
|
|
150
|
+
boxWidth: 50
|
|
151
|
+
|
|
152
|
+
validations:
|
|
153
|
+
# List of enabled validations
|
|
154
|
+
enabled:
|
|
155
|
+
- commit-message
|
|
156
|
+
- component-prefix
|
|
157
|
+
- branch-naming
|
|
158
|
+
# - package-xml # Uncomment to enable
|
|
159
|
+
|
|
160
|
+
# Commit message validation
|
|
161
|
+
commit-message:
|
|
162
|
+
prefix: "Commit for"
|
|
163
|
+
issuePattern: "[IS]-[0-9]+"
|
|
164
|
+
blockedPhrases:
|
|
165
|
+
- "initial commit"
|
|
166
|
+
- "dummy"
|
|
167
|
+
- "wip"
|
|
168
|
+
- "fixup"
|
|
169
|
+
- "squash"
|
|
170
|
+
requireDescription: true
|
|
171
|
+
validComponents:
|
|
172
|
+
- codvr
|
|
173
|
+
- lwc
|
|
174
|
+
- aura
|
|
175
|
+
- apex
|
|
176
|
+
- flow
|
|
177
|
+
- trigger
|
|
178
|
+
- class
|
|
179
|
+
- test
|
|
180
|
+
- batch
|
|
181
|
+
- controller
|
|
182
|
+
- handler
|
|
183
|
+
- service
|
|
184
|
+
- util
|
|
185
|
+
|
|
186
|
+
# Component prefix validation
|
|
187
|
+
component-prefix:
|
|
188
|
+
prefix: "CODVR_"
|
|
189
|
+
checkTypes:
|
|
190
|
+
- lwc
|
|
191
|
+
- aura
|
|
192
|
+
- classes
|
|
193
|
+
- triggers
|
|
194
|
+
paths:
|
|
195
|
+
lwc: "force-app/main/default/lwc"
|
|
196
|
+
aura: "force-app/main/default/aura"
|
|
197
|
+
classes: "force-app/main/default/classes"
|
|
198
|
+
triggers: "force-app/main/default/triggers"
|
|
199
|
+
|
|
200
|
+
# Branch naming validation
|
|
201
|
+
branch-naming:
|
|
202
|
+
pattern: "^(feature|bugfix|hotfix|release|develop|main|master)/"
|
|
203
|
+
exempt:
|
|
204
|
+
- main
|
|
205
|
+
- master
|
|
206
|
+
- develop
|
|
207
|
+
|
|
208
|
+
# Package XML sync validation
|
|
209
|
+
package-xml:
|
|
210
|
+
path: "manifest/package.xml"
|
|
211
|
+
enabled: false
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Install git hooks
|
|
217
|
+
*/
|
|
218
|
+
async installHooks() {
|
|
219
|
+
const gitDir = path.join(this.projectRoot, '.git');
|
|
220
|
+
|
|
221
|
+
if (!fs.existsSync(gitDir)) {
|
|
222
|
+
throw new Error('Not a git repository');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
226
|
+
if (!fs.existsSync(hooksDir)) {
|
|
227
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Pre-commit hook
|
|
231
|
+
const preCommitHook = `#!/usr/bin/env sh
|
|
232
|
+
npx sfdx-gatekeeper run pre-commit
|
|
233
|
+
`;
|
|
234
|
+
fs.writeFileSync(path.join(hooksDir, 'pre-commit'), preCommitHook, { mode: 0o755 });
|
|
235
|
+
|
|
236
|
+
// Commit-msg hook
|
|
237
|
+
const commitMsgHook = `#!/usr/bin/env sh
|
|
238
|
+
npx sfdx-gatekeeper run commit-msg "$1"
|
|
239
|
+
`;
|
|
240
|
+
fs.writeFileSync(path.join(hooksDir, 'commit-msg'), commitMsgHook, { mode: 0o755 });
|
|
241
|
+
|
|
242
|
+
// Pre-push hook
|
|
243
|
+
const prePushHook = `#!/usr/bin/env sh
|
|
244
|
+
npx sfdx-gatekeeper run pre-push
|
|
245
|
+
`;
|
|
246
|
+
fs.writeFileSync(path.join(hooksDir, 'pre-push'), prePushHook, { mode: 0o755 });
|
|
247
|
+
|
|
248
|
+
console.log('✔ Git hooks installed successfully!');
|
|
249
|
+
console.log(' Hooks installed in: ' + hooksDir);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Uninstall git hooks
|
|
254
|
+
*/
|
|
255
|
+
async uninstallHooks() {
|
|
256
|
+
const hooksDir = path.join(this.projectRoot, '.git', 'hooks');
|
|
257
|
+
|
|
258
|
+
for (const hook of ['pre-commit', 'commit-msg', 'pre-push']) {
|
|
259
|
+
const hookPath = path.join(hooksDir, hook);
|
|
260
|
+
if (fs.existsSync(hookPath)) {
|
|
261
|
+
fs.unlinkSync(hookPath);
|
|
262
|
+
console.log(` Removed: ${hook}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log('✔ Git hooks uninstalled!');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Run validations for a hook
|
|
271
|
+
*/
|
|
272
|
+
async run(hook, hookArg) {
|
|
273
|
+
this.hookType = hook;
|
|
274
|
+
this.results = [];
|
|
275
|
+
|
|
276
|
+
switch (hook) {
|
|
277
|
+
case 'pre-commit':
|
|
278
|
+
await this.runPreCommit();
|
|
279
|
+
break;
|
|
280
|
+
case 'commit-msg':
|
|
281
|
+
this.commitMsgFile = hookArg;
|
|
282
|
+
await this.runCommitMsg();
|
|
283
|
+
break;
|
|
284
|
+
case 'pre-push':
|
|
285
|
+
await this.runPrePush();
|
|
286
|
+
break;
|
|
287
|
+
default:
|
|
288
|
+
throw new Error(`Unknown hook: ${hook}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.displayResults();
|
|
292
|
+
|
|
293
|
+
const failedCount = this.results.filter(r => r.status === 'fail').length;
|
|
294
|
+
return failedCount > 0 ? 1 : 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Run pre-commit validations
|
|
299
|
+
*/
|
|
300
|
+
async runPreCommit() {
|
|
301
|
+
const enabled = this.config.validations.enabled;
|
|
302
|
+
|
|
303
|
+
for (const name of enabled) {
|
|
304
|
+
if (name === 'commit-message') continue; // Runs in commit-msg hook
|
|
305
|
+
await this.runValidation(name);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Run commit-msg validations
|
|
311
|
+
*/
|
|
312
|
+
async runCommitMsg() {
|
|
313
|
+
await this.runValidation('commit-message');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Run pre-push validations
|
|
318
|
+
*/
|
|
319
|
+
async runPrePush() {
|
|
320
|
+
await this.runValidation('branch-naming');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Run a single validation
|
|
325
|
+
*/
|
|
326
|
+
async runValidation(name) {
|
|
327
|
+
const validationPath = path.join(__dirname, '..', 'validations', `${name}.js`);
|
|
328
|
+
|
|
329
|
+
if (!fs.existsSync(validationPath)) {
|
|
330
|
+
console.warn(`Warning: Validation not found: ${name}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const Validation = require(validationPath);
|
|
335
|
+
const validation = new Validation(this);
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const result = await validation.run();
|
|
339
|
+
this.results.push({
|
|
340
|
+
name: validation.displayName,
|
|
341
|
+
status: result.status,
|
|
342
|
+
details: result.details
|
|
343
|
+
});
|
|
344
|
+
} catch (error) {
|
|
345
|
+
this.results.push({
|
|
346
|
+
name: name,
|
|
347
|
+
status: 'fail',
|
|
348
|
+
details: error.message
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Display results
|
|
355
|
+
*/
|
|
356
|
+
displayResults() {
|
|
357
|
+
const passed = this.results.filter(r => r.status === 'pass').length;
|
|
358
|
+
const total = this.results.length;
|
|
359
|
+
|
|
360
|
+
this.output.printHeader(this.config.settings.name);
|
|
361
|
+
|
|
362
|
+
for (const result of this.results) {
|
|
363
|
+
this.output.printResultLine(result.name, result.status);
|
|
364
|
+
if (result.details && result.status === 'fail') {
|
|
365
|
+
this.output.printDetail(result.details);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.output.printSummary(passed, total);
|
|
370
|
+
this.output.printFooter();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* List available validations
|
|
375
|
+
*/
|
|
376
|
+
listValidations() {
|
|
377
|
+
const validationsDir = path.join(__dirname, '..', 'validations');
|
|
378
|
+
const enabled = this.config.validations.enabled;
|
|
379
|
+
|
|
380
|
+
console.log('Available validations:\n');
|
|
381
|
+
|
|
382
|
+
if (fs.existsSync(validationsDir)) {
|
|
383
|
+
const files = fs.readdirSync(validationsDir).filter(f => f.endsWith('.js'));
|
|
384
|
+
|
|
385
|
+
for (const file of files) {
|
|
386
|
+
const name = path.basename(file, '.js');
|
|
387
|
+
const marker = enabled.includes(name) ? '*' : ' ';
|
|
388
|
+
console.log(` [${marker}] ${name}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log('\n [*] = enabled in sfdx-gatekeeper.yml');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Test output formatting
|
|
397
|
+
*/
|
|
398
|
+
testOutput() {
|
|
399
|
+
this.output.printHeader(this.config.settings.name);
|
|
400
|
+
this.output.printResultLine('Commit Message Standard', 'pass');
|
|
401
|
+
this.output.printResultLine('Package XML in Sync', 'pass');
|
|
402
|
+
this.output.printResultLine('Component Prefix (CODVR_)', 'fail');
|
|
403
|
+
this.output.printDetail('myComponent missing CODVR_ prefix');
|
|
404
|
+
this.output.printResultLine('Branch Naming Convention', 'pass');
|
|
405
|
+
this.output.printSummary(3, 4);
|
|
406
|
+
this.output.printFooter();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get staged files
|
|
411
|
+
*/
|
|
412
|
+
getStagedFiles() {
|
|
413
|
+
try {
|
|
414
|
+
const result = execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
415
|
+
cwd: this.projectRoot,
|
|
416
|
+
encoding: 'utf8'
|
|
417
|
+
});
|
|
418
|
+
return result.trim().split('\n').filter(f => f);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get current branch name
|
|
426
|
+
*/
|
|
427
|
+
getCurrentBranch() {
|
|
428
|
+
try {
|
|
429
|
+
const result = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
430
|
+
cwd: this.projectRoot,
|
|
431
|
+
encoding: 'utf8'
|
|
432
|
+
});
|
|
433
|
+
return result.trim();
|
|
434
|
+
} catch (error) {
|
|
435
|
+
return '';
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
module.exports = { SFDXGatekeeper };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* Validation: Branch Naming Convention
|
|
4
|
+
* Ensures branch names follow the standard pattern
|
|
5
|
+
*
|
|
6
|
+
* Author: Milanjeet Singh
|
|
7
|
+
* Created: March 2026
|
|
8
|
+
* =============================================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class BranchNamingValidation {
|
|
12
|
+
constructor(validator) {
|
|
13
|
+
this.validator = validator;
|
|
14
|
+
this.config = validator.config.validations['branch-naming'];
|
|
15
|
+
this.displayName = 'Branch Naming Convention';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async run() {
|
|
19
|
+
const branch = this.validator.getCurrentBranch();
|
|
20
|
+
|
|
21
|
+
if (!branch) {
|
|
22
|
+
return {
|
|
23
|
+
status: 'fail',
|
|
24
|
+
details: 'Could not determine branch name'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if branch is exempt
|
|
29
|
+
const exempt = this.config.exempt || [];
|
|
30
|
+
if (exempt.includes(branch)) {
|
|
31
|
+
return { status: 'pass', details: '' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if branch matches pattern
|
|
35
|
+
const pattern = new RegExp(this.config.pattern);
|
|
36
|
+
if (pattern.test(branch)) {
|
|
37
|
+
return { status: 'pass', details: '' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
status: 'fail',
|
|
42
|
+
details: `'${branch}' doesn't match pattern`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = BranchNamingValidation;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* Validation: Commit Message Standard
|
|
4
|
+
*
|
|
5
|
+
* Author: Milanjeet Singh
|
|
6
|
+
* Created: March 2026
|
|
7
|
+
* =============================================================================
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
class CommitMessageValidation {
|
|
13
|
+
constructor(validator) {
|
|
14
|
+
this.validator = validator;
|
|
15
|
+
this.config = validator.config.validations['commit-message'];
|
|
16
|
+
this.displayName = 'Commit Message Standard';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
const commitMsgFile = this.validator.commitMsgFile;
|
|
21
|
+
|
|
22
|
+
// Read commit message
|
|
23
|
+
let commitMsg = '';
|
|
24
|
+
if (commitMsgFile && fs.existsSync(commitMsgFile)) {
|
|
25
|
+
commitMsg = fs.readFileSync(commitMsgFile, 'utf8').trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check for empty message
|
|
29
|
+
if (!commitMsg) {
|
|
30
|
+
return {
|
|
31
|
+
status: 'fail',
|
|
32
|
+
details: 'Commit message is empty'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check for blocked phrases
|
|
37
|
+
const blockedPhrases = this.config.blockedPhrases || [];
|
|
38
|
+
for (const phrase of blockedPhrases) {
|
|
39
|
+
if (commitMsg.toLowerCase().includes(phrase.toLowerCase())) {
|
|
40
|
+
return {
|
|
41
|
+
status: 'fail',
|
|
42
|
+
details: `Contains blocked phrase: "${phrase}"`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for required prefix
|
|
48
|
+
const prefix = this.config.prefix;
|
|
49
|
+
if (prefix && !commitMsg.startsWith(prefix + ' ')) {
|
|
50
|
+
return {
|
|
51
|
+
status: 'fail',
|
|
52
|
+
details: `Must start with "${prefix}"`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for Issue/Story number
|
|
57
|
+
const issuePattern = new RegExp(this.config.issuePattern);
|
|
58
|
+
if (issuePattern.test(commitMsg)) {
|
|
59
|
+
return { status: 'pass', details: '' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for valid component name
|
|
63
|
+
const afterPrefix = commitMsg.substring(prefix.length + 1);
|
|
64
|
+
const component = afterPrefix.split(' ')[0];
|
|
65
|
+
|
|
66
|
+
const validComponents = this.config.validComponents || [];
|
|
67
|
+
const isValidComponent = validComponents.some(vc =>
|
|
68
|
+
component.toLowerCase().startsWith(vc.toLowerCase())
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (isValidComponent) {
|
|
72
|
+
// Check for description after dash
|
|
73
|
+
if (this.config.requireDescription) {
|
|
74
|
+
const hasDescription = /^[^ ]+ - .+/.test(afterPrefix);
|
|
75
|
+
if (!hasDescription) {
|
|
76
|
+
return {
|
|
77
|
+
status: 'fail',
|
|
78
|
+
details: "Missing description after ' - '"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { status: 'pass', details: '' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// No valid pattern found
|
|
86
|
+
return {
|
|
87
|
+
status: 'fail',
|
|
88
|
+
details: 'Need I-xxx, S-xxx, or component name'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = CommitMessageValidation;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* Validation: Component Prefix
|
|
4
|
+
* Ensures all components have the required prefix (e.g., CODVR_)
|
|
5
|
+
*
|
|
6
|
+
* Author: Milanjeet Singh
|
|
7
|
+
* Created: March 2026
|
|
8
|
+
* =============================================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
class ComponentPrefixValidation {
|
|
15
|
+
constructor(validator) {
|
|
16
|
+
this.validator = validator;
|
|
17
|
+
this.config = validator.config.validations['component-prefix'];
|
|
18
|
+
this.prefix = this.config.prefix || 'CODVR_';
|
|
19
|
+
this.displayName = `Component Prefix (${this.prefix})`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async run() {
|
|
23
|
+
const invalidComponents = [];
|
|
24
|
+
const stagedFiles = this.validator.getStagedFiles();
|
|
25
|
+
const projectRoot = this.validator.projectRoot;
|
|
26
|
+
const checkTypes = this.config.checkTypes || [];
|
|
27
|
+
const paths = this.config.paths || {};
|
|
28
|
+
|
|
29
|
+
// Check LWC components
|
|
30
|
+
if (checkTypes.includes('lwc')) {
|
|
31
|
+
const lwcPath = path.join(projectRoot, paths.lwc || 'force-app/main/default/lwc');
|
|
32
|
+
if (fs.existsSync(lwcPath)) {
|
|
33
|
+
const components = fs.readdirSync(lwcPath, { withFileTypes: true })
|
|
34
|
+
.filter(d => d.isDirectory())
|
|
35
|
+
.map(d => d.name);
|
|
36
|
+
|
|
37
|
+
for (const name of components) {
|
|
38
|
+
if (!this.hasValidPrefix(name)) {
|
|
39
|
+
// Only check staged components
|
|
40
|
+
const isStaged = stagedFiles.some(f => f.includes(`/lwc/${name}/`));
|
|
41
|
+
if (stagedFiles.length === 0 || isStaged) {
|
|
42
|
+
invalidComponents.push(`${name} (LWC)`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check Aura components
|
|
50
|
+
if (checkTypes.includes('aura')) {
|
|
51
|
+
const auraPath = path.join(projectRoot, paths.aura || 'force-app/main/default/aura');
|
|
52
|
+
if (fs.existsSync(auraPath)) {
|
|
53
|
+
const components = fs.readdirSync(auraPath, { withFileTypes: true })
|
|
54
|
+
.filter(d => d.isDirectory())
|
|
55
|
+
.map(d => d.name);
|
|
56
|
+
|
|
57
|
+
for (const name of components) {
|
|
58
|
+
if (!this.hasValidPrefix(name)) {
|
|
59
|
+
const isStaged = stagedFiles.some(f => f.includes(`/aura/${name}/`));
|
|
60
|
+
if (stagedFiles.length === 0 || isStaged) {
|
|
61
|
+
invalidComponents.push(`${name} (Aura)`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check Apex classes
|
|
69
|
+
if (checkTypes.includes('classes')) {
|
|
70
|
+
const classesPath = path.join(projectRoot, paths.classes || 'force-app/main/default/classes');
|
|
71
|
+
if (fs.existsSync(classesPath)) {
|
|
72
|
+
const classes = fs.readdirSync(classesPath)
|
|
73
|
+
.filter(f => f.endsWith('.cls'))
|
|
74
|
+
.map(f => f.replace('.cls', ''));
|
|
75
|
+
|
|
76
|
+
for (const name of classes) {
|
|
77
|
+
if (!this.hasValidPrefix(name)) {
|
|
78
|
+
const isStaged = stagedFiles.some(f => f.includes(`/classes/${name}.cls`));
|
|
79
|
+
if (stagedFiles.length === 0 || isStaged) {
|
|
80
|
+
invalidComponents.push(`${name} (Apex)`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check Triggers
|
|
88
|
+
if (checkTypes.includes('triggers')) {
|
|
89
|
+
const triggersPath = path.join(projectRoot, paths.triggers || 'force-app/main/default/triggers');
|
|
90
|
+
if (fs.existsSync(triggersPath)) {
|
|
91
|
+
const triggers = fs.readdirSync(triggersPath)
|
|
92
|
+
.filter(f => f.endsWith('.trigger'))
|
|
93
|
+
.map(f => f.replace('.trigger', ''));
|
|
94
|
+
|
|
95
|
+
for (const name of triggers) {
|
|
96
|
+
if (!this.hasValidPrefix(name)) {
|
|
97
|
+
const isStaged = stagedFiles.some(f => f.includes(`/triggers/${name}.trigger`));
|
|
98
|
+
if (stagedFiles.length === 0 || isStaged) {
|
|
99
|
+
invalidComponents.push(`${name} (Trigger)`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (invalidComponents.length === 0) {
|
|
107
|
+
return { status: 'pass', details: '' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
status: 'fail',
|
|
112
|
+
details: invalidComponents.slice(0, 3).join(', ') +
|
|
113
|
+
(invalidComponents.length > 3 ? ` (+${invalidComponents.length - 3} more)` : '')
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
hasValidPrefix(name) {
|
|
118
|
+
const prefix = this.prefix.toLowerCase();
|
|
119
|
+
return name.toLowerCase().startsWith(prefix) ||
|
|
120
|
+
name.toLowerCase().startsWith('c' + prefix.substring(1)); // cODVR_ format
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = ComponentPrefixValidation;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* Validation: Package XML Sync
|
|
4
|
+
* Checks if package.xml is in sync with force-app components
|
|
5
|
+
*
|
|
6
|
+
* Author: Milanjeet Singh
|
|
7
|
+
* Created: March 2026
|
|
8
|
+
* =============================================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
class PackageXmlValidation {
|
|
15
|
+
constructor(validator) {
|
|
16
|
+
this.validator = validator;
|
|
17
|
+
this.config = validator.config.validations['package-xml'];
|
|
18
|
+
this.displayName = 'Package XML in Sync';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async run() {
|
|
22
|
+
const packageXmlPath = path.join(
|
|
23
|
+
this.validator.projectRoot,
|
|
24
|
+
this.config.path || 'manifest/package.xml'
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Check if package.xml exists
|
|
28
|
+
if (!fs.existsSync(packageXmlPath)) {
|
|
29
|
+
return {
|
|
30
|
+
status: 'warn',
|
|
31
|
+
details: 'package.xml not found'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!this.config.enabled) {
|
|
36
|
+
return { status: 'skip', details: '' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const packageXmlContent = fs.readFileSync(packageXmlPath, 'utf8');
|
|
40
|
+
const stagedFiles = this.validator.getStagedFiles();
|
|
41
|
+
const missingComponents = [];
|
|
42
|
+
|
|
43
|
+
// Check staged Apex classes
|
|
44
|
+
const stagedClasses = stagedFiles
|
|
45
|
+
.filter(f => f.endsWith('.cls') && !f.endsWith('-meta.xml'))
|
|
46
|
+
.map(f => path.basename(f, '.cls'));
|
|
47
|
+
|
|
48
|
+
for (const className of stagedClasses) {
|
|
49
|
+
if (!packageXmlContent.includes(`<members>${className}</members>`)) {
|
|
50
|
+
missingComponents.push(className);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check staged LWC components
|
|
55
|
+
const stagedLwc = [...new Set(
|
|
56
|
+
stagedFiles
|
|
57
|
+
.filter(f => f.includes('/lwc/'))
|
|
58
|
+
.map(f => {
|
|
59
|
+
const match = f.match(/\/lwc\/([^/]+)\//);
|
|
60
|
+
return match ? match[1] : null;
|
|
61
|
+
})
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
)];
|
|
64
|
+
|
|
65
|
+
for (const lwcName of stagedLwc) {
|
|
66
|
+
if (!packageXmlContent.includes(`<members>${lwcName}</members>`)) {
|
|
67
|
+
missingComponents.push(lwcName);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (missingComponents.length === 0) {
|
|
72
|
+
return { status: 'pass', details: '' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
status: 'fail',
|
|
77
|
+
details: `Missing: ${missingComponents.slice(0, 3).join(', ')}` +
|
|
78
|
+
(missingComponents.length > 3 ? ` (+${missingComponents.length - 3} more)` : '')
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = PackageXmlValidation;
|