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 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
- semi: ['error', 'always'],
74
- quotes: ['error', 'single', { avoidEscape: true }],
75
- 'object-curly-spacing': ['error', 'always'],
76
- 'space-infix-ops': 'error',
77
- 'keyword-spacing': ['error', { before: true, after: true }],
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
- 'error',
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
- 'error',
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. In a PR only the changed files are checked; a full-project scan is the
33
- fallback outside PR context.
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
- "**/node_modules/**",
1241
- "**/dist/**",
1242
- "**/.git/**",
1243
- "**/build/**",
1244
- "**/coverage/**",
1245
- "**/.next/**",
1246
- "**/.nuxt/**",
1247
- "**/out/**",
1248
- "**/__pycache__/**",
1249
- "**/.pytest_cache/**",
1250
- "**/.venv/**",
1251
- "**/venv/**",
1252
- "**/package-lock.json",
1253
- "**/yarn.lock",
1254
- "**/pnpm-lock.yaml",
1255
- "**/*.min.js",
1256
- "**/*.min.css"
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
- const args = process.argv.slice(2);
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;
@@ -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
- const from = ctx.baseSha ?? 'HEAD~1';
67
- const to = ctx.headSha ?? 'HEAD';
68
- const args = ['--from', from, '--to', to, '--verbose'];
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
- applies() {
57
- return true;
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 files = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.md', '*.mdx', '*.json'], (0, config_1.getSourcePaths)());
61
- if (files === null)
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 = [...patterns, '--no-progress'];
131
+ args = baseArgs;
72
132
  }
73
133
  else {
74
134
  tempConfigPath = createTempCspellConfig(ctx.cwd);
75
- args = [...patterns, '--config', tempConfigPath, '--no-progress'];
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
- const args = ['--diff-mode', 'ci', '--gitleaks-version', gitleaksVersion];
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 } : {}),
@@ -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 {};