pr-checkmate 1.23.20 → 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 +2 -0
- 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 +20 -4
- package/package.json +2 -1
- package/pr-checkmate.schema.json +6 -1
- package/src/config/.eslintrc.js +9 -7
- package/src/config/cspell.json +2 -0
- package/src/config/eslint-default-rules.json +7 -7
- package/src/config/pr-checkmate.schema.json +6 -1
package/.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,
|
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ every PR.
|
|
|
29
29
|
- One package, any stack. Language is auto-detected from the repo.
|
|
30
30
|
- Bundled tooling. ESLint, Prettier, Ruff, clang-format, and gitleaks run inside the
|
|
31
31
|
package (WASM/JS), so clients install nothing for those languages.
|
|
32
|
-
- Delta mode.
|
|
33
|
-
|
|
32
|
+
- Delta mode. A PR run checks only the changed files; outside CI the default is every tracked
|
|
33
|
+
file. `--full` forces that anywhere, and a run that cannot resolve its base falls back to it.
|
|
34
34
|
- Parallel phases. Checks run concurrently within each phase: blocking, then
|
|
35
35
|
informational, then format.
|
|
36
36
|
- Reports in the PR. Results post as a single comment that updates in place, and
|
|
@@ -79,6 +79,24 @@ Run all checks:
|
|
|
79
79
|
npx pr-checkmate all
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
Scope depends on where the command runs. In CI it reviews the pull request diff, taking the
|
|
83
|
+
base SHA from the GitHub `pull_request` event or Bitbucket's destination branch; a CI build
|
|
84
|
+
with no PR attached falls back to `HEAD^`. Locally, with no CI environment detected, it
|
|
85
|
+
reviews every tracked file. It used to fall back to `HEAD^` there too, so a local run saw
|
|
86
|
+
only the last commit and would report a clean project with a `debugger` committed the day
|
|
87
|
+
before. `--full` forces the whole-repo review anywhere, including inside a CI build, and it
|
|
88
|
+
composes with any command:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx pr-checkmate all --full
|
|
92
|
+
npx pr-checkmate lint --full
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The 16 checks that only make sense on a diff (PR Size, Diff Security, Merge Conflict, Custom
|
|
96
|
+
Rules, Banned Imports, the git-diff family) drop out of the report on a full scan. The
|
|
97
|
+
[per-check reference](https://github.com/Mybono/pr_checkmate_public/blob/main/documentation/checks/INDEX.md)
|
|
98
|
+
spells out the rules under "Review scope".
|
|
99
|
+
|
|
82
100
|
### Running in CI without installing
|
|
83
101
|
|
|
84
102
|
Plenty of repos never add pr-checkmate to `package.json` and call it on the fly in CI
|
package/cspell.json
CHANGED
|
@@ -903,6 +903,7 @@
|
|
|
903
903
|
"riskfolio",
|
|
904
904
|
"robolectric",
|
|
905
905
|
"rowwise",
|
|
906
|
+
"rseidelsohn",
|
|
906
907
|
"Rshum",
|
|
907
908
|
"rtol",
|
|
908
909
|
"rtype",
|
|
@@ -1010,6 +1011,7 @@
|
|
|
1010
1011
|
"taxinfo",
|
|
1011
1012
|
"Tayebeh",
|
|
1012
1013
|
"tcwf",
|
|
1014
|
+
"teamcity",
|
|
1013
1015
|
"telaviv",
|
|
1014
1016
|
"temurin",
|
|
1015
1017
|
"Tenés",
|
package/dist/cli.js
CHANGED
|
@@ -56,8 +56,51 @@ const COMMAND_TO_CHECK = {
|
|
|
56
56
|
};
|
|
57
57
|
/** Commands that run the whole pipeline (or no pipeline at all) and take no siblings. */
|
|
58
58
|
const STANDALONE_COMMANDS = ['init', 'all', 'precommit'];
|
|
59
|
-
|
|
59
|
+
/** Flags accepted anywhere on the command line, with what each one means. */
|
|
60
|
+
const FLAGS = {
|
|
61
|
+
'--full': 'review every tracked file instead of the diff (the default outside CI)',
|
|
62
|
+
};
|
|
63
|
+
const argv = process.argv.slice(2);
|
|
64
|
+
/**
|
|
65
|
+
* Flags are separated from positional arguments so they compose with every command
|
|
66
|
+
* — scope is orthogonal to which checks run, hence `pr-checkmate lint --full` as
|
|
67
|
+
* well as `pr-checkmate all --full`.
|
|
68
|
+
*/
|
|
69
|
+
const flags = argv.filter(arg => arg.startsWith('-'));
|
|
70
|
+
const args = argv.filter(arg => !arg.startsWith('-'));
|
|
60
71
|
const cmd = args[0];
|
|
72
|
+
/**
|
|
73
|
+
* An unrecognised flag aborts the run. Ignoring it would report success for a scope
|
|
74
|
+
* the user did not ask for: a misspelling of `--full` must not quietly review the
|
|
75
|
+
* diff and print a green summary.
|
|
76
|
+
*/
|
|
77
|
+
function rejectUnknownFlags() {
|
|
78
|
+
const unknown = flags.filter(flag => !FLAGS[flag]);
|
|
79
|
+
if (unknown.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
const known = Object.entries(FLAGS)
|
|
82
|
+
.map(([flag, help]) => `${flag} (${help})`)
|
|
83
|
+
.join(', ');
|
|
84
|
+
utils_1.logger.error(`[cli]: Unknown flag(s) "${unknown.join('", "')}". Available: ${known}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `--full` and `precommit` ask for different things — every tracked file versus the
|
|
89
|
+
* staged changes only. `planDiffRange` resolves the conflict in favour of `--full`,
|
|
90
|
+
* which would silently turn a pre-commit hook into a whole-repo scan, so the
|
|
91
|
+
* contradiction is refused here instead.
|
|
92
|
+
*/
|
|
93
|
+
function rejectConflictingScope() {
|
|
94
|
+
if (cmd === 'precommit' && flags.includes('--full')) {
|
|
95
|
+
utils_1.logger.error('[cli]: "precommit --full" is contradictory — precommit reviews the staged changes. Run "pr-checkmate all --full" for a whole-repo review.');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
if (cmd === 'init' && flags.length > 0) {
|
|
99
|
+
utils_1.logger.error(`[cli]: "init" takes no flags, got: ${flags.join(', ')}.`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const full = flags.includes('--full');
|
|
61
104
|
/**
|
|
62
105
|
* Reject extra arguments after a standalone command. Silently ignoring them would
|
|
63
106
|
* report success for work that never ran — `pr-checkmate init lint` must not look
|
|
@@ -75,6 +118,8 @@ function rejectExtraArgs() {
|
|
|
75
118
|
}
|
|
76
119
|
void (async () => {
|
|
77
120
|
try {
|
|
121
|
+
rejectUnknownFlags();
|
|
122
|
+
rejectConflictingScope();
|
|
78
123
|
rejectExtraArgs();
|
|
79
124
|
if (cmd === 'init') {
|
|
80
125
|
await (0, init_1.init)();
|
|
@@ -86,7 +131,7 @@ void (async () => {
|
|
|
86
131
|
// Fetch the update notice concurrently (best-effort, never blocks/affects
|
|
87
132
|
// the exit code), but print it LAST — after the summary — so it's noticeable.
|
|
88
133
|
const noticePromise = (0, utils_1.getUpdateNotice)();
|
|
89
|
-
const report = await (0, core_1.runChecks)({ reporter: 'auto', write: true });
|
|
134
|
+
const report = await (0, core_1.runChecks)({ reporter: 'auto', write: true, full });
|
|
90
135
|
const notice = await noticePromise.catch(() => null);
|
|
91
136
|
if (notice) {
|
|
92
137
|
utils_1.logger.info('');
|
|
@@ -109,7 +154,7 @@ void (async () => {
|
|
|
109
154
|
process.exit(1);
|
|
110
155
|
}
|
|
111
156
|
const checkNames = [...new Set(args.map(arg => COMMAND_TO_CHECK[arg]))];
|
|
112
|
-
const report = await (0, core_1.runChecks)({ only: checkNames, reporter: 'stdout', write: true });
|
|
157
|
+
const report = await (0, core_1.runChecks)({ only: checkNames, reporter: 'stdout', write: true, full });
|
|
113
158
|
process.exit(report.ok ? 0 : 1);
|
|
114
159
|
}
|
|
115
160
|
catch (err) {
|
|
@@ -55,6 +55,7 @@ export declare const SOURCE_EXTENSIONS: {
|
|
|
55
55
|
export declare const DEFAULT_COMMANDS: {
|
|
56
56
|
'npx pr-checkmate init': string;
|
|
57
57
|
'npx pr-checkmate all': string;
|
|
58
|
+
'npx pr-checkmate all --full': string;
|
|
58
59
|
'npx pr-checkmate lint': string;
|
|
59
60
|
'npx pr-checkmate prettier': string;
|
|
60
61
|
'npx pr-checkmate deps': string;
|
package/dist/config/constants.js
CHANGED
|
@@ -181,7 +181,8 @@ exports.SOURCE_EXTENSIONS = {
|
|
|
181
181
|
};
|
|
182
182
|
exports.DEFAULT_COMMANDS = {
|
|
183
183
|
'npx pr-checkmate init': 'Update pr-checkmate.json',
|
|
184
|
-
'npx pr-checkmate all': 'Run all checks',
|
|
184
|
+
'npx pr-checkmate all': 'Run all checks (the PR diff in CI, every tracked file locally)',
|
|
185
|
+
'npx pr-checkmate all --full': 'Run all checks over every tracked file, CI included',
|
|
185
186
|
'npx pr-checkmate lint': 'Lint code using ESLint',
|
|
186
187
|
'npx pr-checkmate prettier': 'Format code using Prettier',
|
|
187
188
|
'npx pr-checkmate deps': 'Check project dependencies',
|
|
@@ -57,6 +57,13 @@ exports.lintCheck = {
|
|
|
57
57
|
// parser, breaking TypeScript linting. Only pass it on the legacy path.
|
|
58
58
|
if (version < 9)
|
|
59
59
|
args.push('--ext', '.ts,.tsx,.js,.jsx');
|
|
60
|
+
// Files are named explicitly, so any of them that the config ignores produces a
|
|
61
|
+
// "File ignored because of a matching ignore pattern" warning. That is a fact
|
|
62
|
+
// about the config, not about the client's code, and a whole-repo run surfaces
|
|
63
|
+
// one per ignored-but-tracked file (`.eslintrc.js`, generated sources). The flag
|
|
64
|
+
// suppresses the warning only — nothing changes about which files are linted.
|
|
65
|
+
if (version >= 9)
|
|
66
|
+
args.push('--no-warn-ignored');
|
|
60
67
|
// Only auto-fix when the caller allows file mutations.
|
|
61
68
|
if (ctx.write)
|
|
62
69
|
args.push('--fix');
|
|
@@ -63,9 +63,20 @@ exports.commitlintCheck = {
|
|
|
63
63
|
const commitlintBin = (0, utils_1.resolvePackageBin)('@commitlint/cli', 'cli.js');
|
|
64
64
|
const hasLocal = (0, config_1.hasLocalConfig)(LOCAL_CONFIG_FILES);
|
|
65
65
|
let tempConfigFile = null;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// With no diff range (a full-repo review, or `--full`) the check still has a
|
|
67
|
+
// meaningful subject: the newest commit message. `HEAD~1` cannot be assumed to
|
|
68
|
+
// exist, though — it is absent on a repository's first commit and in a shallow
|
|
69
|
+
// clone (`actions/checkout` fetches depth 1 by default). Passing it blind made
|
|
70
|
+
// commitlint die with an unhandled "ambiguous argument 'HEAD~1..HEAD'" stack
|
|
71
|
+
// trace in the client's log, and the check then reported "commit message
|
|
72
|
+
// violations found" — a verdict about code quality for what was a crash.
|
|
73
|
+
// `--last` lints exactly the newest commit and needs no ancestor.
|
|
74
|
+
const range = ctx.baseSha
|
|
75
|
+
? ['--from', ctx.baseSha, '--to', ctx.headSha ?? 'HEAD']
|
|
76
|
+
: (await (0, utils_1.resolveGitRef)(ctx.cwd, 'HEAD~1'))
|
|
77
|
+
? ['--from', 'HEAD~1', '--to', ctx.headSha ?? 'HEAD']
|
|
78
|
+
: ['--last'];
|
|
79
|
+
const args = [...range, '--verbose'];
|
|
69
80
|
try {
|
|
70
81
|
if (hasLocal) {
|
|
71
82
|
utils_1.logger.info('[Commitlint]: Using local config');
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { Check } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* True when a parser problem describes something actually wrong with the file —
|
|
4
|
+
* a syntax error, a duplicate key, a tab used for indentation.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isFileDefect(problem: {
|
|
7
|
+
code?: string;
|
|
8
|
+
}): boolean;
|
|
2
9
|
/**
|
|
3
10
|
* Bundled YAML linter (the `yaml` package — no runner dependency, always
|
|
4
11
|
* available like Markdown/Spellcheck). Parses every changed `.yml`/`.yaml` file
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.yamlLintCheck = void 0;
|
|
7
|
+
exports.isFileDefect = isFileDefect;
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const yaml_1 = require("yaml");
|
|
@@ -15,6 +16,28 @@ const findings_1 = require("../findings");
|
|
|
15
16
|
function shortMessage(message) {
|
|
16
17
|
return message.split('\n')[0].trim();
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Parser problems that are not defects in the file.
|
|
21
|
+
*
|
|
22
|
+
* `TAG_RESOLVE_FAILED` means the parser met an application-specific tag it has no
|
|
23
|
+
* schema for. YAML is explicitly extensible there, so the document is valid and the
|
|
24
|
+
* parser simply is not the application that defines the tag: `!Ref`, `!Sub`,
|
|
25
|
+
* `!GetAtt`, `!Equals` in CloudFormation and SAM templates, `!include` in Home
|
|
26
|
+
* Assistant, `!vault` in Ansible. Reporting those calls a whole ecosystem's valid
|
|
27
|
+
* files broken — one CloudFormation template produced **557 blocking findings** for
|
|
28
|
+
* a client, none of which was a real problem.
|
|
29
|
+
*
|
|
30
|
+
* Matched on the parser's `code`, not on message text, so a wording change upstream
|
|
31
|
+
* cannot silently re-enable it.
|
|
32
|
+
*/
|
|
33
|
+
const NON_DEFECT_CODES = new Set(['TAG_RESOLVE_FAILED']);
|
|
34
|
+
/**
|
|
35
|
+
* True when a parser problem describes something actually wrong with the file —
|
|
36
|
+
* a syntax error, a duplicate key, a tab used for indentation.
|
|
37
|
+
*/
|
|
38
|
+
function isFileDefect(problem) {
|
|
39
|
+
return problem.code === undefined || !NON_DEFECT_CODES.has(problem.code);
|
|
40
|
+
}
|
|
18
41
|
/**
|
|
19
42
|
* Bundled YAML linter (the `yaml` package — no runner dependency, always
|
|
20
43
|
* available like Markdown/Spellcheck). Parses every changed `.yml`/`.yaml` file
|
|
@@ -60,7 +83,7 @@ exports.yamlLintCheck = {
|
|
|
60
83
|
// so a single malformed file cannot abort the whole check.
|
|
61
84
|
const docs = (0, yaml_1.parseAllDocuments)(source, { prettyErrors: true });
|
|
62
85
|
for (const doc of docs) {
|
|
63
|
-
for (const problem of [...doc.errors, ...doc.warnings]) {
|
|
86
|
+
for (const problem of [...doc.errors, ...doc.warnings].filter(isFileDefect)) {
|
|
64
87
|
const pos = problem.linePos?.[0];
|
|
65
88
|
issues.push({
|
|
66
89
|
file,
|
|
@@ -39,10 +39,30 @@ exports.securityCheck = {
|
|
|
39
39
|
// times out in locked-down/offline CI (the scanner already falls back to
|
|
40
40
|
// this exact version). Overridable via `security.gitleaks.version`.
|
|
41
41
|
const gitleaksVersion = ctx.config.security?.gitleaks?.version ?? '8.30.0';
|
|
42
|
-
|
|
42
|
+
// The scan scope has to follow the run's scope. `--diff-mode ci` REQUIRES
|
|
43
|
+
// HEAD_SHA and aborts without it, so on a full-scan run (no diff range — the
|
|
44
|
+
// local default) it exited 2 and the secret scan silently became a warning
|
|
45
|
+
// about the scanner instead of a review of the code. `history` is the only
|
|
46
|
+
// whole-repository mode the scanner offers; it walks the full commit history,
|
|
47
|
+
// which took ~1s over 700 commits here but is worth bounding on a large
|
|
48
|
+
// monorepo via `security.gitleaks.depth`.
|
|
49
|
+
const isDelta = Boolean(ctx.baseSha);
|
|
50
|
+
const depth = ctx.config.security?.gitleaks?.depth;
|
|
51
|
+
// History mode needs a repository to walk. Without one the scanner exits 2
|
|
52
|
+
// after printing `fatal: not a git repository` and "An unexpected error
|
|
53
|
+
// occurred" — alarming output about the tool, for a directory it was never
|
|
54
|
+
// able to review. Every other check skips when git is unusable; so does this.
|
|
55
|
+
if (!isDelta && !(await (0, utils_1.resolveGitRef)(ctx.cwd, 'HEAD'))) {
|
|
56
|
+
return (0, types_1.skip)('git history unavailable');
|
|
57
|
+
}
|
|
58
|
+
const args = isDelta
|
|
59
|
+
? ['--diff-mode', 'ci']
|
|
60
|
+
: ['--diff-mode', 'history', ...(depth ? ['--depth', String(depth)] : [])];
|
|
61
|
+
args.push('--gitleaks-version', gitleaksVersion);
|
|
43
62
|
if (!hasClientConfig) {
|
|
44
63
|
args.push('--config', (0, config_1.getPackageConfigPath)('gitleaks.toml'));
|
|
45
64
|
}
|
|
65
|
+
utils_1.logger.info(`[Security Scan]: scanning ${isDelta ? 'the diff range' : `full history${depth ? ` (last ${depth} commits)` : ''}`}`);
|
|
46
66
|
const env = {
|
|
47
67
|
...process.env,
|
|
48
68
|
...(ctx.headSha ? { HEAD_SHA: ctx.headSha } : {}),
|
package/dist/core/context.d.ts
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
import type { CheckContext, RunOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* What a run should look at, before any git call is made. Separated from
|
|
4
|
+
* {@link resolveDiffRange} so the precedence rules are a pure function of the
|
|
5
|
+
* options and the environment — the part worth pinning with tests.
|
|
6
|
+
*
|
|
7
|
+
* - `full` — every tracked file. Checks fall back to `git ls-files` when the range
|
|
8
|
+
* is undefined, so this is expressed by resolving to no SHAs at all.
|
|
9
|
+
* - `staged` — index vs HEAD (pre-commit).
|
|
10
|
+
* - `explicit` — SHAs the caller supplied.
|
|
11
|
+
* - `github-event` — base SHA from the pull_request event payload.
|
|
12
|
+
* - `ref` — diff against a named ref (a PR's destination branch, or `HEAD^`).
|
|
13
|
+
*/
|
|
14
|
+
type RangePlan = {
|
|
15
|
+
kind: 'full';
|
|
16
|
+
reason: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'staged';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'explicit';
|
|
21
|
+
baseSha?: string;
|
|
22
|
+
headSha?: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'github-event';
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'ref';
|
|
27
|
+
ref: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Decide what to look at. Order matters and is the contract:
|
|
32
|
+
*
|
|
33
|
+
* 1. `full` wins over everything — an explicit "check all of it" must not be
|
|
34
|
+
* quietly narrowed by a CI variable that happens to be set.
|
|
35
|
+
* 2. `staged`, then caller-supplied SHAs.
|
|
36
|
+
* 3. The CI platform's PR context (GitHub event payload, Bitbucket destination
|
|
37
|
+
* branch) — a gate exists to judge the diff.
|
|
38
|
+
* 4. A CI build with no PR context still diffs `HEAD^`: it is a push/branch build,
|
|
39
|
+
* and delta is why CI runs are fast.
|
|
40
|
+
* 5. Anything else is a human at a terminal → full scan. `HEAD^` used to be the
|
|
41
|
+
* fallback here, which meant a local `pr-checkmate all` judged only the last
|
|
42
|
+
* commit and could report a clean project with a `debugger` committed earlier.
|
|
43
|
+
*
|
|
44
|
+
* @param opts Run options as passed to `runChecks`.
|
|
45
|
+
* @param env Environment to read CI markers from. Defaults to the current process.
|
|
46
|
+
*/
|
|
47
|
+
export declare function planDiffRange(opts: RunOptions, env?: NodeJS.ProcessEnv): RangePlan;
|
|
2
48
|
/**
|
|
3
49
|
* Assemble the shared CheckContext once per run. Language detection is skipped
|
|
4
50
|
* when the caller supplies `languages` (e.g. PRValidator passes its ProjectType).
|
|
5
51
|
*/
|
|
6
52
|
export declare function buildContext(opts?: RunOptions): Promise<CheckContext>;
|
|
53
|
+
export {};
|
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,10 +34,22 @@ 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
|
|
@@ -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"
|
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,
|
package/src/config/cspell.json
CHANGED
|
@@ -903,6 +903,7 @@
|
|
|
903
903
|
"riskfolio",
|
|
904
904
|
"robolectric",
|
|
905
905
|
"rowwise",
|
|
906
|
+
"rseidelsohn",
|
|
906
907
|
"Rshum",
|
|
907
908
|
"rtol",
|
|
908
909
|
"rtype",
|
|
@@ -1010,6 +1011,7 @@
|
|
|
1010
1011
|
"taxinfo",
|
|
1011
1012
|
"Tayebeh",
|
|
1012
1013
|
"tcwf",
|
|
1014
|
+
"teamcity",
|
|
1013
1015
|
"telaviv",
|
|
1014
1016
|
"temurin",
|
|
1015
1017
|
"Tenés",
|
|
@@ -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,
|
|
@@ -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": {
|