pr-checkmate 1.21.1 → 1.21.2
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 +36 -3
- package/dist/config/constants.js +4 -0
- package/dist/config/loader.js +8 -0
- package/dist/config/pr-checkmate-workflow.yml +88 -0
- package/dist/init.js +24 -0
- package/dist/scripts/diff-smell.js +97 -0
- package/dist/scripts/duplicate-check.js +67 -0
- package/dist/scripts/index.js +73 -21
- package/dist/scripts/npm-audit.js +26 -16
- package/dist/scripts/pr-body.js +62 -0
- package/dist/scripts/pr-summary.js +119 -0
- package/dist/scripts/typecheck.js +34 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/result-collector.js +33 -0
- package/package.json +2 -1
- package/src/config/pr-checkmate-workflow.yml +88 -0
package/README.md
CHANGED
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
* ✅ Secret scanning via `gitleaks`.
|
|
23
23
|
* ✅ **Delta mode** — in GitHub PR context only changed files are checked, not the entire project.
|
|
24
24
|
* ✅ **Parallel execution** — all independent checks run concurrently, cutting CI time ~4×.
|
|
25
|
+
* ✅ TypeScript type check — runs `tsc --noEmit`, catches type errors before merge. Skips if no `tsconfig.json` is found.
|
|
26
|
+
* ✅ Diff code smells — scans PR diff for `console.*()` calls, `debugger` statements, and `TODO/FIXME/HACK/XXX` comments left in added lines.
|
|
27
|
+
* ✅ PR description check — validates the PR body has at least 20 characters and includes a ticket reference (`Closes #123` or `PROJ-456` style).
|
|
28
|
+
* ✅ Duplicate code detection — uses `jscpd` in delta mode (changed files in PR, or `src/` otherwise) with a 5-line / 50-token minimum threshold.
|
|
29
|
+
* ✅ PR summary comment — after `npx pr-checkmate all`, posts a summary table to the CI log and upserts a comment on the PR. No spam on repeated pushes.
|
|
25
30
|
|
|
26
31
|
<br>
|
|
27
32
|
> ⚠️ **Note:** Currently, PR CheckMate only supports **Node.js projects**.
|
|
@@ -52,7 +57,7 @@ Users can override:
|
|
|
52
57
|
|
|
53
58
|
User configuration is stored in:
|
|
54
59
|
|
|
55
|
-
```
|
|
60
|
+
```text
|
|
56
61
|
<project-root>/pr-checkmate.json
|
|
57
62
|
```
|
|
58
63
|
|
|
@@ -115,7 +120,7 @@ npx pr-checkmate <job>
|
|
|
115
120
|
|
|
116
121
|
| Job | Description |
|
|
117
122
|
|----------------|-------------|
|
|
118
|
-
| `all` | Run all checks in parallel (lint, deps, audit, security, circular, license, then spellcheck, pr-size, pr-title, outdated, markdown, then prettier) |
|
|
123
|
+
| `all` | Run all checks in parallel (lint, deps, audit, security, circular, license, then spellcheck, pr-size, pr-title, outdated, markdown, typecheck, diff-smell, pr-body, duplicate, then prettier). Posts a PR summary comment when run in a GitHub Actions PR context. |
|
|
119
124
|
| `lint` | Lint code using ESLint |
|
|
120
125
|
| `prettier` | Format code using Prettier |
|
|
121
126
|
| `deps` | Check unused / missing dependencies via depcheck |
|
|
@@ -128,12 +133,16 @@ npx pr-checkmate <job>
|
|
|
128
133
|
| `pr-title` | Validate PR title against Conventional Commits format |
|
|
129
134
|
| `outdated` | Report packages that are major versions behind |
|
|
130
135
|
| `markdown` | Lint `.md`/`.mdx` files via markdownlint (warning-only, delta mode) |
|
|
136
|
+
| `typecheck` | Run `tsc --noEmit` to catch type errors (blocking). Skips automatically if no `tsconfig.json` is found. |
|
|
137
|
+
| `diff-smell` | Scan PR diff for `console.*()`, `debugger`, and `TODO/FIXME/HACK/XXX` in added lines (warning-only, delta mode) |
|
|
138
|
+
| `pr-body` | Check PR description for minimum length (20 chars) and a ticket reference (warning-only) |
|
|
139
|
+
| `duplicate` | Detect duplicate code with `jscpd` — min 5 lines / 50 tokens (warning-only, delta mode in PR context) |
|
|
131
140
|
|
|
132
141
|
<br>
|
|
133
142
|
|
|
134
143
|
### ⚡ Delta Mode
|
|
135
144
|
|
|
136
|
-
In a GitHub Actions PR context, `lint`, `prettier`, `spellcheck`, and `
|
|
145
|
+
In a GitHub Actions PR context, `lint`, `prettier`, `spellcheck`, `markdown`, `diff-smell`, and `duplicate` automatically switch to **delta mode** — only the files changed in the PR are checked instead of the entire project. This is transparent and requires no configuration.
|
|
137
146
|
|
|
138
147
|
Outside of a PR context (local runs, push workflows) the checks fall back to the full project scan.
|
|
139
148
|
|
|
@@ -324,6 +333,30 @@ For the auto-commit feature to work, ensure your repository has the correct perm
|
|
|
324
333
|
|
|
325
334
|
<br>
|
|
326
335
|
|
|
336
|
+
### PR summary comment
|
|
337
|
+
|
|
338
|
+
After `npx pr-checkmate all` finishes, it prints a summary table to the CI log and posts a comment on the PR. On repeated pushes the comment is updated in place — no duplicate comments accumulate.
|
|
339
|
+
|
|
340
|
+
The comment looks like this:
|
|
341
|
+
|
|
342
|
+
```text
|
|
343
|
+
## PR CheckMate Summary
|
|
344
|
+
|
|
345
|
+
**11 passed** · **3 warnings** · **0 failed** · **0 skipped**
|
|
346
|
+
|
|
347
|
+
| Check | Result |
|
|
348
|
+
|---|---|
|
|
349
|
+
| ESLint | pass |
|
|
350
|
+
| TypeScript | pass |
|
|
351
|
+
| Diff Smells | warn — 2x console.*(), 1x TODO comment |
|
|
352
|
+
| PR Body | warn — no ticket reference |
|
|
353
|
+
...
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
This requires `GITHUB_TOKEN` with `pull-requests: write` permission. The auto-generated workflow already includes both — no extra setup is needed.
|
|
357
|
+
|
|
358
|
+
<br>
|
|
359
|
+
|
|
327
360
|
## 🌍 Multilingual Spellcheck Support
|
|
328
361
|
|
|
329
362
|
PR CheckMate includes built-in dictionaries for:
|
package/dist/config/constants.js
CHANGED
|
@@ -84,6 +84,10 @@ exports.DEFAULT_COMMANDS = {
|
|
|
84
84
|
'npx pr-checkmate npm-audit': 'Run package vulnerabilities check',
|
|
85
85
|
'npx pr-checkmate security': 'Security scan',
|
|
86
86
|
'npx pr-checkmate spellcheck': 'Run spellcheck via cspell',
|
|
87
|
+
'npx pr-checkmate typecheck': 'TypeScript type check (tsc --noEmit)',
|
|
88
|
+
'npx pr-checkmate diff-smell': 'Scan git diff for code smells',
|
|
89
|
+
'npx pr-checkmate pr-body': 'Validate PR description',
|
|
90
|
+
'npx pr-checkmate duplicate': 'Detect duplicate code blocks (jscpd)',
|
|
87
91
|
};
|
|
88
92
|
exports.documentation = {
|
|
89
93
|
eslint: 'https://eslint.org/docs/latest/rules/',
|
package/dist/config/loader.js
CHANGED
|
@@ -54,6 +54,10 @@ function mergeWithDefaults(userConfig) {
|
|
|
54
54
|
],
|
|
55
55
|
},
|
|
56
56
|
security: userConfig.security || defaults.security,
|
|
57
|
+
typecheck: { ...defaults.typecheck, ...userConfig.typecheck },
|
|
58
|
+
diffSmell: { ...defaults.diffSmell, ...userConfig.diffSmell },
|
|
59
|
+
prBody: { ...defaults.prBody, ...userConfig.prBody },
|
|
60
|
+
duplicate: { ...defaults.duplicate, ...userConfig.duplicate },
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
@@ -110,6 +114,10 @@ function getDefaultConfig() {
|
|
|
110
114
|
gitleaks: { enabled: true },
|
|
111
115
|
'npm-audit': { enabled: true, auditLevel: 'moderate' },
|
|
112
116
|
},
|
|
117
|
+
typecheck: { enabled: true },
|
|
118
|
+
diffSmell: { enabled: true },
|
|
119
|
+
prBody: { enabled: true, minLength: 20, requireTicket: true },
|
|
120
|
+
duplicate: { enabled: true, minLines: 5, minTokens: 50 },
|
|
113
121
|
};
|
|
114
122
|
}
|
|
115
123
|
/**
|
|
@@ -263,3 +263,91 @@ jobs:
|
|
|
263
263
|
- name: Run Markdown Lint
|
|
264
264
|
run: npx pr-checkmate markdown
|
|
265
265
|
continue-on-error: true
|
|
266
|
+
|
|
267
|
+
typecheck:
|
|
268
|
+
name: TypeScript Type Check
|
|
269
|
+
runs-on: ubuntu-latest
|
|
270
|
+
steps:
|
|
271
|
+
- name: Checkout code
|
|
272
|
+
uses: actions/checkout@v4
|
|
273
|
+
with:
|
|
274
|
+
ref: ${{ github.head_ref }}
|
|
275
|
+
fetch-depth: 0
|
|
276
|
+
|
|
277
|
+
- name: Setup Node.js
|
|
278
|
+
uses: actions/setup-node@v4
|
|
279
|
+
with:
|
|
280
|
+
node-version: 20
|
|
281
|
+
|
|
282
|
+
- name: Install dependencies
|
|
283
|
+
run: npm ci
|
|
284
|
+
|
|
285
|
+
- name: Run TypeScript Type Check
|
|
286
|
+
run: npx pr-checkmate typecheck
|
|
287
|
+
continue-on-error: true
|
|
288
|
+
|
|
289
|
+
diff-smell:
|
|
290
|
+
name: Diff Code Smell
|
|
291
|
+
runs-on: ubuntu-latest
|
|
292
|
+
steps:
|
|
293
|
+
- name: Checkout code
|
|
294
|
+
uses: actions/checkout@v4
|
|
295
|
+
with:
|
|
296
|
+
ref: ${{ github.head_ref }}
|
|
297
|
+
fetch-depth: 0
|
|
298
|
+
|
|
299
|
+
- name: Setup Node.js
|
|
300
|
+
uses: actions/setup-node@v4
|
|
301
|
+
with:
|
|
302
|
+
node-version: 20
|
|
303
|
+
|
|
304
|
+
- name: Install dependencies
|
|
305
|
+
run: npm ci
|
|
306
|
+
|
|
307
|
+
- name: Scan Diff for Code Smells
|
|
308
|
+
run: npx pr-checkmate diff-smell
|
|
309
|
+
continue-on-error: true
|
|
310
|
+
|
|
311
|
+
pr-body:
|
|
312
|
+
name: PR Description
|
|
313
|
+
runs-on: ubuntu-latest
|
|
314
|
+
steps:
|
|
315
|
+
- name: Checkout code
|
|
316
|
+
uses: actions/checkout@v4
|
|
317
|
+
with:
|
|
318
|
+
ref: ${{ github.head_ref }}
|
|
319
|
+
fetch-depth: 0
|
|
320
|
+
|
|
321
|
+
- name: Setup Node.js
|
|
322
|
+
uses: actions/setup-node@v4
|
|
323
|
+
with:
|
|
324
|
+
node-version: 20
|
|
325
|
+
|
|
326
|
+
- name: Install dependencies
|
|
327
|
+
run: npm ci
|
|
328
|
+
|
|
329
|
+
- name: Check PR Description
|
|
330
|
+
run: npx pr-checkmate pr-body
|
|
331
|
+
continue-on-error: true
|
|
332
|
+
|
|
333
|
+
duplicate-check:
|
|
334
|
+
name: Duplicate Code
|
|
335
|
+
runs-on: ubuntu-latest
|
|
336
|
+
steps:
|
|
337
|
+
- name: Checkout code
|
|
338
|
+
uses: actions/checkout@v4
|
|
339
|
+
with:
|
|
340
|
+
ref: ${{ github.head_ref }}
|
|
341
|
+
fetch-depth: 0
|
|
342
|
+
|
|
343
|
+
- name: Setup Node.js
|
|
344
|
+
uses: actions/setup-node@v4
|
|
345
|
+
with:
|
|
346
|
+
node-version: 20
|
|
347
|
+
|
|
348
|
+
- name: Install dependencies
|
|
349
|
+
run: npm ci
|
|
350
|
+
|
|
351
|
+
- name: Check for Duplicate Code
|
|
352
|
+
run: npx pr-checkmate duplicate
|
|
353
|
+
continue-on-error: true
|
package/dist/init.js
CHANGED
|
@@ -37,6 +37,26 @@ const DEFAULT_CONFIG = {
|
|
|
37
37
|
rules: {},
|
|
38
38
|
ignores: [],
|
|
39
39
|
},
|
|
40
|
+
// TypeScript type check
|
|
41
|
+
typecheck: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
},
|
|
44
|
+
// Diff code smell detection
|
|
45
|
+
diffSmell: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
},
|
|
48
|
+
// PR description validation
|
|
49
|
+
prBody: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
minLength: 20,
|
|
52
|
+
requireTicket: true,
|
|
53
|
+
},
|
|
54
|
+
// Duplicate code detection (jscpd)
|
|
55
|
+
duplicate: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
minLines: 5,
|
|
58
|
+
minTokens: 50,
|
|
59
|
+
},
|
|
40
60
|
documentation: config_1.documentation,
|
|
41
61
|
};
|
|
42
62
|
async function init() {
|
|
@@ -117,6 +137,10 @@ async function init() {
|
|
|
117
137
|
eslint: { ...DEFAULT_CONFIG.eslint, ...(json.eslint || {}) },
|
|
118
138
|
prettier: { ...DEFAULT_CONFIG.prettier, ...(json.prettier || {}) },
|
|
119
139
|
cspell: { ...DEFAULT_CONFIG.cspell, ...(json.cspell || {}) },
|
|
140
|
+
typecheck: { ...DEFAULT_CONFIG.typecheck, ...(json.typecheck || {}) },
|
|
141
|
+
diffSmell: { ...DEFAULT_CONFIG.diffSmell, ...(json.diffSmell || {}) },
|
|
142
|
+
prBody: { ...DEFAULT_CONFIG.prBody, ...(json.prBody || {}) },
|
|
143
|
+
duplicate: { ...DEFAULT_CONFIG.duplicate, ...(json.duplicate || {}) },
|
|
120
144
|
};
|
|
121
145
|
fs_1.default.writeFileSync(exports.CONFIG_FILE, JSON.stringify(updated, null, 2));
|
|
122
146
|
utils_1.logger.info('[init]: 🔧 Updated pr-checkmate.json with customization options.');
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDiffSmell = runDiffSmell;
|
|
7
|
+
const execa_1 = require("execa");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const result_collector_1 = require("../utils/result-collector");
|
|
10
|
+
const loader_1 = require("../config/loader");
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const SMELL_PATTERNS = [
|
|
13
|
+
// console.* calls added in diff (not in comments)
|
|
14
|
+
{ pattern: /^\+(?!\+)(?:[^/]|\/[^/]).*\bconsole\.(log|error|warn|debug|info)\s*\(/, label: 'console.*()' },
|
|
15
|
+
{ pattern: /^\+(?!\+).*\bdebugger\b/, label: 'debugger statement' },
|
|
16
|
+
{ pattern: /^\+\s*\/\/\s*TODO\b/i, label: 'TODO comment' },
|
|
17
|
+
{ pattern: /^\+\s*\/\/\s*FIXME\b/i, label: 'FIXME comment' },
|
|
18
|
+
{ pattern: /^\+\s*\/\/\s*HACK\b/i, label: 'HACK comment' },
|
|
19
|
+
{ pattern: /^\+\s*\/\/\s*XXX\b/i, label: 'XXX marker' },
|
|
20
|
+
];
|
|
21
|
+
function readBaseSha() {
|
|
22
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
23
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
24
|
+
if (eventName !== 'pull_request' || !eventPath || !node_fs_1.default.existsSync(eventPath)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const event = JSON.parse(node_fs_1.default.readFileSync(eventPath, 'utf-8'));
|
|
29
|
+
return event?.pull_request?.base?.sha ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function runDiffSmell() {
|
|
36
|
+
const cfg = (0, loader_1.loadProjectConfig)();
|
|
37
|
+
if (cfg.diffSmell?.enabled === false) {
|
|
38
|
+
utils_1.logger.info('[runDiffSmell]: Disabled in pr-checkmate.json, skipping');
|
|
39
|
+
result_collector_1.resultCollector.register('Diff Smells', 'skip');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
utils_1.logger.info('[runDiffSmell]: Scanning diff for code smells...');
|
|
43
|
+
const baseSha = readBaseSha();
|
|
44
|
+
if (!baseSha) {
|
|
45
|
+
utils_1.logger.info('[runDiffSmell]: Not in GitHub PR context, skipping');
|
|
46
|
+
result_collector_1.resultCollector.register('Diff Smells', 'skip');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const { stdout } = await (0, execa_1.execa)('git', [
|
|
51
|
+
'diff',
|
|
52
|
+
'--unified=0',
|
|
53
|
+
'--diff-filter=ACMR',
|
|
54
|
+
baseSha,
|
|
55
|
+
'HEAD',
|
|
56
|
+
'--',
|
|
57
|
+
'*.ts',
|
|
58
|
+
'*.tsx',
|
|
59
|
+
'*.js',
|
|
60
|
+
'*.jsx',
|
|
61
|
+
]);
|
|
62
|
+
const lines = stdout.split('\n');
|
|
63
|
+
const findings = {};
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
for (const { pattern, label } of SMELL_PATTERNS) {
|
|
66
|
+
if (pattern.test(line)) {
|
|
67
|
+
if (!findings[label])
|
|
68
|
+
findings[label] = [];
|
|
69
|
+
// Strip leading + and trim for display
|
|
70
|
+
findings[label].push(line.slice(1).trim().substring(0, 100));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const totalFindings = Object.values(findings).reduce((sum, arr) => sum + arr.length, 0);
|
|
75
|
+
if (totalFindings === 0) {
|
|
76
|
+
utils_1.logger.info('[runDiffSmell]: ✅ No code smells detected in diff');
|
|
77
|
+
result_collector_1.resultCollector.register('Diff Smells', 'pass');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
utils_1.logger.warn(`[runDiffSmell]: ⚠️ Found ${totalFindings} code smell(s) in diff:`);
|
|
81
|
+
for (const [label, occurrences] of Object.entries(findings)) {
|
|
82
|
+
utils_1.logger.warn(` ${label}: ${occurrences.length} occurrence(s)`);
|
|
83
|
+
occurrences.slice(0, 3).forEach(line => utils_1.logger.warn(` ${line}`));
|
|
84
|
+
if (occurrences.length > 3) {
|
|
85
|
+
utils_1.logger.warn(` ... and ${occurrences.length - 3} more`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const summary = Object.entries(findings)
|
|
89
|
+
.map(([label, arr]) => `${arr.length}× ${label}`)
|
|
90
|
+
.join(', ');
|
|
91
|
+
result_collector_1.resultCollector.register('Diff Smells', 'warn', summary);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
utils_1.logger.warn(`[runDiffSmell]: ⚠️ Could not analyze diff\n${err}`);
|
|
95
|
+
result_collector_1.resultCollector.register('Diff Smells', 'skip');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDuplicateCheck = runDuplicateCheck;
|
|
7
|
+
const execa_1 = require("execa");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const result_collector_1 = require("../utils/result-collector");
|
|
10
|
+
const loader_1 = require("../config/loader");
|
|
11
|
+
const delta_1 = require("../utils/delta");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
async function runDuplicateCheck() {
|
|
15
|
+
const cfg = (0, loader_1.loadProjectConfig)();
|
|
16
|
+
if (cfg.duplicate?.enabled === false) {
|
|
17
|
+
utils_1.logger.info('[runDuplicateCheck]: Disabled in pr-checkmate.json, skipping');
|
|
18
|
+
result_collector_1.resultCollector.register('Duplicate Code', 'skip');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const minLines = cfg.duplicate?.minLines ?? 5;
|
|
22
|
+
const minTokens = cfg.duplicate?.minTokens ?? 50;
|
|
23
|
+
utils_1.logger.info('[runDuplicateCheck]: Checking for duplicate code...');
|
|
24
|
+
const projectRoot = process.cwd();
|
|
25
|
+
const changedFiles = await (0, delta_1.getChangedFiles)(['.ts', '.tsx', '.js', '.jsx']);
|
|
26
|
+
// In PR context but no relevant files changed
|
|
27
|
+
if (changedFiles !== null && changedFiles.length === 0) {
|
|
28
|
+
utils_1.logger.info('[runDuplicateCheck]: No relevant files changed, skipping');
|
|
29
|
+
result_collector_1.resultCollector.register('Duplicate Code', 'skip');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Fall back to src/ or project root when not in delta mode
|
|
33
|
+
const srcPath = node_path_1.default.join(projectRoot, 'src');
|
|
34
|
+
const targetPaths = changedFiles ?? (node_fs_1.default.existsSync(srcPath) ? [srcPath] : [projectRoot]);
|
|
35
|
+
try {
|
|
36
|
+
const result = await (0, execa_1.execa)('npx', [
|
|
37
|
+
'jscpd',
|
|
38
|
+
'--min-lines',
|
|
39
|
+
String(minLines),
|
|
40
|
+
'--min-tokens',
|
|
41
|
+
String(minTokens),
|
|
42
|
+
'--reporters',
|
|
43
|
+
'console',
|
|
44
|
+
'--ignore',
|
|
45
|
+
'node_modules/**,dist/**,.git/**',
|
|
46
|
+
'--quiet',
|
|
47
|
+
...targetPaths,
|
|
48
|
+
], { reject: false });
|
|
49
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
50
|
+
const clonesMatch = output.match(/(\d+)\s+clone/i);
|
|
51
|
+
const cloneCount = clonesMatch ? parseInt(clonesMatch[1], 10) : 0;
|
|
52
|
+
if (cloneCount > 0) {
|
|
53
|
+
utils_1.logger.warn(`[runDuplicateCheck]: ⚠️ Found ${cloneCount} duplicate block(s)`);
|
|
54
|
+
if (output)
|
|
55
|
+
utils_1.logger.warn(output);
|
|
56
|
+
result_collector_1.resultCollector.register('Duplicate Code', 'warn', `${cloneCount} duplicate block(s)`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
utils_1.logger.info('[runDuplicateCheck]: ✅ No significant code duplication found');
|
|
60
|
+
result_collector_1.resultCollector.register('Duplicate Code', 'pass');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
utils_1.logger.warn(`[runDuplicateCheck]: ⚠️ Could not run duplicate check\n${err}`);
|
|
65
|
+
result_collector_1.resultCollector.register('Duplicate Code', 'skip');
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/scripts/index.js
CHANGED
|
@@ -26,7 +26,13 @@ const pr_size_check_1 = require("./pr-size-check");
|
|
|
26
26
|
const pr_title_lint_1 = require("./pr-title-lint");
|
|
27
27
|
const outdated_deps_1 = require("./outdated-deps");
|
|
28
28
|
const markdown_lint_1 = require("./markdown-lint");
|
|
29
|
+
const typecheck_1 = require("./typecheck");
|
|
30
|
+
const diff_smell_1 = require("./diff-smell");
|
|
31
|
+
const pr_body_1 = require("./pr-body");
|
|
32
|
+
const duplicate_check_1 = require("./duplicate-check");
|
|
33
|
+
const pr_summary_1 = require("./pr-summary");
|
|
29
34
|
const utils_1 = require("../utils");
|
|
35
|
+
const result_collector_1 = require("../utils/result-collector");
|
|
30
36
|
const lint_1 = require("./lint");
|
|
31
37
|
__exportStar(require("./dependency-check"), exports);
|
|
32
38
|
__exportStar(require("./lint"), exports);
|
|
@@ -39,35 +45,69 @@ __exportStar(require("./pr-size-check"), exports);
|
|
|
39
45
|
__exportStar(require("./pr-title-lint"), exports);
|
|
40
46
|
__exportStar(require("./outdated-deps"), exports);
|
|
41
47
|
__exportStar(require("./markdown-lint"), exports);
|
|
48
|
+
__exportStar(require("./typecheck"), exports);
|
|
49
|
+
__exportStar(require("./diff-smell"), exports);
|
|
50
|
+
__exportStar(require("./pr-body"), exports);
|
|
51
|
+
__exportStar(require("./duplicate-check"), exports);
|
|
52
|
+
__exportStar(require("./pr-summary"), exports);
|
|
53
|
+
/**
|
|
54
|
+
* Wraps a blocking check: registers 'pass' on success, 'fail' on throw (and re-throws).
|
|
55
|
+
*/
|
|
56
|
+
async function trackCheck(name, fn) {
|
|
57
|
+
try {
|
|
58
|
+
await fn();
|
|
59
|
+
result_collector_1.resultCollector.register(name, 'pass');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const msg = err instanceof Error ? err.message.split('\n')[0].substring(0, 120) : String(err);
|
|
63
|
+
result_collector_1.resultCollector.register(name, 'fail', msg);
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Wraps an informational check: registers 'pass' on success, 'warn' on throw (never re-throws).
|
|
69
|
+
*/
|
|
70
|
+
async function trackSoftCheck(name, fn) {
|
|
71
|
+
try {
|
|
72
|
+
await fn();
|
|
73
|
+
result_collector_1.resultCollector.register(name, 'pass');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const msg = err instanceof Error ? err.message.split('\n')[0].substring(0, 120) : String(err);
|
|
77
|
+
result_collector_1.resultCollector.register(name, 'warn', msg);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
42
80
|
async function runJob(jobName) {
|
|
43
81
|
utils_1.logger.info(`[runJob]: 🚀 Running job: ${jobName}`);
|
|
44
82
|
try {
|
|
45
83
|
switch (jobName) {
|
|
46
84
|
case 'all':
|
|
47
|
-
// Phase 1: blocking
|
|
85
|
+
// Phase 1: blocking quality gates — run in parallel, fail fast on any throw
|
|
48
86
|
await Promise.all([
|
|
49
|
-
(
|
|
50
|
-
(
|
|
51
|
-
(
|
|
52
|
-
(
|
|
53
|
-
(
|
|
54
|
-
(
|
|
87
|
+
trackCheck('ESLint', lint_1.runLint),
|
|
88
|
+
trackCheck('Dependencies', dependency_check_1.runDependencyCheck),
|
|
89
|
+
trackCheck('NPM Audit', npm_audit_1.runNpmAudit),
|
|
90
|
+
trackCheck('Security Scan', security_1.runSecurityScan),
|
|
91
|
+
trackCheck('Circular Deps', circular_deps_1.runCircularDeps),
|
|
92
|
+
trackCheck('License Check', license_check_1.runLicenseCheck),
|
|
93
|
+
trackCheck('TypeScript', typecheck_1.runTypecheck),
|
|
55
94
|
]);
|
|
56
|
-
// Phase 2: informational checks
|
|
95
|
+
// Phase 2: informational checks — old scripts tracked via wrapper,
|
|
96
|
+
// new scripts register themselves with accurate warn/skip status
|
|
57
97
|
await Promise.all([
|
|
58
|
-
(
|
|
59
|
-
(
|
|
60
|
-
(
|
|
61
|
-
(
|
|
62
|
-
(
|
|
98
|
+
trackSoftCheck('PR Size', pr_size_check_1.runPrSizeCheck),
|
|
99
|
+
trackSoftCheck('PR Title', pr_title_lint_1.runPrTitleLint),
|
|
100
|
+
trackSoftCheck('Outdated Deps', outdated_deps_1.runOutdatedDeps),
|
|
101
|
+
trackSoftCheck('Markdown', markdown_lint_1.runMarkdownLint),
|
|
102
|
+
trackSoftCheck('Spellcheck', spellcheck_1.runSpellcheck),
|
|
103
|
+
(0, diff_smell_1.runDiffSmell)(),
|
|
104
|
+
(0, pr_body_1.runPrBodyCheck)(),
|
|
105
|
+
(0, duplicate_check_1.runDuplicateCheck)(),
|
|
63
106
|
]);
|
|
64
|
-
// Phase 3: prettier last —
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
catch {
|
|
69
|
-
utils_1.logger.warn('⚠️ Prettier failed, continuing...');
|
|
70
|
-
}
|
|
107
|
+
// Phase 3: prettier last — may write files, then auto-commit in CI
|
|
108
|
+
await trackSoftCheck('Prettier', prettier_autoformat_1.runPrettier);
|
|
109
|
+
// Phase 4: print summary table + post GitHub PR comment
|
|
110
|
+
await (0, pr_summary_1.runPrSummary)();
|
|
71
111
|
break;
|
|
72
112
|
case 'lint':
|
|
73
113
|
await (0, lint_1.runLint)();
|
|
@@ -106,8 +146,20 @@ async function runJob(jobName) {
|
|
|
106
146
|
case 'markdown':
|
|
107
147
|
await (0, markdown_lint_1.runMarkdownLint)();
|
|
108
148
|
break;
|
|
149
|
+
case 'typecheck':
|
|
150
|
+
await (0, typecheck_1.runTypecheck)();
|
|
151
|
+
break;
|
|
152
|
+
case 'diff-smell':
|
|
153
|
+
await (0, diff_smell_1.runDiffSmell)();
|
|
154
|
+
break;
|
|
155
|
+
case 'pr-body':
|
|
156
|
+
await (0, pr_body_1.runPrBodyCheck)();
|
|
157
|
+
break;
|
|
158
|
+
case 'duplicate':
|
|
159
|
+
await (0, duplicate_check_1.runDuplicateCheck)();
|
|
160
|
+
break;
|
|
109
161
|
default:
|
|
110
|
-
utils_1.logger.warn(`[runJob]: Unknown job "${jobName}". Available: all, lint, deps, npm-audit, prettier, spellcheck, security, circular, license, pr-size, pr-title, outdated, markdown`);
|
|
162
|
+
utils_1.logger.warn(`[runJob]: Unknown job "${jobName}". Available: all, lint, deps, npm-audit, prettier, spellcheck, security, circular, license, pr-size, pr-title, outdated, markdown, typecheck, diff-smell, pr-body, duplicate`);
|
|
111
163
|
}
|
|
112
164
|
}
|
|
113
165
|
catch (err) {
|
|
@@ -8,8 +8,16 @@ const utils_1 = require("../utils");
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
10
|
const execa_1 = require("execa");
|
|
11
|
+
const config_1 = require("../config");
|
|
12
|
+
const SEVERITY_ORDER = ['info', 'low', 'moderate', 'high', 'critical'];
|
|
13
|
+
function isSeverityAtLeast(severity, level) {
|
|
14
|
+
const sevIdx = SEVERITY_ORDER.indexOf(severity);
|
|
15
|
+
const lvlIdx = SEVERITY_ORDER.indexOf(level);
|
|
16
|
+
return sevIdx >= 0 && lvlIdx >= 0 && sevIdx >= lvlIdx;
|
|
17
|
+
}
|
|
11
18
|
/**
|
|
12
|
-
* Run npm audit on the current project
|
|
19
|
+
* Run npm audit on the current project.
|
|
20
|
+
* Only fails on vulnerabilities in direct dependencies — transitive ones are silently ignored.
|
|
13
21
|
*/
|
|
14
22
|
async function runNpmAudit() {
|
|
15
23
|
utils_1.logger.info('Running npm audit...');
|
|
@@ -19,25 +27,27 @@ async function runNpmAudit() {
|
|
|
19
27
|
utils_1.logger.warn('[runNpmAudit]: ⚠️ No package.json found in project root — skipping npm audit');
|
|
20
28
|
return;
|
|
21
29
|
}
|
|
30
|
+
const config = (0, config_1.loadProjectConfig)();
|
|
31
|
+
const auditLevel = config.security?.['npm-audit']?.auditLevel ?? 'moderate';
|
|
32
|
+
let report;
|
|
22
33
|
try {
|
|
23
|
-
const { stdout
|
|
34
|
+
const { stdout } = await (0, execa_1.execa)('npm', ['audit', '--json'], {
|
|
24
35
|
cwd: projectRoot,
|
|
36
|
+
reject: false,
|
|
25
37
|
});
|
|
26
|
-
|
|
27
|
-
utils_1.logger.info(stdout);
|
|
28
|
-
if (stderr)
|
|
29
|
-
utils_1.logger.warn(stderr);
|
|
30
|
-
utils_1.logger.info('[runNpmAudit]: ✅ NPM audit finished successfully');
|
|
38
|
+
report = JSON.parse(stdout);
|
|
31
39
|
}
|
|
32
40
|
catch (error) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
utils_1.logger.error('[runNpmAudit]: ❌ Failed to parse npm audit output');
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
const directVulns = Object.values(report.vulnerabilities ?? {}).filter(v => v.isDirect && isSeverityAtLeast(v.severity, auditLevel));
|
|
45
|
+
if (directVulns.length === 0) {
|
|
46
|
+
utils_1.logger.info('[runNpmAudit]: ✅ NPM audit finished successfully');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const v of directVulns) {
|
|
50
|
+
utils_1.logger.error(`[runNpmAudit]: ❌ ${v.name} — severity: ${v.severity}`);
|
|
42
51
|
}
|
|
52
|
+
throw new Error(`[runNpmAudit]: ${directVulns.length} direct dependency vulnerability(ies) found at level "${auditLevel}" or above`);
|
|
43
53
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runPrBodyCheck = runPrBodyCheck;
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
const result_collector_1 = require("../utils/result-collector");
|
|
9
|
+
const loader_1 = require("../config/loader");
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
// Matches "Closes #123", "Fixes #45", "Resolves #7" or JIRA-style "PROJ-456"
|
|
12
|
+
const TICKET_PATTERN = /(?:closes?|fixes?|resolves?)\s+#\d+|[A-Z][A-Z0-9]+-\d+/i;
|
|
13
|
+
async function runPrBodyCheck() {
|
|
14
|
+
const cfg = (0, loader_1.loadProjectConfig)();
|
|
15
|
+
if (cfg.prBody?.enabled === false) {
|
|
16
|
+
utils_1.logger.info('[runPrBodyCheck]: Disabled in pr-checkmate.json, skipping');
|
|
17
|
+
result_collector_1.resultCollector.register('PR Body', 'skip');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const minLength = cfg.prBody?.minLength ?? 20;
|
|
21
|
+
const requireTicket = cfg.prBody?.requireTicket ?? true;
|
|
22
|
+
utils_1.logger.info('[runPrBodyCheck]: Checking PR description...');
|
|
23
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
24
|
+
if (!eventPath || !node_fs_1.default.existsSync(eventPath)) {
|
|
25
|
+
utils_1.logger.info('[runPrBodyCheck]: Not in GitHub CI, skipping');
|
|
26
|
+
result_collector_1.resultCollector.register('PR Body', 'skip');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let event;
|
|
30
|
+
try {
|
|
31
|
+
event = JSON.parse(node_fs_1.default.readFileSync(eventPath, 'utf-8'));
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
utils_1.logger.warn(`[runPrBodyCheck]: ⚠️ Could not read GitHub event\n${err}`);
|
|
35
|
+
result_collector_1.resultCollector.register('PR Body', 'skip');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const pr = event.pull_request;
|
|
39
|
+
if (!pr) {
|
|
40
|
+
utils_1.logger.info('[runPrBodyCheck]: No PR data found, skipping');
|
|
41
|
+
result_collector_1.resultCollector.register('PR Body', 'skip');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const body = pr.body ?? '';
|
|
45
|
+
const trimmed = body.trim();
|
|
46
|
+
const issues = [];
|
|
47
|
+
if (trimmed.length < minLength) {
|
|
48
|
+
issues.push(`description too short (${trimmed.length}/${minLength} chars min)`);
|
|
49
|
+
}
|
|
50
|
+
if (requireTicket && trimmed && !TICKET_PATTERN.test(trimmed)) {
|
|
51
|
+
issues.push('no ticket reference (e.g. "Closes #123" or "PROJ-456")');
|
|
52
|
+
}
|
|
53
|
+
if (issues.length > 0) {
|
|
54
|
+
utils_1.logger.warn('[runPrBodyCheck]: ⚠️ PR description issues:');
|
|
55
|
+
issues.forEach(issue => utils_1.logger.warn(` • ${issue}`));
|
|
56
|
+
result_collector_1.resultCollector.register('PR Body', 'warn', issues.join('; '));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
utils_1.logger.info('[runPrBodyCheck]: ✅ PR description looks good');
|
|
60
|
+
result_collector_1.resultCollector.register('PR Body', 'pass');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runPrSummary = runPrSummary;
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
const result_collector_1 = require("../utils/result-collector");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const STATUS_ICON = {
|
|
11
|
+
pass: '✅',
|
|
12
|
+
warn: '⚠️',
|
|
13
|
+
fail: '❌',
|
|
14
|
+
skip: '⏭️',
|
|
15
|
+
};
|
|
16
|
+
// Unique marker so we can find and update the comment on re-runs
|
|
17
|
+
const COMMENT_MARKER = '<!-- pr-checkmate-summary -->';
|
|
18
|
+
function getPrContext() {
|
|
19
|
+
const token = process.env.GITHUB_TOKEN;
|
|
20
|
+
const repository = process.env.GITHUB_REPOSITORY;
|
|
21
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
22
|
+
if (!token || !repository || !eventPath || !node_fs_1.default.existsSync(eventPath)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const event = JSON.parse(node_fs_1.default.readFileSync(eventPath, 'utf-8'));
|
|
27
|
+
const prNumber = event?.pull_request?.number;
|
|
28
|
+
if (!prNumber)
|
|
29
|
+
return null;
|
|
30
|
+
const [owner, repo] = repository.split('/');
|
|
31
|
+
return { token, owner, repo, prNumber };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildMarkdownReport(results) {
|
|
38
|
+
const { passed, warned, failed, skipped } = result_collector_1.resultCollector.getSummary();
|
|
39
|
+
const rows = results
|
|
40
|
+
.map(r => {
|
|
41
|
+
const icon = STATUS_ICON[r.status];
|
|
42
|
+
const details = r.details ? ` — ${r.details}` : '';
|
|
43
|
+
return `| ${r.name} | ${icon} ${r.status}${details} |`;
|
|
44
|
+
})
|
|
45
|
+
.join('\n');
|
|
46
|
+
return [
|
|
47
|
+
COMMENT_MARKER,
|
|
48
|
+
'## 🔍 PR CheckMate Summary',
|
|
49
|
+
'',
|
|
50
|
+
`**${passed} passed** · **${warned} warnings** · **${failed} failed** · **${skipped} skipped**`,
|
|
51
|
+
'',
|
|
52
|
+
'| Check | Result |',
|
|
53
|
+
'|---|---|',
|
|
54
|
+
rows,
|
|
55
|
+
'',
|
|
56
|
+
'_Generated by [PR CheckMate](https://www.npmjs.com/package/pr-checkmate)_',
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
async function upsertGithubComment(body) {
|
|
60
|
+
const ctx = getPrContext();
|
|
61
|
+
if (!ctx) {
|
|
62
|
+
utils_1.logger.info('[runPrSummary]: Skipping GitHub comment (not in PR context or GITHUB_TOKEN missing)');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const { token, owner, repo, prNumber } = ctx;
|
|
66
|
+
const apiBase = `https://api.github.com/repos/${owner}/${repo}`;
|
|
67
|
+
const headers = {
|
|
68
|
+
Authorization: `Bearer ${token}`,
|
|
69
|
+
'User-Agent': 'pr-checkmate',
|
|
70
|
+
Accept: 'application/vnd.github.v3+json',
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
// Look for an existing pr-checkmate comment to update (avoids spam on repeated pushes)
|
|
75
|
+
const listRes = await fetch(`${apiBase}/issues/${prNumber}/comments`, { headers });
|
|
76
|
+
if (listRes.ok) {
|
|
77
|
+
const comments = (await listRes.json());
|
|
78
|
+
const existing = comments.find(c => typeof c.body === 'string' && c.body.includes(COMMENT_MARKER));
|
|
79
|
+
if (existing) {
|
|
80
|
+
await fetch(`${apiBase}/issues/comments/${existing.id}`, {
|
|
81
|
+
method: 'PATCH',
|
|
82
|
+
headers,
|
|
83
|
+
body: JSON.stringify({ body }),
|
|
84
|
+
});
|
|
85
|
+
utils_1.logger.info('[runPrSummary]: ✅ Updated PR CheckMate comment on PR');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// No existing comment — post a new one
|
|
90
|
+
await fetch(`${apiBase}/issues/${prNumber}/comments`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers,
|
|
93
|
+
body: JSON.stringify({ body }),
|
|
94
|
+
});
|
|
95
|
+
utils_1.logger.info('[runPrSummary]: ✅ Posted PR CheckMate summary comment to PR');
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
utils_1.logger.warn(`[runPrSummary]: ⚠️ Could not post GitHub comment: ${err}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function runPrSummary() {
|
|
102
|
+
const results = result_collector_1.resultCollector.getResults();
|
|
103
|
+
if (results.length === 0)
|
|
104
|
+
return;
|
|
105
|
+
const { passed, warned, failed, skipped } = result_collector_1.resultCollector.getSummary();
|
|
106
|
+
utils_1.logger.info('');
|
|
107
|
+
utils_1.logger.info('═══════════════════════════════════════════════════════════════');
|
|
108
|
+
utils_1.logger.info(' PR CheckMate Summary');
|
|
109
|
+
utils_1.logger.info('═══════════════════════════════════════════════════════════════');
|
|
110
|
+
for (const r of results) {
|
|
111
|
+
const icon = STATUS_ICON[r.status];
|
|
112
|
+
const details = r.details ? `: ${r.details}` : '';
|
|
113
|
+
utils_1.logger.info(` ${icon} ${r.name}${details}`);
|
|
114
|
+
}
|
|
115
|
+
utils_1.logger.info('───────────────────────────────────────────────────────────────');
|
|
116
|
+
utils_1.logger.info(` ✅ ${passed} passed ⚠️ ${warned} warnings ❌ ${failed} failed ⏭️ ${skipped} skipped`);
|
|
117
|
+
utils_1.logger.info('═══════════════════════════════════════════════════════════════');
|
|
118
|
+
await upsertGithubComment(buildMarkdownReport(results));
|
|
119
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runTypecheck = runTypecheck;
|
|
7
|
+
const execa_1 = require("execa");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const loader_1 = require("../config/loader");
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
async function runTypecheck() {
|
|
13
|
+
const cfg = (0, loader_1.loadProjectConfig)();
|
|
14
|
+
if (cfg.typecheck?.enabled === false) {
|
|
15
|
+
utils_1.logger.info('[runTypecheck]: Disabled in pr-checkmate.json, skipping');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
utils_1.logger.info('[runTypecheck]: Running TypeScript type check...');
|
|
19
|
+
const tsConfigPath = node_path_1.default.join(process.cwd(), 'tsconfig.json');
|
|
20
|
+
if (!node_fs_1.default.existsSync(tsConfigPath)) {
|
|
21
|
+
utils_1.logger.info('[runTypecheck]: No tsconfig.json found, skipping');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const result = await (0, execa_1.execa)('npx', ['tsc', '--noEmit'], { reject: false });
|
|
25
|
+
if (result.exitCode !== 0) {
|
|
26
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
27
|
+
const errorCount = (output.match(/error TS\d+/g) ?? []).length;
|
|
28
|
+
utils_1.logger.warn(`[runTypecheck]: ⚠️ TypeScript found ${errorCount} type error(s):`);
|
|
29
|
+
if (output)
|
|
30
|
+
utils_1.logger.warn(output);
|
|
31
|
+
throw new Error(`TypeScript: ${errorCount} type error(s)`);
|
|
32
|
+
}
|
|
33
|
+
utils_1.logger.info('[runTypecheck]: ✅ TypeScript type check passed');
|
|
34
|
+
}
|
package/dist/utils/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
22
22
|
const logger_1 = require("./logger");
|
|
23
23
|
__exportStar(require("./logger"), exports);
|
|
24
24
|
__exportStar(require("./delta"), exports);
|
|
25
|
+
__exportStar(require("./result-collector"), exports);
|
|
25
26
|
function cleanupTempFile(filePath) {
|
|
26
27
|
try {
|
|
27
28
|
if (node_fs_1.default.existsSync(filePath)) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resultCollector = void 0;
|
|
4
|
+
class ResultCollector {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.results = [];
|
|
7
|
+
}
|
|
8
|
+
register(name, status, details) {
|
|
9
|
+
const idx = this.results.findIndex(r => r.name === name);
|
|
10
|
+
const entry = { name, status, ...(details && { details }) };
|
|
11
|
+
if (idx >= 0) {
|
|
12
|
+
this.results[idx] = entry;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.results.push(entry);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
getResults() {
|
|
19
|
+
return [...this.results];
|
|
20
|
+
}
|
|
21
|
+
clear() {
|
|
22
|
+
this.results = [];
|
|
23
|
+
}
|
|
24
|
+
getSummary() {
|
|
25
|
+
return {
|
|
26
|
+
passed: this.results.filter(r => r.status === 'pass').length,
|
|
27
|
+
warned: this.results.filter(r => r.status === 'warn').length,
|
|
28
|
+
failed: this.results.filter(r => r.status === 'fail').length,
|
|
29
|
+
skipped: this.results.filter(r => r.status === 'skip').length,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.resultCollector = new ResultCollector();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-checkmate",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.2",
|
|
4
4
|
"description": "Automated PR quality checks: linting, formatting, dependency analysis, and spellcheck",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"eslint-plugin-prettier": "^5.5.4",
|
|
75
75
|
"execa": "^9.6.0",
|
|
76
76
|
"gitleaks-secret-scanner-fixed": "^2.1.3",
|
|
77
|
+
"jscpd": "^3.5.10",
|
|
77
78
|
"markdownlint-cli": "^0.45.0",
|
|
78
79
|
"inquirer": "^13.0.1",
|
|
79
80
|
"prettier": ">=3.0.0",
|
|
@@ -263,3 +263,91 @@ jobs:
|
|
|
263
263
|
- name: Run Markdown Lint
|
|
264
264
|
run: npx pr-checkmate markdown
|
|
265
265
|
continue-on-error: true
|
|
266
|
+
|
|
267
|
+
typecheck:
|
|
268
|
+
name: TypeScript Type Check
|
|
269
|
+
runs-on: ubuntu-latest
|
|
270
|
+
steps:
|
|
271
|
+
- name: Checkout code
|
|
272
|
+
uses: actions/checkout@v4
|
|
273
|
+
with:
|
|
274
|
+
ref: ${{ github.head_ref }}
|
|
275
|
+
fetch-depth: 0
|
|
276
|
+
|
|
277
|
+
- name: Setup Node.js
|
|
278
|
+
uses: actions/setup-node@v4
|
|
279
|
+
with:
|
|
280
|
+
node-version: 20
|
|
281
|
+
|
|
282
|
+
- name: Install dependencies
|
|
283
|
+
run: npm ci
|
|
284
|
+
|
|
285
|
+
- name: Run TypeScript Type Check
|
|
286
|
+
run: npx pr-checkmate typecheck
|
|
287
|
+
continue-on-error: true
|
|
288
|
+
|
|
289
|
+
diff-smell:
|
|
290
|
+
name: Diff Code Smell
|
|
291
|
+
runs-on: ubuntu-latest
|
|
292
|
+
steps:
|
|
293
|
+
- name: Checkout code
|
|
294
|
+
uses: actions/checkout@v4
|
|
295
|
+
with:
|
|
296
|
+
ref: ${{ github.head_ref }}
|
|
297
|
+
fetch-depth: 0
|
|
298
|
+
|
|
299
|
+
- name: Setup Node.js
|
|
300
|
+
uses: actions/setup-node@v4
|
|
301
|
+
with:
|
|
302
|
+
node-version: 20
|
|
303
|
+
|
|
304
|
+
- name: Install dependencies
|
|
305
|
+
run: npm ci
|
|
306
|
+
|
|
307
|
+
- name: Scan Diff for Code Smells
|
|
308
|
+
run: npx pr-checkmate diff-smell
|
|
309
|
+
continue-on-error: true
|
|
310
|
+
|
|
311
|
+
pr-body:
|
|
312
|
+
name: PR Description
|
|
313
|
+
runs-on: ubuntu-latest
|
|
314
|
+
steps:
|
|
315
|
+
- name: Checkout code
|
|
316
|
+
uses: actions/checkout@v4
|
|
317
|
+
with:
|
|
318
|
+
ref: ${{ github.head_ref }}
|
|
319
|
+
fetch-depth: 0
|
|
320
|
+
|
|
321
|
+
- name: Setup Node.js
|
|
322
|
+
uses: actions/setup-node@v4
|
|
323
|
+
with:
|
|
324
|
+
node-version: 20
|
|
325
|
+
|
|
326
|
+
- name: Install dependencies
|
|
327
|
+
run: npm ci
|
|
328
|
+
|
|
329
|
+
- name: Check PR Description
|
|
330
|
+
run: npx pr-checkmate pr-body
|
|
331
|
+
continue-on-error: true
|
|
332
|
+
|
|
333
|
+
duplicate-check:
|
|
334
|
+
name: Duplicate Code
|
|
335
|
+
runs-on: ubuntu-latest
|
|
336
|
+
steps:
|
|
337
|
+
- name: Checkout code
|
|
338
|
+
uses: actions/checkout@v4
|
|
339
|
+
with:
|
|
340
|
+
ref: ${{ github.head_ref }}
|
|
341
|
+
fetch-depth: 0
|
|
342
|
+
|
|
343
|
+
- name: Setup Node.js
|
|
344
|
+
uses: actions/setup-node@v4
|
|
345
|
+
with:
|
|
346
|
+
node-version: 20
|
|
347
|
+
|
|
348
|
+
- name: Install dependencies
|
|
349
|
+
run: npm ci
|
|
350
|
+
|
|
351
|
+
- name: Check for Duplicate Code
|
|
352
|
+
run: npx pr-checkmate duplicate
|
|
353
|
+
continue-on-error: true
|