pr-checkmate 1.23.19 → 1.23.21
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/README.md +20 -2
- package/cspell.json +19 -17
- 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/spellcheck.d.ts +25 -0
- package/dist/core/checks/quality/spellcheck.js +71 -11
- 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 +22 -18
- package/dist/interfaces/index.d.ts +12 -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 +20 -4
- package/package.json +2 -1
- package/pr-checkmate.schema.json +18 -1
- package/src/config/.eslintrc.js +9 -7
- package/src/config/cspell.json +19 -17
- package/src/config/eslint-default-rules.json +7 -7
- package/src/config/pr-checkmate.schema.json +18 -1
package/dist/core/context.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.planDiffRange = planDiffRange;
|
|
6
7
|
exports.buildContext = buildContext;
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const loader_1 = require("../config/loader");
|
|
@@ -10,41 +11,71 @@ const utils_1 = require("../utils");
|
|
|
10
11
|
const detect_language_1 = require("../utils/detect-language");
|
|
11
12
|
const execa_1 = require("../utils/execa");
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Decide what to look at. Order matters and is the contract:
|
|
15
|
+
*
|
|
16
|
+
* 1. `full` wins over everything — an explicit "check all of it" must not be
|
|
17
|
+
* quietly narrowed by a CI variable that happens to be set.
|
|
18
|
+
* 2. `staged`, then caller-supplied SHAs.
|
|
19
|
+
* 3. The CI platform's PR context (GitHub event payload, Bitbucket destination
|
|
20
|
+
* branch) — a gate exists to judge the diff.
|
|
21
|
+
* 4. A CI build with no PR context still diffs `HEAD^`: it is a push/branch build,
|
|
22
|
+
* and delta is why CI runs are fast.
|
|
23
|
+
* 5. Anything else is a human at a terminal → full scan. `HEAD^` used to be the
|
|
24
|
+
* fallback here, which meant a local `pr-checkmate all` judged only the last
|
|
25
|
+
* commit and could report a clean project with a `debugger` committed earlier.
|
|
26
|
+
*
|
|
27
|
+
* @param opts Run options as passed to `runChecks`.
|
|
28
|
+
* @param env Environment to read CI markers from. Defaults to the current process.
|
|
16
29
|
*/
|
|
30
|
+
function planDiffRange(opts, env = process.env) {
|
|
31
|
+
if (opts.full)
|
|
32
|
+
return { kind: 'full', reason: 'requested' };
|
|
33
|
+
if (opts.staged)
|
|
34
|
+
return { kind: 'staged' };
|
|
35
|
+
if (opts.baseSha || opts.headSha) {
|
|
36
|
+
return { kind: 'explicit', baseSha: opts.baseSha, headSha: opts.headSha };
|
|
37
|
+
}
|
|
38
|
+
if (env.GITHUB_EVENT_NAME === 'pull_request' && env.GITHUB_EVENT_PATH) {
|
|
39
|
+
return { kind: 'github-event' };
|
|
40
|
+
}
|
|
41
|
+
if (env.BITBUCKET_PR_DESTINATION_BRANCH) {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'ref',
|
|
44
|
+
ref: `origin/${env.BITBUCKET_PR_DESTINATION_BRANCH}`,
|
|
45
|
+
reason: 'PR build',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if ((0, utils_1.isCiEnvironment)(env))
|
|
49
|
+
return { kind: 'ref', ref: 'HEAD^', reason: 'CI build, no PR context' };
|
|
50
|
+
return { kind: 'full', reason: 'no CI detected' };
|
|
51
|
+
}
|
|
17
52
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
53
|
+
* Carry out a {@link RangePlan}. Every path that cannot produce a usable base
|
|
54
|
+
* commit returns no SHAs, which the checks read as "scan everything" — degrading
|
|
55
|
+
* to a full scan is always safer than diffing against a ref that isn't there and
|
|
56
|
+
* silently reviewing nothing.
|
|
57
|
+
*
|
|
58
|
+
* @returns The resolved range, plus a label for the log line.
|
|
24
59
|
*/
|
|
25
|
-
async function resolveRef(cwd, ref) {
|
|
26
|
-
const { stdout } = await (0, execa_1.execa)('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
|
|
27
|
-
cwd,
|
|
28
|
-
reject: false,
|
|
29
|
-
});
|
|
30
|
-
return stdout.trim() || undefined;
|
|
31
|
-
}
|
|
32
60
|
async function resolveDiffRange(cwd, opts) {
|
|
61
|
+
const plan = planDiffRange(opts, process.env);
|
|
62
|
+
if (plan.kind === 'full')
|
|
63
|
+
return { scope: `full (${plan.reason})` };
|
|
33
64
|
// Pre-commit mode: diff HEAD against the current index. `git write-tree`
|
|
34
65
|
// materialises the staged index as a real tree object, so every check that
|
|
35
66
|
// diffs baseSha..headSha sees exactly the staged changes — no per-check code.
|
|
36
|
-
if (
|
|
67
|
+
if (plan.kind === 'staged') {
|
|
37
68
|
try {
|
|
38
69
|
const head = (await (0, execa_1.execa)('git', ['rev-parse', 'HEAD'], { cwd })).stdout.trim();
|
|
39
70
|
const tree = (await (0, execa_1.execa)('git', ['write-tree'], { cwd })).stdout.trim();
|
|
40
|
-
return { baseSha: head, headSha: tree };
|
|
71
|
+
return { baseSha: head, headSha: tree, scope: 'staged' };
|
|
41
72
|
}
|
|
42
73
|
catch {
|
|
43
|
-
return {};
|
|
74
|
+
return { scope: 'full (no staged range)' };
|
|
44
75
|
}
|
|
45
76
|
}
|
|
46
|
-
if (
|
|
47
|
-
return { baseSha:
|
|
77
|
+
if (plan.kind === 'explicit') {
|
|
78
|
+
return { baseSha: plan.baseSha, headSha: plan.headSha, scope: 'delta (explicit)' };
|
|
48
79
|
}
|
|
49
80
|
let headSha;
|
|
50
81
|
try {
|
|
@@ -52,30 +83,32 @@ async function resolveDiffRange(cwd, opts) {
|
|
|
52
83
|
headSha = stdout.trim();
|
|
53
84
|
}
|
|
54
85
|
catch {
|
|
55
|
-
return {};
|
|
86
|
+
return { scope: 'full (no HEAD)' };
|
|
56
87
|
}
|
|
57
88
|
// GitHub Actions: the PR event payload carries the base branch SHA. Read it
|
|
58
89
|
// directly so delta works without relying on any client-set variable
|
|
59
90
|
// (GITHUB_EVENT_* are injected by the runner).
|
|
60
|
-
if (
|
|
91
|
+
if (plan.kind === 'github-event') {
|
|
61
92
|
try {
|
|
62
93
|
const event = JSON.parse(node_fs_1.default.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf-8'));
|
|
63
94
|
const eventBase = event?.pull_request?.base?.sha;
|
|
64
95
|
// Only use it if the commit is actually present in this clone; otherwise
|
|
65
96
|
// fall through so a shallow clone degrades to a full scan, not 0 files.
|
|
66
|
-
const baseSha = eventBase ? await
|
|
97
|
+
const baseSha = eventBase ? await (0, utils_1.resolveGitRef)(cwd, eventBase) : undefined;
|
|
67
98
|
if (baseSha)
|
|
68
|
-
return { baseSha, headSha };
|
|
99
|
+
return { baseSha, headSha, scope: 'delta (PR event)' };
|
|
69
100
|
}
|
|
70
101
|
catch {
|
|
71
|
-
// Malformed/unreadable event — fall through to
|
|
102
|
+
// Malformed/unreadable event — fall through to a full scan below.
|
|
72
103
|
}
|
|
104
|
+
return { headSha, scope: 'full (PR base unavailable)' };
|
|
73
105
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
const baseSha = await (0, utils_1.resolveGitRef)(cwd, plan.ref);
|
|
107
|
+
return {
|
|
108
|
+
baseSha,
|
|
109
|
+
headSha,
|
|
110
|
+
scope: baseSha ? `delta (${plan.ref}, ${plan.reason})` : `full (${plan.ref} unresolvable)`,
|
|
111
|
+
};
|
|
79
112
|
}
|
|
80
113
|
/**
|
|
81
114
|
* Assemble the shared CheckContext once per run. Language detection is skipped
|
|
@@ -84,9 +117,15 @@ async function resolveDiffRange(cwd, opts) {
|
|
|
84
117
|
async function buildContext(opts = {}) {
|
|
85
118
|
const cwd = opts.cwd ?? process.cwd();
|
|
86
119
|
const config = opts.config ?? (0, loader_1.loadProjectConfig)();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
120
|
+
// Detection reads the client's `ignoreDirs` from the config already loaded here,
|
|
121
|
+
// rather than calling getIgnoredDirs() — that one resolves against process.cwd(),
|
|
122
|
+
// which is not necessarily the `cwd` under review.
|
|
123
|
+
const languages = opts.languages ?? (await (0, detect_language_1.detectLanguages)(cwd, config.ignoreDirs));
|
|
124
|
+
const { baseSha, headSha, scope } = await resolveDiffRange(cwd, opts);
|
|
125
|
+
// `scope` is logged because it decides what the whole run looks at, and getting
|
|
126
|
+
// it wrong is invisible in the results: a run that reviewed one commit and one
|
|
127
|
+
// that reviewed the repository both just print passes.
|
|
128
|
+
utils_1.logger.info(`[buildContext]: cwd=${cwd} languages=[${languages.join(', ') || 'none'}] write=${opts.write ?? false} scope=${scope}`);
|
|
90
129
|
return {
|
|
91
130
|
cwd,
|
|
92
131
|
languages,
|
package/dist/core/targets.js
CHANGED
|
@@ -27,7 +27,7 @@ function excludeIgnoredDirs(files, keepDirs = []) {
|
|
|
27
27
|
ignored.delete(dir);
|
|
28
28
|
if (ignored.size === 0)
|
|
29
29
|
return files;
|
|
30
|
-
return files.filter(f => !
|
|
30
|
+
return files.filter(f => !(0, utils_1.isUnderIgnoredDir)(f, ignored));
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Lists the files a language check should operate on, matched by pathspec `globs`
|
package/dist/core/types.d.ts
CHANGED
|
@@ -90,6 +90,15 @@ export interface RunOptions {
|
|
|
90
90
|
* (`git write-tree`), so every delta-aware check works unchanged.
|
|
91
91
|
*/
|
|
92
92
|
staged?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Review every tracked file instead of a diff (`--full` on the CLI, and the
|
|
95
|
+
* default when no CI environment is detected). Takes precedence over every other
|
|
96
|
+
* range source, including CI variables — see `planDiffRange`. The 16 checks that
|
|
97
|
+
* are about a diff by definition (PR Size, Diff Security, Merge Conflict, …) gate
|
|
98
|
+
* their own `applies` on `baseSha`, so they drop out of the report rather than
|
|
99
|
+
* misreporting.
|
|
100
|
+
*/
|
|
101
|
+
full?: boolean;
|
|
93
102
|
}
|
|
94
103
|
export interface CheckReport {
|
|
95
104
|
results: CheckResult[];
|
package/dist/init.js
CHANGED
|
@@ -11,20 +11,6 @@ const inquirer_1 = __importDefault(require("inquirer"));
|
|
|
11
11
|
const config_1 = require("./config");
|
|
12
12
|
const utils_1 = require("./utils");
|
|
13
13
|
const CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
|
|
14
|
-
/**
|
|
15
|
-
* Load the canonical default ESLint rules (the same set eslint.config.mjs
|
|
16
|
-
* applies) so the generated pr-checkmate.json lists every rule for the user to
|
|
17
|
-
* change or disable. Falls back to an empty object if the file is unavailable.
|
|
18
|
-
*/
|
|
19
|
-
function loadDefaultEslintRules() {
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(fs_1.default.readFileSync((0, config_1.getPackageConfigPath)('eslint-default-rules.json'), 'utf-8'));
|
|
22
|
-
}
|
|
23
|
-
catch (err) {
|
|
24
|
-
utils_1.logger.warn(`[init]: ⚠️ Could not load default ESLint rules: ${err}`);
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
14
|
/**
|
|
29
15
|
* Default configuration structure with customization options
|
|
30
16
|
*/
|
|
@@ -48,18 +34,36 @@ const DEFAULT_CONFIG = {
|
|
|
48
34
|
// Directories excluded from every check (matched on any path segment). Edit
|
|
49
35
|
// freely — add your own generated/vendored dirs or remove ones you want scanned.
|
|
50
36
|
ignoreDirs: config_1.IGNORED_DIRS,
|
|
51
|
-
// ESLint —
|
|
52
|
-
//
|
|
37
|
+
// ESLint — `rules` starts EMPTY on purpose and holds overrides only, each merged
|
|
38
|
+
// over the bundled default for that one rule.
|
|
39
|
+
//
|
|
40
|
+
// It used to be written out in full (all 27 rules with their severities) so every
|
|
41
|
+
// rule was visible and editable in this file. That copy then won over the bundled
|
|
42
|
+
// defaults forever: `eslint.rules` is in REPLACED_OBJECT_PATHS, so a later `init`
|
|
43
|
+
// keeps the user's block wholesale, and improvements to the defaults never reached
|
|
44
|
+
// a repo that had ever run `init`. Downgrading `prettier/prettier` from error to
|
|
45
|
+
// warn — so formatting stops failing pipelines — is exactly such an improvement,
|
|
46
|
+
// and it stopped at the config boundary for every existing client.
|
|
47
|
+
//
|
|
48
|
+
// The full list with defaults lives in the ESLint check documentation and in the
|
|
49
|
+
// package's own eslint-default-rules.json; discoverability there costs nothing,
|
|
50
|
+
// un-updatable defaults cost every client an edit.
|
|
53
51
|
eslint: {
|
|
54
|
-
rules:
|
|
52
|
+
rules: {},
|
|
55
53
|
ignorePatterns: [],
|
|
56
54
|
},
|
|
57
55
|
// Prettier customization
|
|
58
56
|
prettier: {
|
|
59
57
|
rules: {},
|
|
60
58
|
},
|
|
61
|
-
//
|
|
59
|
+
// Spellcheck (cspell). `enabled: false` turns the check off — that part always
|
|
60
|
+
// applies. The lists feed the cspell config pr-checkmate generates, so they are
|
|
61
|
+
// ignored when the repo has a cspell.json of its own; all of them are appended to
|
|
62
|
+
// the bundled defaults. `ignorePaths` takes globs to skip (e.g. "CHANGELOG.md",
|
|
63
|
+
// "generated/**"), anchored to the repo root so a pattern never matches a
|
|
64
|
+
// directory above the checkout.
|
|
62
65
|
cspell: {
|
|
66
|
+
enabled: true,
|
|
63
67
|
words: [],
|
|
64
68
|
ignorePaths: [],
|
|
65
69
|
ignoreRegExpList: [],
|
|
@@ -19,6 +19,8 @@ export interface CspellConfig {
|
|
|
19
19
|
ignorePaths: string[];
|
|
20
20
|
enableFiletypes: string[];
|
|
21
21
|
ignoreRegExpList: string[];
|
|
22
|
+
dictionaries?: string[];
|
|
23
|
+
enabled?: boolean;
|
|
22
24
|
}
|
|
23
25
|
export interface PRCheckMateConfig {
|
|
24
26
|
sourcePath: string | string[];
|
|
@@ -42,14 +44,24 @@ export interface PRCheckMateConfig {
|
|
|
42
44
|
config?: Record<string, any>;
|
|
43
45
|
};
|
|
44
46
|
spellcheck?: {
|
|
47
|
+
enabled?: boolean;
|
|
45
48
|
words?: string[];
|
|
46
49
|
ignoreWords?: string[];
|
|
47
50
|
ignorePaths?: string[];
|
|
48
51
|
};
|
|
52
|
+
cspell?: {
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
words?: string[];
|
|
55
|
+
ignorePaths?: string[];
|
|
56
|
+
ignoreRegExpList?: string[];
|
|
57
|
+
dictionaries?: string[];
|
|
58
|
+
language?: string;
|
|
59
|
+
};
|
|
49
60
|
security?: {
|
|
50
61
|
gitleaks?: {
|
|
51
62
|
enabled?: boolean;
|
|
52
63
|
version?: string;
|
|
64
|
+
depth?: number;
|
|
53
65
|
};
|
|
54
66
|
'npm-audit'?: {
|
|
55
67
|
enabled?: boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when running inside a CI/CD build.
|
|
3
|
+
*
|
|
4
|
+
* Decides the default review scope: a build looks at the diff (that is the point
|
|
5
|
+
* of a PR gate), a human at a terminal looks at the whole project — the local
|
|
6
|
+
* `HEAD^` default used to check only the last commit, so a repo could report a
|
|
7
|
+
* clean run with a `debugger` sitting in it.
|
|
8
|
+
*
|
|
9
|
+
* @param env Environment to inspect. Defaults to the current process.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isCiEnvironment(env?: NodeJS.ProcessEnv): boolean;
|
package/dist/utils/ci.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isCiEnvironment = isCiEnvironment;
|
|
4
|
+
/**
|
|
5
|
+
* Environment variables that mean "this is an automated build". `CI` alone covers
|
|
6
|
+
* GitHub Actions, GitLab, Bitbucket, CircleCI, Buildkite, Travis and most others;
|
|
7
|
+
* the rest are providers that either predate the convention or set only their own
|
|
8
|
+
* marker (Jenkins, TeamCity, Azure Pipelines, AWS CodeBuild).
|
|
9
|
+
*
|
|
10
|
+
* This answers a different question from `detectPlatform()` in reporters.ts, which
|
|
11
|
+
* picks WHERE to post a PR comment and therefore only knows the three platforms we
|
|
12
|
+
* can comment on. A Jenkins build is CI but has nowhere to post.
|
|
13
|
+
*/
|
|
14
|
+
const CI_ENV_VARS = [
|
|
15
|
+
'CI',
|
|
16
|
+
'GITHUB_ACTIONS',
|
|
17
|
+
'GITLAB_CI',
|
|
18
|
+
'BITBUCKET_BUILD_NUMBER',
|
|
19
|
+
'JENKINS_URL',
|
|
20
|
+
'TEAMCITY_VERSION',
|
|
21
|
+
'TF_BUILD',
|
|
22
|
+
'CIRCLECI',
|
|
23
|
+
'BUILDKITE',
|
|
24
|
+
'CODEBUILD_BUILD_ID',
|
|
25
|
+
];
|
|
26
|
+
/** Values that are present but mean "no" — some tooling exports `CI=false`. */
|
|
27
|
+
const FALSY = new Set(['', '0', 'false', 'no', 'off']);
|
|
28
|
+
/**
|
|
29
|
+
* True when running inside a CI/CD build.
|
|
30
|
+
*
|
|
31
|
+
* Decides the default review scope: a build looks at the diff (that is the point
|
|
32
|
+
* of a PR gate), a human at a terminal looks at the whole project — the local
|
|
33
|
+
* `HEAD^` default used to check only the last commit, so a repo could report a
|
|
34
|
+
* clean run with a `debugger` sitting in it.
|
|
35
|
+
*
|
|
36
|
+
* @param env Environment to inspect. Defaults to the current process.
|
|
37
|
+
*/
|
|
38
|
+
function isCiEnvironment(env = process.env) {
|
|
39
|
+
return CI_ENV_VARS.some(name => {
|
|
40
|
+
const value = env[name];
|
|
41
|
+
return value !== undefined && !FALSY.has(value.trim().toLowerCase());
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -4,7 +4,12 @@ export type Language = 'typescript' | 'javascript' | 'python' | 'cpp' | 'swift'
|
|
|
4
4
|
* with tracked source-file counts (breadth). A language is reported when it has a
|
|
5
5
|
* marker file OR at least one tracked source file.
|
|
6
6
|
*
|
|
7
|
+
* @param root Repository root.
|
|
8
|
+
* @param ignoreDirs Directory names whose contents do not count as the repo's own
|
|
9
|
+
* code. Defaults to the curated list; `buildContext` passes the client's
|
|
10
|
+
* `ignoreDirs` so a repo that vendors dependencies is described by its own
|
|
11
|
+
* sources. Marker files are unaffected — they are only ever read from `root`.
|
|
7
12
|
* @returns languages ordered by tracked file count (most-used first). `.h` headers
|
|
8
13
|
* are intentionally excluded from the C++ count since they are ambiguous with C.
|
|
9
14
|
*/
|
|
10
|
-
export declare function detectLanguages(root?: string): Promise<Language[]>;
|
|
15
|
+
export declare function detectLanguages(root?: string, ignoreDirs?: string[]): Promise<Language[]>;
|
|
@@ -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
|
|
|
@@ -117,4 +133,4 @@ export default [
|
|
|
117
133
|
|
|
118
134
|
rules: mergedRules,
|
|
119
135
|
},
|
|
120
|
-
];
|
|
136
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-checkmate",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.21",
|
|
4
4
|
"description": "Automated PR quality checks: linting, formatting, dependency analysis, and spellcheck",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -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"
|