pr-checkmate 3.1.0 → 3.2.1
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/dist/cjs/core/checks/dependencies/grype-scan.d.ts +8 -0
- package/dist/cjs/core/checks/dependencies/grype-scan.js +18 -4
- package/dist/cjs/core/checks/languages/go-format.js +1 -34
- package/dist/cjs/core/checks/quality/markdown-lint.js +94 -57
- package/dist/cjs/core/install/profiles.js +1 -1
- package/dist/cjs/interfaces/index.d.ts +14 -0
- package/dist/cjs/utils/execa.js +1 -34
- package/dist/esm/core/checks/dependencies/grype-scan.d.ts +8 -0
- package/dist/esm/core/checks/dependencies/grype-scan.js +18 -5
- package/dist/esm/core/checks/quality/markdown-lint.js +94 -57
- package/dist/esm/core/install/profiles.js +1 -1
- package/dist/esm/interfaces/index.d.ts +14 -0
- package/install-manifests/duplicate/package-lock.json +0 -12
- package/install-manifests/js/package-lock.json +17 -41
- package/install-manifests/js/package.json +1 -1
- package/install-manifests/markdown/package-lock.json +1 -492
- package/install-manifests/markdown/package.json +1 -1
- package/package.json +3 -3
- package/pr-checkmate.schema.json +20 -0
- package/src/config/pr-checkmate.schema.json +20 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GrypeScanEnvOverrides, PRCheckMateConfig } from '../../../interfaces/index.js';
|
|
1
2
|
import { Check } from '../../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Dependency vulnerability scan via Anchore's `grype`. Non-blocking and ON by
|
|
@@ -19,4 +20,11 @@ import { Check } from '../../types.js';
|
|
|
19
20
|
* installed `node_modules` is minutes of work whose every finding this check then
|
|
20
21
|
* throws away as third-party.
|
|
21
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* The effective grypeScan settings for this run: the top-level values, with the
|
|
25
|
+
* `ci` or `local` block layered on top depending on where we are running. Only
|
|
26
|
+
* keys actually set in the block win, so `local: { enabled: false }` leaves the
|
|
27
|
+
* severity and ignore list alone.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveGrypeScan(config: PRCheckMateConfig['grypeScan'], env?: NodeJS.ProcessEnv): GrypeScanEnvOverrides;
|
|
22
30
|
export declare const grypeScanCheck: Check;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.grypeScanCheck = void 0;
|
|
4
|
+
exports.resolveGrypeScan = resolveGrypeScan;
|
|
4
5
|
const execa_js_1 = require("../../../utils/execa.js");
|
|
5
6
|
const index_js_1 = require("../../../utils/index.js");
|
|
6
7
|
const types_js_1 = require("../../types.js");
|
|
@@ -43,17 +44,30 @@ function severityRank(severity) {
|
|
|
43
44
|
* installed `node_modules` is minutes of work whose every finding this check then
|
|
44
45
|
* throws away as third-party.
|
|
45
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* The effective grypeScan settings for this run: the top-level values, with the
|
|
49
|
+
* `ci` or `local` block layered on top depending on where we are running. Only
|
|
50
|
+
* keys actually set in the block win, so `local: { enabled: false }` leaves the
|
|
51
|
+
* severity and ignore list alone.
|
|
52
|
+
*/
|
|
53
|
+
function resolveGrypeScan(config, env = process.env) {
|
|
54
|
+
const { ci, local, ...base } = config ?? {};
|
|
55
|
+
const overrides = ((0, index_js_1.isCiEnvironment)(env) ? ci : local) ?? {};
|
|
56
|
+
const defined = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
|
|
57
|
+
return { ...base, ...defined };
|
|
58
|
+
}
|
|
46
59
|
exports.grypeScanCheck = {
|
|
47
60
|
name: 'Grype Scan',
|
|
48
61
|
phase: 'informational',
|
|
49
62
|
weight: 'heavy',
|
|
50
63
|
applies(ctx) {
|
|
51
|
-
return ctx.config.grypeScan
|
|
64
|
+
return resolveGrypeScan(ctx.config.grypeScan).enabled !== false;
|
|
52
65
|
},
|
|
53
66
|
async run(ctx) {
|
|
54
|
-
const
|
|
67
|
+
const settings = resolveGrypeScan(ctx.config.grypeScan);
|
|
68
|
+
const minSeverity = (settings.minSeverity ?? 'medium').toLowerCase();
|
|
55
69
|
const minRank = severityRank(minSeverity);
|
|
56
|
-
const ignore = (
|
|
70
|
+
const ignore = (settings.ignore ?? []).map(s => s.toLowerCase());
|
|
57
71
|
// Never walk `ignoreDirs` — the same contract every other check honours, and
|
|
58
72
|
// here it is the difference between a usable check and an unusable one. A bare
|
|
59
73
|
// `grype dir:.` catalogs every file under `node_modules`, which on this repo took
|
|
@@ -61,7 +75,7 @@ exports.grypeScanCheck = {
|
|
|
61
75
|
// findings: grype resolves package versions from the lockfile, and installed
|
|
62
76
|
// third-party code is not something the client could fix anyway (see below).
|
|
63
77
|
const excludes = (ctx.config.ignoreDirs ?? []).flatMap(dir => ['--exclude', `**/${dir}/**`]);
|
|
64
|
-
const timeout =
|
|
78
|
+
const timeout = settings.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
65
79
|
const result = await (0, execa_js_1.execa)('grype', ['dir:.', '-o', 'json', '-q', ...excludes], {
|
|
66
80
|
cwd: ctx.cwd,
|
|
67
81
|
reject: false,
|
|
@@ -1,37 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
@@ -69,7 +36,7 @@ const GO_GLOBS = ['*.go'];
|
|
|
69
36
|
async function loadBundledGofmt() {
|
|
70
37
|
try {
|
|
71
38
|
const entry = (0, index_js_1.resolvePackageBin)('@wasm-fmt/gofmt', 'gofmt_node.js');
|
|
72
|
-
const mod = (await
|
|
39
|
+
const mod = (await import((0, node_url_1.pathToFileURL)(entry).href));
|
|
73
40
|
return mod.format;
|
|
74
41
|
}
|
|
75
42
|
catch (err) {
|
|
@@ -5,10 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.markdownLintCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
9
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_url_1 = require("node:url");
|
|
10
10
|
const index_js_1 = require("../../../config/index.js");
|
|
11
|
-
const execa_js_1 = require("../../../utils/execa.js");
|
|
12
11
|
const index_js_2 = require("../../../utils/index.js");
|
|
13
12
|
const targets_js_1 = require("../../targets.js");
|
|
14
13
|
const types_js_1 = require("../../types.js");
|
|
@@ -24,12 +23,66 @@ function loadUserMarkdownConfig(cwd) {
|
|
|
24
23
|
return {};
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Load the markdownlint library, or `null` when it cannot be found.
|
|
28
|
+
*
|
|
29
|
+
* The library rather than `markdownlint-cli2`: the CLI drags in globby →
|
|
30
|
+
* micromatch → braces, which carries an unpatched advisory every client's
|
|
31
|
+
* `npm audit` then reports against pr-checkmate. We already have the file list
|
|
32
|
+
* from git, so the CLI's globbing bought nothing.
|
|
33
|
+
*
|
|
34
|
+
* Resolved through `resolvePackageBin` and loaded by a dynamic `import()` for the
|
|
35
|
+
* same two reasons as the bundled gofmt: the package may live in a fetched
|
|
36
|
+
* profile directory, and it is ESM while we emit CommonJS.
|
|
37
|
+
*/
|
|
38
|
+
async function loadMarkdownlint() {
|
|
39
|
+
try {
|
|
40
|
+
const promiseEntry = (0, index_js_2.resolvePackageBin)('markdownlint', 'lib/exports-promise.mjs');
|
|
41
|
+
const mainEntry = node_path_1.default.join(node_path_1.default.dirname(promiseEntry), 'exports.mjs');
|
|
42
|
+
const promise = (await import((0, node_url_1.pathToFileURL)(promiseEntry).href));
|
|
43
|
+
const main = (await import((0, node_url_1.pathToFileURL)(mainEntry).href));
|
|
44
|
+
return { lint: promise.lint, readConfig: promise.readConfig, applyFixes: main.applyFixes };
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
index_js_2.logger.warn(`[Markdown]: ⚠️ markdownlint could not be loaded\n${err}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The rules to lint with: the repository's own config when it has one, ours
|
|
53
|
+
* (plus the client's `markdownlint.rules`) when it does not. Someone with a
|
|
54
|
+
* `.markdownlint.json` has an opinion; merging ours into it would silently
|
|
55
|
+
* override the rules they wrote.
|
|
56
|
+
*/
|
|
57
|
+
async function readLintConfig(lib, cwd, userRules) {
|
|
58
|
+
const local = ['.markdownlint.json', '.markdownlint.jsonc'].find(name => (0, index_js_1.hasLocalConfig)([name], cwd));
|
|
59
|
+
if (local)
|
|
60
|
+
return lib.readConfig(node_path_1.default.join(cwd, local));
|
|
61
|
+
if ((0, index_js_1.hasLocalConfig)(['.markdownlint.yaml', '.markdownlint.yml'], cwd)) {
|
|
62
|
+
index_js_2.logger.warn('[Markdown]: ⚠️ YAML markdownlint configs are not supported, using bundled rules');
|
|
63
|
+
}
|
|
64
|
+
const bundled = await lib.readConfig((0, index_js_1.getPackageConfigPath)('.markdownlint.json'));
|
|
65
|
+
return { ...bundled, ...userRules };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* An ignore pattern as a regular expression over a repo-relative path.
|
|
69
|
+
*
|
|
70
|
+
* A bare directory name excludes its contents; a file name matches exactly; `*`
|
|
71
|
+
* stays within a segment and `**` crosses them. `path.extname` is what tells a
|
|
72
|
+
* directory from a file, and it reads a leading dot as part of the name, so
|
|
73
|
+
* `.claude` is still the directory it is.
|
|
74
|
+
*/
|
|
75
|
+
function toIgnoreRegExp(pattern) {
|
|
76
|
+
const clean = pattern.replace(/\/+$/, '');
|
|
77
|
+
const escaped = clean
|
|
78
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
79
|
+
.replace(/\*\*\//g, '\u0001')
|
|
80
|
+
.replace(/\*\*/g, '\u0000')
|
|
81
|
+
.replace(/\*/g, '[^/]*')
|
|
82
|
+
.replace(/\u0001/g, '(?:.*/)?')
|
|
83
|
+
.replace(/\u0000/g, '.*');
|
|
84
|
+
const isDir = !clean.includes('*') && !node_path_1.default.extname(clean);
|
|
85
|
+
return new RegExp(`^${escaped}${isDir ? '(/.*)?' : ''}$`);
|
|
33
86
|
}
|
|
34
87
|
exports.markdownLintCheck = {
|
|
35
88
|
name: 'Markdown',
|
|
@@ -45,23 +98,10 @@ exports.markdownLintCheck = {
|
|
|
45
98
|
return (0, types_js_1.skip)('no markdown files');
|
|
46
99
|
const userConfig = loadUserMarkdownConfig(ctx.cwd);
|
|
47
100
|
const userRules = userConfig.rules ?? {};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
const bundledConfig = (0, index_js_1.getPackageConfigPath)('.markdownlint.json');
|
|
55
|
-
if (Object.keys(userRules).length > 0) {
|
|
56
|
-
tempConfigFile = buildMergedConfig(bundledConfig, userRules);
|
|
57
|
-
configArg = ['--config', tempConfigFile];
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
configArg = ['--config', bundledConfig];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const markdownlintBin = (0, index_js_2.resolvePackageBin)('markdownlint-cli2', 'markdownlint-cli2-bin.mjs');
|
|
64
|
-
const defaultIgnorePatterns = [
|
|
101
|
+
const lib = await loadMarkdownlint();
|
|
102
|
+
if (!lib)
|
|
103
|
+
return (0, types_js_1.warn)('markdownlint failed to run (exit 2) — check the markdownlint config');
|
|
104
|
+
const ignores = [
|
|
65
105
|
'node_modules/**',
|
|
66
106
|
'dist/**',
|
|
67
107
|
'coverage/**',
|
|
@@ -70,41 +110,38 @@ exports.markdownLintCheck = {
|
|
|
70
110
|
// Release-tool output, not prose anyone edits — see GENERATED_DOC_FILES.
|
|
71
111
|
...index_js_1.GENERATED_DOC_FILES,
|
|
72
112
|
...(userConfig.ignores ?? []),
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// is a glob that can never match. `path.extname` is what tells them apart,
|
|
78
|
-
// and it reads a leading dot as part of the name, so `.claude` is still
|
|
79
|
-
// treated as the directory it is.
|
|
80
|
-
const ignoreGlobs = defaultIgnorePatterns.flatMap(pattern => {
|
|
81
|
-
const clean = pattern.replace(/\/+$/, '');
|
|
82
|
-
const globs = [`#${clean}`];
|
|
83
|
-
if (!clean.includes('*') && !node_path_1.default.extname(clean))
|
|
84
|
-
globs.push(`#${clean}/**`);
|
|
85
|
-
return globs;
|
|
86
|
-
});
|
|
87
|
-
const args = [...(ctx.write ? ['--fix'] : []), ...targets, ...ignoreGlobs, ...configArg];
|
|
113
|
+
].map(toIgnoreRegExp);
|
|
114
|
+
const files = targets.filter(file => !ignores.some(re => re.test(file)));
|
|
115
|
+
if (files.length === 0)
|
|
116
|
+
return (0, types_js_1.skip)('no markdown files');
|
|
88
117
|
try {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
const config = await readLintConfig(lib, ctx.cwd, userRules);
|
|
119
|
+
const results = await lib.lint({ files: files.map(f => node_path_1.default.resolve(ctx.cwd, f)), config });
|
|
120
|
+
let findings = 0;
|
|
121
|
+
for (const [file, errors] of Object.entries(results)) {
|
|
122
|
+
if (errors.length === 0)
|
|
123
|
+
continue;
|
|
124
|
+
const rel = node_path_1.default.relative(ctx.cwd, file);
|
|
125
|
+
let remaining = errors;
|
|
126
|
+
if (ctx.write && errors.some(e => e.fixInfo)) {
|
|
127
|
+
const fixed = lib.applyFixes(node_fs_1.default.readFileSync(file, 'utf-8'), errors);
|
|
128
|
+
node_fs_1.default.writeFileSync(file, fixed);
|
|
129
|
+
remaining = errors.filter(e => !e.fixInfo);
|
|
130
|
+
}
|
|
131
|
+
for (const e of remaining) {
|
|
132
|
+
findings++;
|
|
133
|
+
const col = e.errorRange?.[0] ? `:${e.errorRange[0]}` : '';
|
|
134
|
+
index_js_2.logger.warn(`${rel}:${e.lineNumber}${col} ${e.ruleNames.join('/')} ${e.ruleDescription}` +
|
|
135
|
+
(e.errorDetail ? ` [${e.errorDetail}]` : ''));
|
|
136
|
+
}
|
|
102
137
|
}
|
|
103
|
-
return (0, types_js_1.warn)('markdown lint issues found');
|
|
138
|
+
return findings === 0 ? (0, types_js_1.pass)() : (0, types_js_1.warn)('markdown lint issues found');
|
|
104
139
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
140
|
+
catch (err) {
|
|
141
|
+
// An unreadable config or a file that vanished: the tool could not run,
|
|
142
|
+
// which is not the same claim as "the prose has problems".
|
|
143
|
+
index_js_2.logger.warn(`[Markdown]: ⚠️ ${err}`);
|
|
144
|
+
return (0, types_js_1.warn)('markdownlint failed to run (exit 2) — check the markdownlint config');
|
|
108
145
|
}
|
|
109
146
|
},
|
|
110
147
|
};
|
|
@@ -369,6 +369,13 @@ export interface PRCheckMateConfig {
|
|
|
369
369
|
* can never stall a commit.
|
|
370
370
|
*/
|
|
371
371
|
timeoutMs?: number;
|
|
372
|
+
/**
|
|
373
|
+
* Overrides applied only on a CI build (see `isCiEnvironment`). Any key set here wins
|
|
374
|
+
* over the top-level value, so CI can keep the scan while local runs turn it off.
|
|
375
|
+
*/
|
|
376
|
+
ci?: GrypeScanEnvOverrides;
|
|
377
|
+
/** Same as `ci`, applied only outside CI (a developer's machine, `be-commit`, hooks). */
|
|
378
|
+
local?: GrypeScanEnvOverrides;
|
|
372
379
|
};
|
|
373
380
|
bannedImports?: Array<{
|
|
374
381
|
name: string;
|
|
@@ -430,3 +437,10 @@ export interface PRCheckMateConfig {
|
|
|
430
437
|
}
|
|
431
438
|
export type { CheckStatus, CheckResult } from '../utils/result-collector.js';
|
|
432
439
|
export type { Language } from '../utils/detect-language.js';
|
|
440
|
+
/** Per-environment overrides for `grypeScan`: the subset of its settings that may differ by environment. */
|
|
441
|
+
export interface GrypeScanEnvOverrides {
|
|
442
|
+
enabled?: boolean;
|
|
443
|
+
minSeverity?: 'negligible' | 'low' | 'medium' | 'high' | 'critical';
|
|
444
|
+
ignore?: string[];
|
|
445
|
+
timeoutMs?: number;
|
|
446
|
+
}
|
package/dist/cjs/utils/execa.js
CHANGED
|
@@ -1,37 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.execa = execa;
|
|
37
4
|
/**
|
|
@@ -41,6 +8,6 @@ exports.execa = execa;
|
|
|
41
8
|
* this package — all of which await the result and read it synchronously.
|
|
42
9
|
*/
|
|
43
10
|
async function execa(file, args, options) {
|
|
44
|
-
const { execa: run } = await
|
|
11
|
+
const { execa: run } = await import('execa');
|
|
45
12
|
return (await run(file, args, options));
|
|
46
13
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GrypeScanEnvOverrides, PRCheckMateConfig } from '../../../interfaces/index.js';
|
|
1
2
|
import { Check } from '../../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Dependency vulnerability scan via Anchore's `grype`. Non-blocking and ON by
|
|
@@ -19,4 +20,11 @@ import { Check } from '../../types.js';
|
|
|
19
20
|
* installed `node_modules` is minutes of work whose every finding this check then
|
|
20
21
|
* throws away as third-party.
|
|
21
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* The effective grypeScan settings for this run: the top-level values, with the
|
|
25
|
+
* `ci` or `local` block layered on top depending on where we are running. Only
|
|
26
|
+
* keys actually set in the block win, so `local: { enabled: false }` leaves the
|
|
27
|
+
* severity and ignore list alone.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveGrypeScan(config: PRCheckMateConfig['grypeScan'], env?: NodeJS.ProcessEnv): GrypeScanEnvOverrides;
|
|
22
30
|
export declare const grypeScanCheck: Check;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execa } from '../../../utils/execa.js';
|
|
2
|
-
import { collectDirectDeps, logger, normalizeName } from '../../../utils/index.js';
|
|
2
|
+
import { collectDirectDeps, isCiEnvironment, logger, normalizeName, } from '../../../utils/index.js';
|
|
3
3
|
import { pass, skip, warn } from '../../types.js';
|
|
4
4
|
import { notInstalled } from '../tool-install.js';
|
|
5
5
|
const SEVERITY_ORDER = ['negligible', 'low', 'medium', 'high', 'critical'];
|
|
@@ -40,17 +40,30 @@ function severityRank(severity) {
|
|
|
40
40
|
* installed `node_modules` is minutes of work whose every finding this check then
|
|
41
41
|
* throws away as third-party.
|
|
42
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* The effective grypeScan settings for this run: the top-level values, with the
|
|
45
|
+
* `ci` or `local` block layered on top depending on where we are running. Only
|
|
46
|
+
* keys actually set in the block win, so `local: { enabled: false }` leaves the
|
|
47
|
+
* severity and ignore list alone.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveGrypeScan(config, env = process.env) {
|
|
50
|
+
const { ci, local, ...base } = config ?? {};
|
|
51
|
+
const overrides = (isCiEnvironment(env) ? ci : local) ?? {};
|
|
52
|
+
const defined = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
|
|
53
|
+
return { ...base, ...defined };
|
|
54
|
+
}
|
|
43
55
|
export const grypeScanCheck = {
|
|
44
56
|
name: 'Grype Scan',
|
|
45
57
|
phase: 'informational',
|
|
46
58
|
weight: 'heavy',
|
|
47
59
|
applies(ctx) {
|
|
48
|
-
return ctx.config.grypeScan
|
|
60
|
+
return resolveGrypeScan(ctx.config.grypeScan).enabled !== false;
|
|
49
61
|
},
|
|
50
62
|
async run(ctx) {
|
|
51
|
-
const
|
|
63
|
+
const settings = resolveGrypeScan(ctx.config.grypeScan);
|
|
64
|
+
const minSeverity = (settings.minSeverity ?? 'medium').toLowerCase();
|
|
52
65
|
const minRank = severityRank(minSeverity);
|
|
53
|
-
const ignore = (
|
|
66
|
+
const ignore = (settings.ignore ?? []).map(s => s.toLowerCase());
|
|
54
67
|
// Never walk `ignoreDirs` — the same contract every other check honours, and
|
|
55
68
|
// here it is the difference between a usable check and an unusable one. A bare
|
|
56
69
|
// `grype dir:.` catalogs every file under `node_modules`, which on this repo took
|
|
@@ -58,7 +71,7 @@ export const grypeScanCheck = {
|
|
|
58
71
|
// findings: grype resolves package versions from the lockfile, and installed
|
|
59
72
|
// third-party code is not something the client could fix anyway (see below).
|
|
60
73
|
const excludes = (ctx.config.ignoreDirs ?? []).flatMap(dir => ['--exclude', `**/${dir}/**`]);
|
|
61
|
-
const timeout =
|
|
74
|
+
const timeout = settings.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
62
75
|
const result = await execa('grype', ['dir:.', '-o', 'json', '-q', ...excludes], {
|
|
63
76
|
cwd: ctx.cwd,
|
|
64
77
|
reject: false,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { GENERATED_DOC_FILES, getPackageConfigPath, hasLocalConfig, } from '../../../config/index.js';
|
|
5
|
-
import { execa } from '../../../utils/execa.js';
|
|
6
5
|
import { logger, resolvePackageBin } from '../../../utils/index.js';
|
|
7
6
|
import { resolveTargetFiles } from '../../targets.js';
|
|
8
7
|
import { pass, warn, skip } from '../../types.js';
|
|
@@ -18,12 +17,66 @@ function loadUserMarkdownConfig(cwd) {
|
|
|
18
17
|
return {};
|
|
19
18
|
}
|
|
20
19
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Load the markdownlint library, or `null` when it cannot be found.
|
|
22
|
+
*
|
|
23
|
+
* The library rather than `markdownlint-cli2`: the CLI drags in globby →
|
|
24
|
+
* micromatch → braces, which carries an unpatched advisory every client's
|
|
25
|
+
* `npm audit` then reports against pr-checkmate. We already have the file list
|
|
26
|
+
* from git, so the CLI's globbing bought nothing.
|
|
27
|
+
*
|
|
28
|
+
* Resolved through `resolvePackageBin` and loaded by a dynamic `import()` for the
|
|
29
|
+
* same two reasons as the bundled gofmt: the package may live in a fetched
|
|
30
|
+
* profile directory, and it is ESM while we emit CommonJS.
|
|
31
|
+
*/
|
|
32
|
+
async function loadMarkdownlint() {
|
|
33
|
+
try {
|
|
34
|
+
const promiseEntry = resolvePackageBin('markdownlint', 'lib/exports-promise.mjs');
|
|
35
|
+
const mainEntry = path.join(path.dirname(promiseEntry), 'exports.mjs');
|
|
36
|
+
const promise = (await import(pathToFileURL(promiseEntry).href));
|
|
37
|
+
const main = (await import(pathToFileURL(mainEntry).href));
|
|
38
|
+
return { lint: promise.lint, readConfig: promise.readConfig, applyFixes: main.applyFixes };
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
logger.warn(`[Markdown]: ⚠️ markdownlint could not be loaded\n${err}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The rules to lint with: the repository's own config when it has one, ours
|
|
47
|
+
* (plus the client's `markdownlint.rules`) when it does not. Someone with a
|
|
48
|
+
* `.markdownlint.json` has an opinion; merging ours into it would silently
|
|
49
|
+
* override the rules they wrote.
|
|
50
|
+
*/
|
|
51
|
+
async function readLintConfig(lib, cwd, userRules) {
|
|
52
|
+
const local = ['.markdownlint.json', '.markdownlint.jsonc'].find(name => hasLocalConfig([name], cwd));
|
|
53
|
+
if (local)
|
|
54
|
+
return lib.readConfig(path.join(cwd, local));
|
|
55
|
+
if (hasLocalConfig(['.markdownlint.yaml', '.markdownlint.yml'], cwd)) {
|
|
56
|
+
logger.warn('[Markdown]: ⚠️ YAML markdownlint configs are not supported, using bundled rules');
|
|
57
|
+
}
|
|
58
|
+
const bundled = await lib.readConfig(getPackageConfigPath('.markdownlint.json'));
|
|
59
|
+
return { ...bundled, ...userRules };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* An ignore pattern as a regular expression over a repo-relative path.
|
|
63
|
+
*
|
|
64
|
+
* A bare directory name excludes its contents; a file name matches exactly; `*`
|
|
65
|
+
* stays within a segment and `**` crosses them. `path.extname` is what tells a
|
|
66
|
+
* directory from a file, and it reads a leading dot as part of the name, so
|
|
67
|
+
* `.claude` is still the directory it is.
|
|
68
|
+
*/
|
|
69
|
+
function toIgnoreRegExp(pattern) {
|
|
70
|
+
const clean = pattern.replace(/\/+$/, '');
|
|
71
|
+
const escaped = clean
|
|
72
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
73
|
+
.replace(/\*\*\//g, '\u0001')
|
|
74
|
+
.replace(/\*\*/g, '\u0000')
|
|
75
|
+
.replace(/\*/g, '[^/]*')
|
|
76
|
+
.replace(/\u0001/g, '(?:.*/)?')
|
|
77
|
+
.replace(/\u0000/g, '.*');
|
|
78
|
+
const isDir = !clean.includes('*') && !path.extname(clean);
|
|
79
|
+
return new RegExp(`^${escaped}${isDir ? '(/.*)?' : ''}$`);
|
|
27
80
|
}
|
|
28
81
|
export const markdownLintCheck = {
|
|
29
82
|
name: 'Markdown',
|
|
@@ -39,23 +92,10 @@ export const markdownLintCheck = {
|
|
|
39
92
|
return skip('no markdown files');
|
|
40
93
|
const userConfig = loadUserMarkdownConfig(ctx.cwd);
|
|
41
94
|
const userRules = userConfig.rules ?? {};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
const bundledConfig = getPackageConfigPath('.markdownlint.json');
|
|
49
|
-
if (Object.keys(userRules).length > 0) {
|
|
50
|
-
tempConfigFile = buildMergedConfig(bundledConfig, userRules);
|
|
51
|
-
configArg = ['--config', tempConfigFile];
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
configArg = ['--config', bundledConfig];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const markdownlintBin = resolvePackageBin('markdownlint-cli2', 'markdownlint-cli2-bin.mjs');
|
|
58
|
-
const defaultIgnorePatterns = [
|
|
95
|
+
const lib = await loadMarkdownlint();
|
|
96
|
+
if (!lib)
|
|
97
|
+
return warn('markdownlint failed to run (exit 2) — check the markdownlint config');
|
|
98
|
+
const ignores = [
|
|
59
99
|
'node_modules/**',
|
|
60
100
|
'dist/**',
|
|
61
101
|
'coverage/**',
|
|
@@ -64,41 +104,38 @@ export const markdownLintCheck = {
|
|
|
64
104
|
// Release-tool output, not prose anyone edits — see GENERATED_DOC_FILES.
|
|
65
105
|
...GENERATED_DOC_FILES,
|
|
66
106
|
...(userConfig.ignores ?? []),
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// is a glob that can never match. `path.extname` is what tells them apart,
|
|
72
|
-
// and it reads a leading dot as part of the name, so `.claude` is still
|
|
73
|
-
// treated as the directory it is.
|
|
74
|
-
const ignoreGlobs = defaultIgnorePatterns.flatMap(pattern => {
|
|
75
|
-
const clean = pattern.replace(/\/+$/, '');
|
|
76
|
-
const globs = [`#${clean}`];
|
|
77
|
-
if (!clean.includes('*') && !path.extname(clean))
|
|
78
|
-
globs.push(`#${clean}/**`);
|
|
79
|
-
return globs;
|
|
80
|
-
});
|
|
81
|
-
const args = [...(ctx.write ? ['--fix'] : []), ...targets, ...ignoreGlobs, ...configArg];
|
|
107
|
+
].map(toIgnoreRegExp);
|
|
108
|
+
const files = targets.filter(file => !ignores.some(re => re.test(file)));
|
|
109
|
+
if (files.length === 0)
|
|
110
|
+
return skip('no markdown files');
|
|
82
111
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
112
|
+
const config = await readLintConfig(lib, ctx.cwd, userRules);
|
|
113
|
+
const results = await lib.lint({ files: files.map(f => path.resolve(ctx.cwd, f)), config });
|
|
114
|
+
let findings = 0;
|
|
115
|
+
for (const [file, errors] of Object.entries(results)) {
|
|
116
|
+
if (errors.length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
const rel = path.relative(ctx.cwd, file);
|
|
119
|
+
let remaining = errors;
|
|
120
|
+
if (ctx.write && errors.some(e => e.fixInfo)) {
|
|
121
|
+
const fixed = lib.applyFixes(fs.readFileSync(file, 'utf-8'), errors);
|
|
122
|
+
fs.writeFileSync(file, fixed);
|
|
123
|
+
remaining = errors.filter(e => !e.fixInfo);
|
|
124
|
+
}
|
|
125
|
+
for (const e of remaining) {
|
|
126
|
+
findings++;
|
|
127
|
+
const col = e.errorRange?.[0] ? `:${e.errorRange[0]}` : '';
|
|
128
|
+
logger.warn(`${rel}:${e.lineNumber}${col} ${e.ruleNames.join('/')} ${e.ruleDescription}` +
|
|
129
|
+
(e.errorDetail ? ` [${e.errorDetail}]` : ''));
|
|
130
|
+
}
|
|
96
131
|
}
|
|
97
|
-
return warn('markdown lint issues found');
|
|
132
|
+
return findings === 0 ? pass() : warn('markdown lint issues found');
|
|
98
133
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
134
|
+
catch (err) {
|
|
135
|
+
// An unreadable config or a file that vanished: the tool could not run,
|
|
136
|
+
// which is not the same claim as "the prose has problems".
|
|
137
|
+
logger.warn(`[Markdown]: ⚠️ ${err}`);
|
|
138
|
+
return warn('markdownlint failed to run (exit 2) — check the markdownlint config');
|
|
102
139
|
}
|
|
103
140
|
},
|
|
104
141
|
};
|