pr-checkmate 1.23.20 → 1.23.22
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/.eslintrc.js +9 -7
- package/LICENSE +3 -3
- package/README.md +57 -9
- package/cspell.json +504 -1
- package/dist/cli.js +48 -3
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/constants.js +2 -1
- package/dist/core/checks/languages/lint.js +7 -0
- package/dist/core/checks/pr/commitlint.js +14 -3
- package/dist/core/checks/quality/yaml-lint.d.ts +7 -0
- package/dist/core/checks/quality/yaml-lint.js +24 -1
- package/dist/core/checks/security/security.js +21 -1
- package/dist/core/context.d.ts +47 -0
- package/dist/core/context.js +73 -34
- package/dist/core/targets.js +1 -1
- package/dist/core/types.d.ts +9 -0
- package/dist/init.js +15 -17
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/utils/ci.d.ts +11 -0
- package/dist/utils/ci.js +43 -0
- package/dist/utils/detect-language.d.ts +6 -1
- package/dist/utils/detect-language.js +18 -3
- package/dist/utils/git.d.ts +14 -0
- package/dist/utils/git.js +24 -0
- package/dist/utils/glob.d.ts +14 -0
- package/dist/utils/glob.js +20 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/eslint-default-rules.json +7 -7
- package/eslint.config.mjs +38 -4
- package/package.json +3 -2
- package/pr-checkmate.schema.json +6 -1
- package/src/config/.eslintrc.js +9 -7
- package/src/config/cspell.json +504 -1
- package/src/config/eslint-default-rules.json +7 -7
- package/src/config/pr-checkmate.schema.json +6 -1
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.detectLanguages = detectLanguages;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const constants_1 = require("../config/constants");
|
|
9
10
|
const execa_1 = require("./execa");
|
|
10
11
|
const glob_1 = require("./glob");
|
|
11
12
|
const logger_1 = require("./logger");
|
|
@@ -70,7 +71,7 @@ function hasMarker(root, lang) {
|
|
|
70
71
|
* when git is unavailable or the directory is not a repo — callers then fall
|
|
71
72
|
* back to marker files alone.
|
|
72
73
|
*/
|
|
73
|
-
async function countTrackedFiles(root) {
|
|
74
|
+
async function countTrackedFiles(root, ignoreDirs) {
|
|
74
75
|
const counts = {
|
|
75
76
|
typescript: 0,
|
|
76
77
|
javascript: 0,
|
|
@@ -87,9 +88,18 @@ async function countTrackedFiles(root) {
|
|
|
87
88
|
try {
|
|
88
89
|
const { stdout } = await (0, execa_1.execa)('git', ['ls-files'], { cwd: root });
|
|
89
90
|
const files = stdout.split('\n').map(f => f.trim());
|
|
91
|
+
const ignored = new Set(ignoreDirs);
|
|
90
92
|
for (const file of files) {
|
|
91
93
|
if (!file)
|
|
92
94
|
continue;
|
|
95
|
+
// A repository that commits its dependencies (`node_modules`, `vendor`,
|
|
96
|
+
// `third_party`) would otherwise be detected as a Python/Go/PHP project off
|
|
97
|
+
// the back of files nobody in it wrote, switching on whole toolchains for
|
|
98
|
+
// languages absent from the source. Detection has to describe the repo's own
|
|
99
|
+
// code, since it decides which checks run at all — the same exclusion every
|
|
100
|
+
// check's file list already applies via `excludeIgnoredDirs`.
|
|
101
|
+
if ((0, glob_1.isUnderIgnoredDir)(file, ignored))
|
|
102
|
+
continue;
|
|
93
103
|
const ext = node_path_1.default.extname(file).toLowerCase();
|
|
94
104
|
for (const lang of Object.keys(EXTENSIONS)) {
|
|
95
105
|
if (EXTENSIONS[lang].includes(ext)) {
|
|
@@ -109,11 +119,16 @@ async function countTrackedFiles(root) {
|
|
|
109
119
|
* with tracked source-file counts (breadth). A language is reported when it has a
|
|
110
120
|
* marker file OR at least one tracked source file.
|
|
111
121
|
*
|
|
122
|
+
* @param root Repository root.
|
|
123
|
+
* @param ignoreDirs Directory names whose contents do not count as the repo's own
|
|
124
|
+
* code. Defaults to the curated list; `buildContext` passes the client's
|
|
125
|
+
* `ignoreDirs` so a repo that vendors dependencies is described by its own
|
|
126
|
+
* sources. Marker files are unaffected — they are only ever read from `root`.
|
|
112
127
|
* @returns languages ordered by tracked file count (most-used first). `.h` headers
|
|
113
128
|
* are intentionally excluded from the C++ count since they are ambiguous with C.
|
|
114
129
|
*/
|
|
115
|
-
async function detectLanguages(root = process.cwd()) {
|
|
116
|
-
const counts = await countTrackedFiles(root);
|
|
130
|
+
async function detectLanguages(root = process.cwd(), ignoreDirs = constants_1.IGNORED_DIRS) {
|
|
131
|
+
const counts = await countTrackedFiles(root, ignoreDirs);
|
|
117
132
|
const detected = Object.keys(EXTENSIONS).filter(lang => hasMarker(root, lang) || counts[lang] > 0);
|
|
118
133
|
detected.sort((a, b) => counts[b] - counts[a]);
|
|
119
134
|
const countStr = Object.keys(counts).map(l => `${l}=${counts[l]}`).join(', ');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a git ref to a concrete commit SHA, or `undefined` when this clone
|
|
3
|
+
* cannot resolve it.
|
|
4
|
+
*
|
|
5
|
+
* `--verify --quiet` emits nothing (rather than echoing the literal ref) and exits
|
|
6
|
+
* non-zero for a missing ref, so the caller gets a clear "not here" instead of a
|
|
7
|
+
* string that looks like a ref but resolves to nothing. Refs go missing more often
|
|
8
|
+
* than it seems: `HEAD^` on a repository's first commit, and any ancestor in a
|
|
9
|
+
* shallow CI clone — `actions/checkout` fetches depth 1 by default.
|
|
10
|
+
*
|
|
11
|
+
* Every caller must degrade rather than fail when this returns `undefined`: a
|
|
12
|
+
* missing base means "review everything", never "review nothing".
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveGitRef(cwd: string, ref: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveGitRef = resolveGitRef;
|
|
4
|
+
const execa_1 = require("./execa");
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a git ref to a concrete commit SHA, or `undefined` when this clone
|
|
7
|
+
* cannot resolve it.
|
|
8
|
+
*
|
|
9
|
+
* `--verify --quiet` emits nothing (rather than echoing the literal ref) and exits
|
|
10
|
+
* non-zero for a missing ref, so the caller gets a clear "not here" instead of a
|
|
11
|
+
* string that looks like a ref but resolves to nothing. Refs go missing more often
|
|
12
|
+
* than it seems: `HEAD^` on a repository's first commit, and any ancestor in a
|
|
13
|
+
* shallow CI clone — `actions/checkout` fetches depth 1 by default.
|
|
14
|
+
*
|
|
15
|
+
* Every caller must degrade rather than fail when this returns `undefined`: a
|
|
16
|
+
* missing base means "review everything", never "review nothing".
|
|
17
|
+
*/
|
|
18
|
+
async function resolveGitRef(cwd, ref) {
|
|
19
|
+
const { stdout } = await (0, execa_1.execa)('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
|
|
20
|
+
cwd,
|
|
21
|
+
reject: false,
|
|
22
|
+
});
|
|
23
|
+
return stdout.trim() || undefined;
|
|
24
|
+
}
|
package/dist/utils/glob.d.ts
CHANGED
|
@@ -8,3 +8,17 @@
|
|
|
8
8
|
export declare function globToRegExp(glob: string): RegExp;
|
|
9
9
|
/** True when `file` matches any of the glob `patterns`. */
|
|
10
10
|
export declare function matchesAnyGlob(file: string, patterns: string[]): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* True when any segment of a repo-relative path names an excluded directory —
|
|
13
|
+
* `node_modules/x/y.py` for `node_modules`, at any nesting depth.
|
|
14
|
+
*
|
|
15
|
+
* Shared so the two consumers cannot drift: the file list every check operates on
|
|
16
|
+
* (`excludeIgnoredDirs` in core/targets.ts) and language detection, which counts
|
|
17
|
+
* tracked source files. They disagreed once — detection counted a repository that
|
|
18
|
+
* commits its dependencies as a Python/Go/PHP project and switched on the checks
|
|
19
|
+
* for languages nobody had written a line of.
|
|
20
|
+
*
|
|
21
|
+
* @param file Repo-relative path, forward slashes.
|
|
22
|
+
* @param ignoredDirs Directory NAMES (not globs) to exclude.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isUnderIgnoredDir(file: string, ignoredDirs: Iterable<string>): boolean;
|
package/dist/utils/glob.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.globToRegExp = globToRegExp;
|
|
4
4
|
exports.matchesAnyGlob = matchesAnyGlob;
|
|
5
|
+
exports.isUnderIgnoredDir = isUnderIgnoredDir;
|
|
5
6
|
/**
|
|
6
7
|
* Minimal glob → RegExp for path matching against config patterns. Supports `*`
|
|
7
8
|
* (matches within a path segment), `**` (across segments), and a leading `** /`
|
|
@@ -23,3 +24,22 @@ function globToRegExp(glob) {
|
|
|
23
24
|
function matchesAnyGlob(file, patterns) {
|
|
24
25
|
return patterns.some(p => globToRegExp(p).test(file));
|
|
25
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* True when any segment of a repo-relative path names an excluded directory —
|
|
29
|
+
* `node_modules/x/y.py` for `node_modules`, at any nesting depth.
|
|
30
|
+
*
|
|
31
|
+
* Shared so the two consumers cannot drift: the file list every check operates on
|
|
32
|
+
* (`excludeIgnoredDirs` in core/targets.ts) and language detection, which counts
|
|
33
|
+
* tracked source files. They disagreed once — detection counted a repository that
|
|
34
|
+
* commits its dependencies as a Python/Go/PHP project and switched on the checks
|
|
35
|
+
* for languages nobody had written a line of.
|
|
36
|
+
*
|
|
37
|
+
* @param file Repo-relative path, forward slashes.
|
|
38
|
+
* @param ignoredDirs Directory NAMES (not globs) to exclude.
|
|
39
|
+
*/
|
|
40
|
+
function isUnderIgnoredDir(file, ignoredDirs) {
|
|
41
|
+
const ignored = ignoredDirs instanceof Set ? ignoredDirs : new Set(ignoredDirs);
|
|
42
|
+
if (ignored.size === 0)
|
|
43
|
+
return false;
|
|
44
|
+
return file.split('/').some(segment => ignored.has(segment));
|
|
45
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -28,6 +28,8 @@ __exportStar(require("./direct-deps"), exports);
|
|
|
28
28
|
__exportStar(require("./detect-language"), exports);
|
|
29
29
|
__exportStar(require("./glob"), exports);
|
|
30
30
|
__exportStar(require("./update-notifier"), exports);
|
|
31
|
+
__exportStar(require("./ci"), exports);
|
|
32
|
+
__exportStar(require("./git"), exports);
|
|
31
33
|
/**
|
|
32
34
|
*
|
|
33
35
|
*/
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"space-before-function-paren": "off",
|
|
14
14
|
"operator-linebreak": "off",
|
|
15
15
|
"max-len": ["warn", { "code": 100 }],
|
|
16
|
-
"semi": ["
|
|
17
|
-
"quotes": ["
|
|
18
|
-
"object-curly-spacing": ["
|
|
19
|
-
"space-infix-ops": "
|
|
20
|
-
"keyword-spacing": ["
|
|
16
|
+
"semi": ["warn", "always"],
|
|
17
|
+
"quotes": ["warn", "single", { "avoidEscape": true }],
|
|
18
|
+
"object-curly-spacing": ["warn", "always"],
|
|
19
|
+
"space-infix-ops": "warn",
|
|
20
|
+
"keyword-spacing": ["warn", { "before": true, "after": true }],
|
|
21
21
|
"padding-line-between-statements": [
|
|
22
|
-
"
|
|
22
|
+
"warn",
|
|
23
23
|
{ "blankLine": "always", "prev": "block", "next": "*" },
|
|
24
24
|
{ "blankLine": "always", "prev": "*", "next": "return" }
|
|
25
25
|
],
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"jsdoc/check-tag-names": "warn",
|
|
51
51
|
"jsdoc/check-types": "warn",
|
|
52
52
|
"prettier/prettier": [
|
|
53
|
-
"
|
|
53
|
+
"warn",
|
|
54
54
|
{
|
|
55
55
|
"semi": true,
|
|
56
56
|
"singleQuote": true,
|
package/eslint.config.mjs
CHANGED
|
@@ -12,6 +12,22 @@ import jsdocPlugin from 'eslint-plugin-jsdoc';
|
|
|
12
12
|
* Default ESLint rules — single source of truth, shared with init.ts so the
|
|
13
13
|
* pr-checkmate.json it generates lists exactly the rules applied here. Edit the
|
|
14
14
|
* rules in `eslint-default-rules.json` (shipped alongside this file).
|
|
15
|
+
*
|
|
16
|
+
* SEVERITY POLICY: `error` is reserved for defects a human has to resolve —
|
|
17
|
+
* `no-debugger`, `eqeqeq`, `prefer-const`, `@typescript-eslint/no-unused-vars`,
|
|
18
|
+
* `@typescript-eslint/no-floating-promises`, `import/no-duplicates`. Everything
|
|
19
|
+
* that is purely formatting is `warn`, even
|
|
20
|
+
* though it is auto-fixable, precisely BECAUSE it is auto-fixable: ESLint exits 1
|
|
21
|
+
* on errors only, and the ESLint check maps exit 1 to a blocking failure, so an
|
|
22
|
+
* `error`-level whitespace rule turns `npx prettier --write` worth of work into a
|
|
23
|
+
* red gate on someone's PR. Formatting still gets reported — as a warning here and
|
|
24
|
+
* by the dedicated Prettier check, which has always been advisory. A repo that
|
|
25
|
+
* wants formatting to gate promotes it back with
|
|
26
|
+
* `eslint.rules: { "prettier/prettier": "error" }` or
|
|
27
|
+
* `severity: { "Prettier": "error" }` in pr-checkmate.json.
|
|
28
|
+
*
|
|
29
|
+
* A JSON file cannot carry this note, hence it lives here; `src/config/.eslintrc.js`
|
|
30
|
+
* holds the same rule set for the legacy ESLint <9 path and must stay in sync.
|
|
15
31
|
*/
|
|
16
32
|
const require = createRequire(import.meta.url);
|
|
17
33
|
export const defaultRules = require('./eslint-default-rules.json');
|
|
@@ -34,7 +50,7 @@ const defaultIgnores = [
|
|
|
34
50
|
function loadUserConfig() {
|
|
35
51
|
let currentDir = process.cwd();
|
|
36
52
|
let configPath = path.join(currentDir, 'pr-checkmate.json');
|
|
37
|
-
|
|
53
|
+
|
|
38
54
|
// If running from node_modules, find project root
|
|
39
55
|
if (currentDir.includes('node_modules')) {
|
|
40
56
|
const parts = currentDir.split(path.sep);
|
|
@@ -44,7 +60,7 @@ function loadUserConfig() {
|
|
|
44
60
|
configPath = path.join(currentDir, 'pr-checkmate.json');
|
|
45
61
|
}
|
|
46
62
|
}
|
|
47
|
-
|
|
63
|
+
|
|
48
64
|
if (fs.existsSync(configPath)) {
|
|
49
65
|
try {
|
|
50
66
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
@@ -56,7 +72,7 @@ function loadUserConfig() {
|
|
|
56
72
|
} else {
|
|
57
73
|
console.error(`[loadUserConfig]: No pr-checkmate.json found, using defaults`);
|
|
58
74
|
}
|
|
59
|
-
|
|
75
|
+
|
|
60
76
|
return { sourcePath: 'src', eslint: {} };
|
|
61
77
|
}
|
|
62
78
|
|
|
@@ -84,6 +100,24 @@ const customIgnoresCount = (userConfig.eslint?.ignorePatterns || []).length;
|
|
|
84
100
|
if (customRulesCount > 0) {
|
|
85
101
|
console.error(`Applied ${customRulesCount} custom ESLint rules`);
|
|
86
102
|
}
|
|
103
|
+
|
|
104
|
+
// A `pr-checkmate.json` written by an older `init` contains a VERBATIM COPY of the
|
|
105
|
+
// bundled rule set — all of it, severities included. Client rules merge on top of
|
|
106
|
+
// the defaults, so that copy silently pins every severity forever: a repo in this
|
|
107
|
+
// state keeps failing on `prettier/prettier: error` long after the bundled default
|
|
108
|
+
// became `warn`, and nothing in the output says why. Detected by rule NAMES rather
|
|
109
|
+
// than values (nobody hand-writes exactly this list), and reported where the person
|
|
110
|
+
// reading a failed lint run will actually see it.
|
|
111
|
+
const defaultRuleNames = Object.keys(defaultRules);
|
|
112
|
+
const userRuleNames = Object.keys(userConfig.eslint?.rules || {});
|
|
113
|
+
if (defaultRuleNames.every(name => userRuleNames.includes(name))) {
|
|
114
|
+
console.error(
|
|
115
|
+
`⚠️ eslint.rules in pr-checkmate.json lists all ${defaultRuleNames.length} built-in rules — ` +
|
|
116
|
+
'a copy written by an older `init`. It overrides the bundled defaults, so updates to them ' +
|
|
117
|
+
'(formatting is advisory since 1.23.21) never reach this repo. Delete the block to track ' +
|
|
118
|
+
'the defaults again, or keep it to pin these severities deliberately.',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
87
121
|
if (customIgnoresCount > 0) {
|
|
88
122
|
console.error(`🚫 Applied ${customIgnoresCount} custom ignore patterns`);
|
|
89
123
|
}
|
|
@@ -117,4 +151,4 @@ export default [
|
|
|
117
151
|
|
|
118
152
|
rules: mergedRules,
|
|
119
153
|
},
|
|
120
|
-
];
|
|
154
|
+
];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-checkmate",
|
|
3
|
-
"version": "1.23.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.23.22",
|
|
4
|
+
"description": "Security-first PR gate for CI: secret scanning, grype dependency scanning, SBOM — plus linting and formatting across 11 languages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
7
7
|
"pull-request",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"docs": "typedoc",
|
|
52
52
|
"scan:secrets": "gitleaks-secret-scanner --diff-mode ci --html-report gitleaks-report.html",
|
|
53
53
|
"test": "node --require ts-node/register --test \"src/**/*.test.ts\"",
|
|
54
|
+
"verify:tarball": "bash scripts/verify-tarball.sh",
|
|
54
55
|
"security-scan": "npx pr-checkmate security",
|
|
55
56
|
"clean": "rm -rf dist",
|
|
56
57
|
"release": "standard-version"
|
package/pr-checkmate.schema.json
CHANGED
|
@@ -108,7 +108,12 @@
|
|
|
108
108
|
"type": "object",
|
|
109
109
|
"properties": {
|
|
110
110
|
"enabled": { "type": "boolean" },
|
|
111
|
-
"version": { "type": "string" }
|
|
111
|
+
"version": { "type": "string" },
|
|
112
|
+
"depth": {
|
|
113
|
+
"description": "Caps the whole-repository history scan used when there is no diff range (a local run or --full) to the last N commits. Ignored in a PR run, which scans the diff range.",
|
|
114
|
+
"type": "integer",
|
|
115
|
+
"minimum": 1
|
|
116
|
+
}
|
|
112
117
|
}
|
|
113
118
|
},
|
|
114
119
|
"npm-audit": {
|
package/src/config/.eslintrc.js
CHANGED
|
@@ -70,13 +70,15 @@ module.exports = {
|
|
|
70
70
|
'space-before-function-paren': 'off',
|
|
71
71
|
'operator-linebreak': 'off',
|
|
72
72
|
'max-len': ['warn', { code: 100 }],
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
'
|
|
77
|
-
'
|
|
73
|
+
// Formatting is advisory — see the note above `rules` in eslint-flat-config.mjs.
|
|
74
|
+
// Keep these in sync with src/config/eslint-default-rules.json.
|
|
75
|
+
semi: ['warn', 'always'],
|
|
76
|
+
quotes: ['warn', 'single', { avoidEscape: true }],
|
|
77
|
+
'object-curly-spacing': ['warn', 'always'],
|
|
78
|
+
'space-infix-ops': 'warn',
|
|
79
|
+
'keyword-spacing': ['warn', { before: true, after: true }],
|
|
78
80
|
'padding-line-between-statements': [
|
|
79
|
-
'
|
|
81
|
+
'warn',
|
|
80
82
|
{ blankLine: 'always', prev: 'block', next: '*' },
|
|
81
83
|
{ blankLine: 'always', prev: '*', next: 'return' },
|
|
82
84
|
],
|
|
@@ -108,7 +110,7 @@ module.exports = {
|
|
|
108
110
|
'jsdoc/check-tag-names': 'warn',
|
|
109
111
|
'jsdoc/check-types': 'warn',
|
|
110
112
|
'prettier/prettier': [
|
|
111
|
-
'
|
|
113
|
+
'warn',
|
|
112
114
|
{
|
|
113
115
|
semi: true,
|
|
114
116
|
singleQuote: true,
|