pr-checkmate 1.23.18 → 1.23.19
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 +0 -3
- package/cspell.json +2 -0
- package/dist/config/constants.js +1 -0
- package/dist/core/checks/dependencies/circular-deps.js +132 -14
- package/dist/core/checks/dependencies/dependency-check.d.ts +29 -0
- package/dist/core/checks/dependencies/dependency-check.js +38 -12
- package/dist/core/checks/dependencies/npm-audit.js +22 -1
- package/dist/core/checks/languages/lint.js +10 -1
- package/dist/core/checks/languages/typecheck.js +7 -0
- package/dist/core/checks/quality/markdown-lint.js +10 -1
- package/dist/core/checks/security/security.js +12 -1
- package/dist/init.js +13 -0
- package/dist/interfaces/index.d.ts +6 -0
- package/package.json +2 -1
- package/pr-checkmate.schema.json +9 -0
- package/src/config/cspell.json +2 -0
- package/src/config/pr-checkmate.schema.json +9 -0
package/README.md
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|

|
|
11
11
|
[](https://pr-checkmate-docs.pages.dev)
|
|
12
12
|
[](https://github.com/Mybono/pr_checkmate_public/releases/latest)
|
|
13
|
-
[](https://scorecard.dev/viewer/?uri=github.com/Mybono/pr_checkmate_public)
|
|
14
13
|
|
|
15
14
|
> PR CheckMate is a universal PR quality checker: one `npm install`, 51 checks across
|
|
16
15
|
> 11 languages, running in CI on every pull request. The bundled languages need no
|
|
@@ -20,8 +19,6 @@ Every release is published with a CycloneDX SBOM and an independent
|
|
|
20
19
|
[VirusTotal scan report](https://github.com/Mybono/pr_checkmate_public/releases/latest)
|
|
21
20
|
of the exact npm tarball, attached to that version's
|
|
22
21
|
[GitHub release notes](https://github.com/Mybono/pr_checkmate_public/releases/latest).
|
|
23
|
-
The public repo also runs an [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/Mybono/pr_checkmate_public)
|
|
24
|
-
analysis of its own supply-chain hygiene.
|
|
25
22
|
|
|
26
23
|
## Why PR CheckMate
|
|
27
24
|
|
package/cspell.json
CHANGED
package/dist/config/constants.js
CHANGED
|
@@ -1,33 +1,151 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.circularDepsCheck = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
4
9
|
const config_1 = require("../../../config");
|
|
5
10
|
const utils_1 = require("../../../utils");
|
|
6
11
|
const execa_1 = require("../../../utils/execa");
|
|
7
12
|
const targets_1 = require("../../targets");
|
|
8
13
|
const types_1 = require("../../types");
|
|
14
|
+
const findings_1 = require("../findings");
|
|
15
|
+
/**
|
|
16
|
+
* Cap for the analysis. dpdm builds the whole module graph, which on a large repo
|
|
17
|
+
* is the slowest part of a run — an uncapped analysis looks like a hung commit.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
20
|
+
/**
|
|
21
|
+
* Ceiling for the assembled argument list. Passing every tracked source file as a
|
|
22
|
+
* separate argument overflows the OS argument limit (`E2BIG`) on a large repo, and
|
|
23
|
+
* a spawn that never starts writes no report. Past the ceiling dpdm gets globs
|
|
24
|
+
* instead — it expands them itself, over the same source paths.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_ARGV_CHARS = 100000;
|
|
27
|
+
/** Extensions dpdm resolves for us; also the globs used in the fallback. */
|
|
28
|
+
const SOURCE_GLOBS = ['*.ts', '*.tsx', '*.js', '*.jsx'];
|
|
29
|
+
/**
|
|
30
|
+
* Resolve dpdm, preferring the consumer project's own copy (so a repo pinned to a
|
|
31
|
+
* different major keeps its own behaviour) and falling back to the copy bundled
|
|
32
|
+
* with pr-checkmate — the order `lint` and `typecheck` use for eslint/tsc.
|
|
33
|
+
*
|
|
34
|
+
* Returns null instead of throwing: an unresolvable analyzer is a skip, never a
|
|
35
|
+
* failed PR. This check used to shell out to `npx dpdm`, which silently fetched
|
|
36
|
+
* dpdm from the registry on first use — and when that fetch could not happen
|
|
37
|
+
* (offline or locked-down CI, private registry, exit 127) nothing was analyzed.
|
|
38
|
+
*/
|
|
39
|
+
function resolveDpdm(cwd) {
|
|
40
|
+
for (const options of [{ paths: [cwd] }, undefined]) {
|
|
41
|
+
try {
|
|
42
|
+
return (0, utils_1.resolvePackageBin)('dpdm', 'lib/bin/dpdm.js', options);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Try the next location; both failing is a skip, handled by the caller.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the file arguments for dpdm, falling back to globs when the explicit list
|
|
52
|
+
* would overflow the OS argument limit. `sourcePaths` scopes the globs the same way
|
|
53
|
+
* `listTrackedFiles` scopes the explicit list.
|
|
54
|
+
*/
|
|
55
|
+
function toFileArgs(files, sourcePaths) {
|
|
56
|
+
const explicitLength = files.reduce((sum, file) => sum + file.length + 1, 0);
|
|
57
|
+
if (explicitLength <= MAX_ARGV_CHARS)
|
|
58
|
+
return files;
|
|
59
|
+
const roots = sourcePaths.length > 0 ? sourcePaths : ['.'];
|
|
60
|
+
const globs = roots.flatMap(root => SOURCE_GLOBS.map(ext => `${root.replace(/\/+$/, '')}/**/${ext}`));
|
|
61
|
+
utils_1.logger.info(`[Circular Deps]: ${files.length} files exceed the argument limit — analyzing via globs (${globs.join(', ')})`);
|
|
62
|
+
return globs;
|
|
63
|
+
}
|
|
64
|
+
/** Read dpdm's report file; null when it is absent or unparseable. */
|
|
65
|
+
function readReport(reportPath) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(node_fs_1.default.readFileSync(reportPath, 'utf-8'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
9
73
|
exports.circularDepsCheck = {
|
|
10
74
|
name: 'Circular Deps',
|
|
11
75
|
phase: 'blocking',
|
|
12
76
|
applies(ctx) {
|
|
77
|
+
if (ctx.config.circularDeps?.enabled === false)
|
|
78
|
+
return false;
|
|
13
79
|
return ctx.languages.includes('typescript') || ctx.languages.includes('javascript');
|
|
14
80
|
},
|
|
15
81
|
async run(ctx) {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
82
|
+
const allFiles = await (0, targets_1.listTrackedFiles)(ctx, SOURCE_GLOBS, (0, config_1.getSourcePaths)());
|
|
83
|
+
if (allFiles.length === 0)
|
|
18
84
|
return (0, types_1.pass)();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
utils_1.logger.
|
|
29
|
-
return (0, types_1.
|
|
85
|
+
// Files the client exempted (generated code, vendored bundles) are dropped
|
|
86
|
+
// before analysis — and again from the cycles below, since the graph can reach
|
|
87
|
+
// an ignored file by following imports from one that is analyzed.
|
|
88
|
+
const ignore = ctx.config.circularDeps?.ignore ?? [];
|
|
89
|
+
const files = ignore.length > 0 ? allFiles.filter(f => !(0, utils_1.matchesAnyGlob)(f, ignore)) : allFiles;
|
|
90
|
+
if (files.length === 0)
|
|
91
|
+
return (0, types_1.skip)('all files ignored');
|
|
92
|
+
const dpdmBin = resolveDpdm(ctx.cwd);
|
|
93
|
+
if (!dpdmBin) {
|
|
94
|
+
utils_1.logger.warn('[Circular Deps]: ⚠️ dpdm could not be resolved — skipping');
|
|
95
|
+
return (0, types_1.skip)('dpdm unavailable');
|
|
96
|
+
}
|
|
97
|
+
const timeout = ctx.config.circularDeps?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
98
|
+
const reportPath = node_path_1.default.join(ctx.cwd, '.dpdm.temp.json');
|
|
99
|
+
try {
|
|
100
|
+
// Read the cycles from dpdm's JSON report rather than its console output.
|
|
101
|
+
// The console shapes this check used to grep for ("Circular Dependencies:",
|
|
102
|
+
// "[1] a -> b") are not what dpdm 4 prints ("• Circular Dependencies",
|
|
103
|
+
// "1) a -> b"), so no cycle ever matched and the check passed every repo,
|
|
104
|
+
// including one built solely out of a cycle. `--exit-code circular:1` gives
|
|
105
|
+
// the same answer independently: dpdm otherwise exits 0 even when it finds
|
|
106
|
+
// cycles, so the exit code alone cannot tell a clean repo from a broken run.
|
|
107
|
+
const result = await (0, execa_1.execa)(process.execPath, [
|
|
108
|
+
dpdmBin,
|
|
109
|
+
'--no-warning',
|
|
110
|
+
'--no-tree',
|
|
111
|
+
'--no-circular',
|
|
112
|
+
'--exit-code',
|
|
113
|
+
'circular:1',
|
|
114
|
+
'-o',
|
|
115
|
+
reportPath,
|
|
116
|
+
...toFileArgs(files, (0, config_1.getSourcePaths)()),
|
|
117
|
+
], { cwd: ctx.cwd, reject: false, timeout });
|
|
118
|
+
if (result.timedOut) {
|
|
119
|
+
utils_1.logger.warn(`[Circular Deps]: ⚠️ dpdm timed out after ${timeout}ms — skipping`);
|
|
120
|
+
return (0, types_1.skip)('dpdm timed out');
|
|
121
|
+
}
|
|
122
|
+
const report = readReport(reportPath);
|
|
123
|
+
// No report means dpdm never got far enough to write one (a tsconfig it
|
|
124
|
+
// cannot read, a file it cannot parse, a killed process). Skip: reporting
|
|
125
|
+
// that as "no cycles" hides a check that did not run, and failing on it
|
|
126
|
+
// blocks a PR that may have no cycle at all.
|
|
127
|
+
if (!report) {
|
|
128
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
129
|
+
utils_1.logger.warn(`[Circular Deps]: ⚠️ dpdm wrote no report (exit ${result.exitCode ?? 'killed'})${output ? `:\n${output}` : ''}`);
|
|
130
|
+
return (0, types_1.skip)(`dpdm failed to run (exit ${result.exitCode ?? 'killed'})`);
|
|
131
|
+
}
|
|
132
|
+
const cycles = (report.circulars ?? []).filter(cycle => ignore.length === 0 || !cycle.some(file => (0, utils_1.matchesAnyGlob)(file, ignore)));
|
|
133
|
+
if (cycles.length === 0)
|
|
134
|
+
return (0, types_1.pass)();
|
|
135
|
+
// Advisory by default. This check reported nothing for its whole existence
|
|
136
|
+
// (see the note above), so switching it on turns every long-standing cycle
|
|
137
|
+
// in every client repo into a finding at once — barrel files alone account
|
|
138
|
+
// for most of them. Warning first lets a repo see its cycles before they
|
|
139
|
+
// gate anything; promote with `circularDeps.severity: "error"`.
|
|
140
|
+
const sev = (0, findings_1.severityEmitter)(ctx.config.circularDeps?.severity, 'warn');
|
|
141
|
+
sev.log(`[Circular Deps]: ${sev.icon} ${cycles.length} circular dependency(ies):`);
|
|
142
|
+
cycles.slice(0, 10).forEach((cycle, i) => sev.log(` ${i + 1}) ${cycle.join(' -> ')}`));
|
|
143
|
+
if (cycles.length > 10)
|
|
144
|
+
sev.log(` ... and ${cycles.length - 10} more`);
|
|
145
|
+
return sev.emit(`${cycles.length} circular dependency(ies) found`);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
(0, utils_1.cleanupTempFile)(reportPath);
|
|
30
149
|
}
|
|
31
|
-
return (0, types_1.pass)();
|
|
32
150
|
},
|
|
33
151
|
};
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { Check } from '../../types';
|
|
2
|
+
/** One entry of knip's JSON reporter — a file plus the issues found for it. */
|
|
3
|
+
interface KnipIssue {
|
|
4
|
+
file: string;
|
|
5
|
+
dependencies?: {
|
|
6
|
+
name: string;
|
|
7
|
+
}[];
|
|
8
|
+
unlisted?: {
|
|
9
|
+
name: string;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
interface KnipReport {
|
|
13
|
+
issues?: KnipIssue[];
|
|
14
|
+
}
|
|
2
15
|
/**
|
|
3
16
|
* Compile one `dependency.ignore` entry into a pattern knip matches the way this
|
|
4
17
|
* config documents it — a glob over the whole dependency name.
|
|
@@ -11,4 +24,20 @@ import { Check } from '../../types';
|
|
|
11
24
|
* only `eslint-…`, and keeps a plain name matching exactly that name.
|
|
12
25
|
*/
|
|
13
26
|
export declare function toKnipIgnorePattern(entry: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Pull knip's report out of a stdout stream that may carry unrelated output.
|
|
29
|
+
*
|
|
30
|
+
* knip imports the project's own config files while analyzing (a `wdio.conf.ts`
|
|
31
|
+
* for its WebdriverIO plugin, say), so any top-level side effect in them —
|
|
32
|
+
* a dotenv banner, a `console.log` in a config module — is printed before the
|
|
33
|
+
* report. Slicing from the first `{` to the last `}` therefore captured that
|
|
34
|
+
* noise instead: a dotenvx tip ending in `{ path: '…/.env' }` made the whole
|
|
35
|
+
* check fail on a repo whose report was perfectly valid.
|
|
36
|
+
*
|
|
37
|
+
* The json reporter writes the report as a single line (`JSON.stringify` with no
|
|
38
|
+
* indentation), so the last line that parses AND carries `issues` is the report.
|
|
39
|
+
* Anything else on stdout is noise, whatever braces it happens to contain.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseKnipReport(stdout: string): KnipReport | null;
|
|
14
42
|
export declare const dependencyCheck: Check;
|
|
43
|
+
export {};
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.dependencyCheck = void 0;
|
|
7
7
|
exports.toKnipIgnorePattern = toKnipIgnorePattern;
|
|
8
|
+
exports.parseKnipReport = parseKnipReport;
|
|
8
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
11
|
const config_1 = require("../../../config");
|
|
@@ -57,6 +58,37 @@ function hasPackageJsonConfig(cwd) {
|
|
|
57
58
|
return false;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Pull knip's report out of a stdout stream that may carry unrelated output.
|
|
63
|
+
*
|
|
64
|
+
* knip imports the project's own config files while analyzing (a `wdio.conf.ts`
|
|
65
|
+
* for its WebdriverIO plugin, say), so any top-level side effect in them —
|
|
66
|
+
* a dotenv banner, a `console.log` in a config module — is printed before the
|
|
67
|
+
* report. Slicing from the first `{` to the last `}` therefore captured that
|
|
68
|
+
* noise instead: a dotenvx tip ending in `{ path: '…/.env' }` made the whole
|
|
69
|
+
* check fail on a repo whose report was perfectly valid.
|
|
70
|
+
*
|
|
71
|
+
* The json reporter writes the report as a single line (`JSON.stringify` with no
|
|
72
|
+
* indentation), so the last line that parses AND carries `issues` is the report.
|
|
73
|
+
* Anything else on stdout is noise, whatever braces it happens to contain.
|
|
74
|
+
*/
|
|
75
|
+
function parseKnipReport(stdout) {
|
|
76
|
+
const lines = stdout.split('\n');
|
|
77
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
78
|
+
const line = lines[i].trim();
|
|
79
|
+
if (!line.startsWith('{'))
|
|
80
|
+
continue;
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(line);
|
|
83
|
+
if (Array.isArray(parsed.issues))
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// A noise line that merely looks like JSON — keep scanning upwards.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
60
92
|
/** Collect the names reported for one issue type across every file in the report. */
|
|
61
93
|
function collectNames(issues, key) {
|
|
62
94
|
return [...new Set(issues.flatMap(issue => (issue[key] ?? []).map(entry => entry.name)))];
|
|
@@ -122,18 +154,12 @@ exports.dependencyCheck = {
|
|
|
122
154
|
cwd: ctx.cwd,
|
|
123
155
|
reject: false,
|
|
124
156
|
});
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
result = JSON.parse(stdout.slice(start, end + 1));
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return (0, types_1.fail)('knip produced invalid JSON');
|
|
136
|
-
}
|
|
157
|
+
const result = parseKnipReport(stdout);
|
|
158
|
+
// No report on stdout → the tool couldn't produce a result; skip rather than
|
|
159
|
+
// fail the client's PR. Never fail here: a check that blocks a PR because a
|
|
160
|
+
// tool misbehaved reports our problem as the client's.
|
|
161
|
+
if (!result)
|
|
162
|
+
return (0, types_1.skip)('knip produced no report');
|
|
137
163
|
const issues = result.issues ?? [];
|
|
138
164
|
const unused = collectNames(issues, 'dependencies');
|
|
139
165
|
const missing = collectNames(issues, 'unlisted');
|
|
@@ -41,7 +41,28 @@ exports.npmAuditCheck = {
|
|
|
41
41
|
const ignore = new Set((ctx.config.security?.['npm-audit']?.ignore ?? []).map(name => name.toLowerCase()));
|
|
42
42
|
const ownName = getOwnPackageName();
|
|
43
43
|
const { stdout } = await (0, execa_1.execa)('npm', ['audit', '--json'], { cwd: ctx.cwd, reject: false });
|
|
44
|
-
|
|
44
|
+
// `npm audit` reports registry/lockfile problems as JSON on stdout, but not
|
|
45
|
+
// always: an absent npm (execa resolves with `code: 'ENOENT'`), a killed
|
|
46
|
+
// process or a proxy that answers with HTML all leave stdout unparseable.
|
|
47
|
+
// Parsing it bare made those cases throw, and a throw in a blocking check is
|
|
48
|
+
// a failed PR — the client saw "NPM Audit threw: Unexpected end of JSON
|
|
49
|
+
// input" and a red gate for an environment problem they did not cause.
|
|
50
|
+
let report;
|
|
51
|
+
try {
|
|
52
|
+
report = JSON.parse(stdout);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
utils_1.logger.warn('[NPM Audit]: ⚠️ npm audit produced no parseable report — skipping');
|
|
56
|
+
return (0, types_1.skip)('npm audit produced no parseable report');
|
|
57
|
+
}
|
|
58
|
+
// A repo with no lockfile, or a registry npm cannot reach, yields
|
|
59
|
+
// `{"error":{"code":"ENOLOCK",…}}` — no `vulnerabilities` key at all. Passing
|
|
60
|
+
// on that reads as "no vulnerabilities" when nothing was actually audited.
|
|
61
|
+
if (report.error) {
|
|
62
|
+
const detail = report.error.summary ?? report.error.code ?? 'unknown error';
|
|
63
|
+
utils_1.logger.warn(`[NPM Audit]: ⚠️ npm could not audit this project — ${detail}`);
|
|
64
|
+
return (0, types_1.skip)(`npm audit unavailable: ${report.error.code ?? 'error'}`);
|
|
65
|
+
}
|
|
45
66
|
const relevant = Object.values(report.vulnerabilities ?? {}).filter(v => v.isDirect && v.name !== ownName && isSeverityAtLeast(v.severity, auditLevel));
|
|
46
67
|
const [ignored, directVulns] = relevant.reduce(([skip, block], v) => {
|
|
47
68
|
(ignore.has(v.name.toLowerCase()) ? skip : block).push(v);
|
|
@@ -74,6 +74,15 @@ exports.lintCheck = {
|
|
|
74
74
|
stdio: 'inherit',
|
|
75
75
|
reject: false,
|
|
76
76
|
});
|
|
77
|
-
|
|
77
|
+
if (result.exitCode === 0)
|
|
78
|
+
return (0, types_1.pass)();
|
|
79
|
+
// ESLint exits 1 for lint errors and 2 for a fatal error — a config it cannot
|
|
80
|
+
// load, an unresolvable plugin, a parser it cannot construct. Only the
|
|
81
|
+
// former is the client's code. Treating exit 2 as "lint errors found" turned
|
|
82
|
+
// a config incompatibility (often between our bundled config and the client's
|
|
83
|
+
// toolchain) into a red gate on their PR, and named the wrong cause.
|
|
84
|
+
if (result.exitCode === 1)
|
|
85
|
+
return (0, types_1.fail)('lint errors found');
|
|
86
|
+
return (0, types_1.warn)(`eslint failed to run (exit ${result.exitCode ?? 'killed'})${hasLocal ? '' : ' — using the pr-checkmate config'}`);
|
|
78
87
|
},
|
|
79
88
|
};
|
|
@@ -52,6 +52,13 @@ exports.typecheckCheck = {
|
|
|
52
52
|
const errorCount = (output.match(/error TS\d+/g) ?? []).length;
|
|
53
53
|
if (output)
|
|
54
54
|
utils_1.logger.warn(output);
|
|
55
|
+
// A non-zero exit with no diagnostic in it is not a type error — tsc was
|
|
56
|
+
// killed (OOM on a large project), crashed, or never started. Reporting that
|
|
57
|
+
// as `0 type error(s)` blocked PRs with a message naming nothing to fix.
|
|
58
|
+
if (errorCount === 0) {
|
|
59
|
+
utils_1.logger.warn(`[TypeScript]: ⚠️ tsc exited ${result.exitCode ?? 'abnormally'} without reporting a type error — treating as a tooling failure, not a type error`);
|
|
60
|
+
return (0, types_1.warn)(`tsc failed to run (exit ${result.exitCode ?? 'killed'})`);
|
|
61
|
+
}
|
|
55
62
|
return (0, types_1.fail)(`${errorCount} type error(s)`);
|
|
56
63
|
},
|
|
57
64
|
};
|
|
@@ -91,7 +91,16 @@ exports.markdownLintCheck = {
|
|
|
91
91
|
reject: false,
|
|
92
92
|
stdio: 'inherit',
|
|
93
93
|
});
|
|
94
|
-
|
|
94
|
+
if (result.exitCode === 0)
|
|
95
|
+
return (0, types_1.pass)();
|
|
96
|
+
// markdownlint-cli2 exits 1 for lint findings and 2 when it could not run
|
|
97
|
+
// (an unreadable config, a bad glob). Reporting the latter as "markdown lint
|
|
98
|
+
// issues found" names the wrong cause — and under `severity: error` it
|
|
99
|
+
// becomes a gate on a tooling problem.
|
|
100
|
+
if (result.exitCode === 2) {
|
|
101
|
+
return (0, types_1.warn)('markdownlint failed to run (exit 2) — check the markdownlint config');
|
|
102
|
+
}
|
|
103
|
+
return (0, types_1.warn)('markdown lint issues found');
|
|
95
104
|
}
|
|
96
105
|
finally {
|
|
97
106
|
if (tempConfigFile && node_fs_1.default.existsSync(tempConfigFile))
|
|
@@ -17,7 +17,18 @@ exports.securityCheck = {
|
|
|
17
17
|
return ctx.config.security?.gitleaks?.enabled !== false;
|
|
18
18
|
},
|
|
19
19
|
async run(ctx) {
|
|
20
|
-
|
|
20
|
+
// A binary that cannot be resolved throws, and a throw in a blocking check is
|
|
21
|
+
// a failed PR — the client would see "Security Scan threw: Cannot resolve
|
|
22
|
+
// binary" as a red gate on a repo with no secrets in it. Every other bundled
|
|
23
|
+
// tool (knip, license-checker) already skips here; this one did not.
|
|
24
|
+
let gitleaksBin;
|
|
25
|
+
try {
|
|
26
|
+
gitleaksBin = (0, utils_1.resolvePackageBin)('gitleaks-secret-scanner-fixed', 'bin/cli.js');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
utils_1.logger.warn('[Security Scan]: ⚠️ bundled gitleaks scanner could not be resolved — skipping');
|
|
30
|
+
return (0, types_1.skip)('gitleaks unavailable');
|
|
31
|
+
}
|
|
21
32
|
const hasClientConfig = (0, config_1.hasLocalConfig)([
|
|
22
33
|
'.gitleaks.toml',
|
|
23
34
|
'gitleaks.toml',
|
package/dist/init.js
CHANGED
|
@@ -109,6 +109,19 @@ const DEFAULT_CONFIG = {
|
|
|
109
109
|
// which knip can't see as imports and would report as unused).
|
|
110
110
|
ignore: [],
|
|
111
111
|
},
|
|
112
|
+
// Circular-import check (dpdm, bundled — nothing needed on the runner). Uses
|
|
113
|
+
// the project's own dpdm when it has one. Advisory by default (severity
|
|
114
|
+
// "warn"): most repos have long-standing cycles through barrel files, so see
|
|
115
|
+
// yours first, then set severity "error" to gate merges on them. enabled false
|
|
116
|
+
// disables the check; `ignore` takes glob patterns kept out of the analysis
|
|
117
|
+
// (generated or vendored code, or barrels whose cycles you tolerate). When the
|
|
118
|
+
// analyzer cannot run, the check skips rather than passing or failing.
|
|
119
|
+
circularDeps: {
|
|
120
|
+
enabled: true,
|
|
121
|
+
severity: 'warn',
|
|
122
|
+
ignore: [],
|
|
123
|
+
timeoutMs: 120000,
|
|
124
|
+
},
|
|
112
125
|
// Unused-exports check (ts-unused-exports)
|
|
113
126
|
deadCode: {
|
|
114
127
|
enabled: true,
|
|
@@ -204,6 +204,12 @@ export interface PRCheckMateConfig {
|
|
|
204
204
|
severity?: 'error' | 'warn';
|
|
205
205
|
ignore?: string[];
|
|
206
206
|
};
|
|
207
|
+
circularDeps?: {
|
|
208
|
+
enabled?: boolean;
|
|
209
|
+
severity?: 'error' | 'warn';
|
|
210
|
+
ignore?: string[];
|
|
211
|
+
timeoutMs?: number;
|
|
212
|
+
};
|
|
207
213
|
vulnScan?: {
|
|
208
214
|
enabled?: boolean;
|
|
209
215
|
timeoutMs?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-checkmate",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.19",
|
|
4
4
|
"description": "Automated PR quality checks: linting, formatting, dependency analysis, and spellcheck",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"@typescript-eslint/parser": "^8.62.1",
|
|
84
84
|
"@wasm-fmt/clang-format": "^22.1.8",
|
|
85
85
|
"cspell": "^10.0.1",
|
|
86
|
+
"dpdm": "^4.2.0",
|
|
86
87
|
"eslint": "^10.6.0",
|
|
87
88
|
"eslint-config-prettier": "^10.1.8",
|
|
88
89
|
"eslint-plugin-import-x": "^4.17.1",
|
package/pr-checkmate.schema.json
CHANGED
|
@@ -157,6 +157,15 @@
|
|
|
157
157
|
"ignore": { "type": "array", "items": { "type": "string" } }
|
|
158
158
|
}
|
|
159
159
|
},
|
|
160
|
+
"circularDeps": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"enabled": { "type": "boolean" },
|
|
164
|
+
"severity": { "enum": ["error", "warn"] },
|
|
165
|
+
"ignore": { "type": "array", "items": { "type": "string" } },
|
|
166
|
+
"timeoutMs": { "type": "number" }
|
|
167
|
+
}
|
|
168
|
+
},
|
|
160
169
|
"typecheck": { "$ref": "#/$defs/toggle" },
|
|
161
170
|
"prBody": {
|
|
162
171
|
"type": "object",
|
package/src/config/cspell.json
CHANGED
|
@@ -157,6 +157,15 @@
|
|
|
157
157
|
"ignore": { "type": "array", "items": { "type": "string" } }
|
|
158
158
|
}
|
|
159
159
|
},
|
|
160
|
+
"circularDeps": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"enabled": { "type": "boolean" },
|
|
164
|
+
"severity": { "enum": ["error", "warn"] },
|
|
165
|
+
"ignore": { "type": "array", "items": { "type": "string" } },
|
|
166
|
+
"timeoutMs": { "type": "number" }
|
|
167
|
+
}
|
|
168
|
+
},
|
|
160
169
|
"typecheck": { "$ref": "#/$defs/toggle" },
|
|
161
170
|
"prBody": {
|
|
162
171
|
"type": "object",
|