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/.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",
|
|
@@ -1237,23 +1239,23 @@
|
|
|
1237
1239
|
"רטינול"
|
|
1238
1240
|
],
|
|
1239
1241
|
"ignorePaths": [
|
|
1240
|
-
"
|
|
1241
|
-
"
|
|
1242
|
-
"
|
|
1243
|
-
"
|
|
1244
|
-
"
|
|
1245
|
-
"
|
|
1246
|
-
"
|
|
1247
|
-
"
|
|
1248
|
-
"
|
|
1249
|
-
"
|
|
1250
|
-
"
|
|
1251
|
-
"
|
|
1252
|
-
"
|
|
1253
|
-
"
|
|
1254
|
-
"
|
|
1255
|
-
"
|
|
1256
|
-
"
|
|
1242
|
+
"/**/node_modules/**",
|
|
1243
|
+
"/**/dist/**",
|
|
1244
|
+
"/**/.git/**",
|
|
1245
|
+
"/**/build/**",
|
|
1246
|
+
"/**/coverage/**",
|
|
1247
|
+
"/**/.next/**",
|
|
1248
|
+
"/**/.nuxt/**",
|
|
1249
|
+
"/**/out/**",
|
|
1250
|
+
"/**/__pycache__/**",
|
|
1251
|
+
"/**/.pytest_cache/**",
|
|
1252
|
+
"/**/.venv/**",
|
|
1253
|
+
"/**/venv/**",
|
|
1254
|
+
"/**/package-lock.json",
|
|
1255
|
+
"/**/yarn.lock",
|
|
1256
|
+
"/**/pnpm-lock.yaml",
|
|
1257
|
+
"/**/*.min.js",
|
|
1258
|
+
"/**/*.min.css"
|
|
1257
1259
|
],
|
|
1258
1260
|
"enableFiletypes": ["typescript", "javascript", "markdown", "json"],
|
|
1259
1261
|
"ignoreRegExpList": ["/https?:\\/\\/[^\\s]+/g", "/\\b[0-9a-f]{7,40}\\b/gi"]
|
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,2 +1,27 @@
|
|
|
1
|
+
import { CspellConfig } from '../../../interfaces';
|
|
1
2
|
import { Check } from '../../types';
|
|
3
|
+
/**
|
|
4
|
+
* Anchors an ignore glob to the repository root.
|
|
5
|
+
*
|
|
6
|
+
* cspell matches `ignorePaths` against each file's ABSOLUTE path, so an unanchored
|
|
7
|
+
* `**\/build/**` also matches every ancestor directory of the workspace. On Bitbucket
|
|
8
|
+
* Pipelines the clone lives in `/opt/atlassian/pipelines/agent/build`, so that one
|
|
9
|
+
* pattern excluded the entire repository and the check silently spellchecked nothing.
|
|
10
|
+
* A leading `/` anchors the pattern to the config's directory (the repo root) while
|
|
11
|
+
* `/**\/dir/**` still matches at any depth INSIDE the repo — verified against cspell 10.
|
|
12
|
+
*
|
|
13
|
+
* Patterns that are already anchored (`/…`) or plainly relative (`dir/**`,
|
|
14
|
+
* `CHANGELOG.md`) are left untouched: they cannot match an ancestor.
|
|
15
|
+
*/
|
|
16
|
+
export declare function anchorIgnoreGlob(pattern: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Folds the client's `cspell` block from pr-checkmate.json into the bundled
|
|
19
|
+
* defaults. `enabled` is deliberately not carried over — it gates the check
|
|
20
|
+
* itself (see `applies`) and is not a cspell setting.
|
|
21
|
+
*
|
|
22
|
+
* Lists are appended so a client extends the curated vocabulary instead of
|
|
23
|
+
* replacing it; `language` is the one exception and replaces the default, because
|
|
24
|
+
* a client writing `"en"` means exactly that and appending would be meaningless.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mergeCspellConfig(defaultConfig: CspellConfig, userConfig: Partial<CspellConfig>): CspellConfig;
|
|
2
27
|
export declare const spellcheckCheck: Check;
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.spellcheckCheck = void 0;
|
|
7
|
+
exports.anchorIgnoreGlob = anchorIgnoreGlob;
|
|
8
|
+
exports.mergeCspellConfig = mergeCspellConfig;
|
|
7
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
11
|
const config_1 = require("../../../config");
|
|
@@ -23,6 +25,47 @@ function loadUserCspellConfig(cwd) {
|
|
|
23
25
|
return {};
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Anchors an ignore glob to the repository root.
|
|
30
|
+
*
|
|
31
|
+
* cspell matches `ignorePaths` against each file's ABSOLUTE path, so an unanchored
|
|
32
|
+
* `**\/build/**` also matches every ancestor directory of the workspace. On Bitbucket
|
|
33
|
+
* Pipelines the clone lives in `/opt/atlassian/pipelines/agent/build`, so that one
|
|
34
|
+
* pattern excluded the entire repository and the check silently spellchecked nothing.
|
|
35
|
+
* A leading `/` anchors the pattern to the config's directory (the repo root) while
|
|
36
|
+
* `/**\/dir/**` still matches at any depth INSIDE the repo — verified against cspell 10.
|
|
37
|
+
*
|
|
38
|
+
* Patterns that are already anchored (`/…`) or plainly relative (`dir/**`,
|
|
39
|
+
* `CHANGELOG.md`) are left untouched: they cannot match an ancestor.
|
|
40
|
+
*/
|
|
41
|
+
function anchorIgnoreGlob(pattern) {
|
|
42
|
+
return pattern.startsWith('**/') ? `/${pattern}` : pattern;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Folds the client's `cspell` block from pr-checkmate.json into the bundled
|
|
46
|
+
* defaults. `enabled` is deliberately not carried over — it gates the check
|
|
47
|
+
* itself (see `applies`) and is not a cspell setting.
|
|
48
|
+
*
|
|
49
|
+
* Lists are appended so a client extends the curated vocabulary instead of
|
|
50
|
+
* replacing it; `language` is the one exception and replaces the default, because
|
|
51
|
+
* a client writing `"en"` means exactly that and appending would be meaningless.
|
|
52
|
+
*/
|
|
53
|
+
function mergeCspellConfig(defaultConfig, userConfig) {
|
|
54
|
+
const dictionaries = [...(defaultConfig.dictionaries ?? []), ...(userConfig.dictionaries ?? [])];
|
|
55
|
+
return {
|
|
56
|
+
...defaultConfig,
|
|
57
|
+
language: userConfig.language || defaultConfig.language,
|
|
58
|
+
words: [...defaultConfig.words, ...(userConfig.words || [])],
|
|
59
|
+
// Client globs are anchored too — a repo's own pattern is meant to be relative
|
|
60
|
+
// to that repo, never to whatever directories sit above the checkout.
|
|
61
|
+
ignorePaths: [...defaultConfig.ignorePaths, ...(userConfig.ignorePaths || [])].map(anchorIgnoreGlob),
|
|
62
|
+
ignoreRegExpList: [...defaultConfig.ignoreRegExpList, ...(userConfig.ignoreRegExpList || [])],
|
|
63
|
+
// The bundled dictionaries arrive through `import`, so there is nothing to
|
|
64
|
+
// override here. cspell ignores names it cannot resolve, so a client typo
|
|
65
|
+
// costs coverage but never fails the run.
|
|
66
|
+
...(dictionaries.length > 0 ? { dictionaries } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
26
69
|
function createTempCspellConfig(cwd) {
|
|
27
70
|
const packageConfigPath = (0, config_1.getPackageConfigPath)('cspell.json');
|
|
28
71
|
const packageConfigDir = node_path_1.default.dirname(packageConfigPath);
|
|
@@ -40,11 +83,8 @@ function createTempCspellConfig(cwd) {
|
|
|
40
83
|
}
|
|
41
84
|
});
|
|
42
85
|
const mergedConfig = {
|
|
43
|
-
...defaultConfig,
|
|
86
|
+
...mergeCspellConfig(defaultConfig, userConfig),
|
|
44
87
|
import: resolvedImports,
|
|
45
|
-
words: [...defaultConfig.words, ...(userConfig.words || [])],
|
|
46
|
-
ignorePaths: [...defaultConfig.ignorePaths, ...(userConfig.ignorePaths || [])],
|
|
47
|
-
ignoreRegExpList: [...defaultConfig.ignoreRegExpList, ...(userConfig.ignoreRegExpList || [])],
|
|
48
88
|
};
|
|
49
89
|
const tempConfigPath = node_path_1.default.join(cwd, '.cspell.temp.json');
|
|
50
90
|
node_fs_1.default.writeFileSync(tempConfigPath, JSON.stringify(mergedConfig, null, 2));
|
|
@@ -53,26 +93,46 @@ function createTempCspellConfig(cwd) {
|
|
|
53
93
|
exports.spellcheckCheck = {
|
|
54
94
|
name: 'Spellcheck',
|
|
55
95
|
phase: 'informational',
|
|
56
|
-
|
|
57
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Clients turn the check off with `cspell.enabled: false` (or the `spellcheck`
|
|
98
|
+
* alias the config interface uses). The universal `severity: { Spellcheck: "off" }`
|
|
99
|
+
* switch keeps working too — this is the per-check flag every other check exposes.
|
|
100
|
+
*/
|
|
101
|
+
applies(ctx) {
|
|
102
|
+
return ctx.config.cspell?.enabled !== false && ctx.config.spellcheck?.enabled !== false;
|
|
58
103
|
},
|
|
59
104
|
async run(ctx) {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
105
|
+
const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.md', '*.mdx', '*.json'], (0, config_1.getSourcePaths)());
|
|
106
|
+
if (targets === null)
|
|
62
107
|
return (0, types_1.skip)('git unavailable');
|
|
108
|
+
// Committed build output and test reports are dropped here, in JS, against
|
|
109
|
+
// repo-RELATIVE paths — a client cannot fix a typo in a generated file, so
|
|
110
|
+
// reporting one is noise that buries the real findings. Neither cspell
|
|
111
|
+
// mechanism can do this safely: `--exclude` ignores glob anchoring outright
|
|
112
|
+
// (`--exclude=/**/build/**` still matches an ancestor `build` dir, which on
|
|
113
|
+
// Bitbucket Pipelines is the checkout itself), and `ignorePaths` only reaches
|
|
114
|
+
// repos that have no cspell config of their own. Filtering the list keeps one
|
|
115
|
+
// rule for every repo. `ignoreDirs` already covers plain directory names via
|
|
116
|
+
// `resolveTargetFiles`; this adds the composite globs it cannot express
|
|
117
|
+
// (`**/cypress/videos/**`, `**/jest-html-reporters-*/**`).
|
|
118
|
+
const files = targets.filter(f => !(0, utils_1.matchesAnyGlob)(f, config_1.IGNORE_PATTERNS));
|
|
63
119
|
if (files.length === 0)
|
|
64
120
|
return (0, types_1.pass)('no files to spellcheck');
|
|
65
|
-
const patterns = [...files, ...config_1.IGNORE_PATTERNS.map(p => `--exclude=${p}`)];
|
|
66
121
|
const hasLocal = (0, config_1.hasLocalConfig)(['cspell.json', '.cspell.json', 'cspell.config.json']);
|
|
67
122
|
let tempConfigPath = null;
|
|
68
123
|
const cspellBin = (0, utils_1.resolvePackageBin)('cspell', 'bin.mjs');
|
|
124
|
+
// `--no-must-find-files`: cspell exits 1 when it checked nothing, which the
|
|
125
|
+
// exit-code mapping below would report as "spelling issues found" — a warning
|
|
126
|
+
// about problems that do not exist. Every file being excluded is a pass, not a
|
|
127
|
+
// finding.
|
|
128
|
+
const baseArgs = [...files, '--no-progress', '--no-must-find-files'];
|
|
69
129
|
let args;
|
|
70
130
|
if (hasLocal) {
|
|
71
|
-
args =
|
|
131
|
+
args = baseArgs;
|
|
72
132
|
}
|
|
73
133
|
else {
|
|
74
134
|
tempConfigPath = createTempCspellConfig(ctx.cwd);
|
|
75
|
-
args = [...
|
|
135
|
+
args = [...baseArgs, '--config', tempConfigPath];
|
|
76
136
|
}
|
|
77
137
|
try {
|
|
78
138
|
const result = await (0, execa_1.execa)(process.execPath, [cspellBin, ...args], {
|
|
@@ -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 {};
|