pr-checkmate 1.23.22 → 1.23.24

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
@@ -93,19 +93,9 @@ module.exports = {
93
93
  },
94
94
  ],
95
95
  'import-x/no-duplicates': 'error',
96
- // Documentation (#62)
97
- 'jsdoc/require-jsdoc': [
98
- 'warn',
99
- {
100
- publicOnly: true,
101
- require: {
102
- FunctionDeclaration: true,
103
- MethodDefinition: true,
104
- ClassDeclaration: true,
105
- ArrowFunctionExpression: false,
106
- },
107
- },
108
- ],
96
+ // Documentation (#62). `jsdoc/require-jsdoc` is deliberately absent — see the
97
+ // note in eslint-flat-config.mjs. These three check docblocks that exist and
98
+ // fix only inside them; they never fabricate one.
109
99
  'jsdoc/check-param-names': 'warn',
110
100
  'jsdoc/check-tag-names': 'warn',
111
101
  'jsdoc/check-types': 'warn',
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  > PR CheckMate is a security-first PR gate: secret scanning, dependency
15
15
  > vulnerability checks, and a signed SBOM + independent VirusTotal scan on
16
- > every release — plus 51 code-quality checks across 11 languages bundled in
16
+ > every release — plus 53 code-quality checks across 12 languages bundled in
17
17
  > for free, running in CI on every pull request. The bundled languages need no
18
18
  > per-language toolchain setup.
19
19
 
@@ -82,6 +82,7 @@ tool installed; the check skips gracefully when the tool is absent.
82
82
  | C# | `dotnet format` | .NET SDK on the runner |
83
83
  | Ruby | RuboCop | `rubocop` on the runner |
84
84
  | PHP | PHP-CS-Fixer | `php-cs-fixer` on the runner |
85
+ | Shell | ShellCheck | `shellcheck` on the runner |
85
86
 
86
87
  Secret scanning (gitleaks) and the universal git-diff checks are bundled and run for
87
88
  every language.
@@ -181,12 +182,13 @@ For a runner-dependency language, add its toolchain setup step before the run: G
181
182
  `actions/setup-go`, Rust with `dtolnay/rust-toolchain`, C# with `actions/setup-dotnet`,
182
183
  Ruby with `ruby/setup-ruby` (plus `gem install rubocop`), PHP with
183
184
  `shivammathur/setup-php` (tools: `php-cs-fixer`), Kotlin by fetching the `ktlint`
184
- binary. Swift's `swiftlint` is pre-installed on `macos-latest`. Anything missing is
185
+ binary. Swift's `swiftlint` is pre-installed on `macos-latest`, and `shellcheck` is
186
+ pre-installed on `ubuntu-latest` — neither needs a setup step. Anything missing is
185
187
  skipped, not failed.
186
188
 
187
189
  ## Checks
188
190
 
189
- 51 checks, grouped by category and run in parallel within each phase:
191
+ 53 checks, grouped by category and run in parallel within each phase:
190
192
 
191
193
  - Universal git-diff (18) — language-agnostic and bundled: merge-conflict markers,
192
194
  secret scan, diff security (JS/TS/Python/Go/Kotlin), diff smells, GitHub Actions and
@@ -196,9 +198,10 @@ skipped, not failed.
196
198
  - Dependencies (7) — unused/missing, circular, outdated, license compliance, npm audit,
197
199
  a cross-language vuln scan (osv-scanner, opt-in), and a direct-dependency vuln scan
198
200
  (grype, on by default).
199
- - Quality (6) — duplicate code, dead code, multi-language spellcheck, markdown lint, YAML lint, config validation.
201
+ - Quality (7) — duplicate code, dead code, multi-language spellcheck, markdown lint,
202
+ YAML lint, broken symlinks, config validation.
200
203
  - PR hygiene (4) — size, title, body, and commit-message convention.
201
- - Per-language (16) — lint, format, and type-check for the languages listed above.
204
+ - Per-language (17) — lint, format, and type-check for the languages listed above.
202
205
 
203
206
  Each check has its own page in the
204
207
  [per-check reference](https://github.com/Mybono/pr_checkmate_public/blob/main/documentation/checks/INDEX.md),
@@ -269,4 +272,4 @@ affect it.
269
272
 
270
273
  LicenseRef Proprietary © 2025-2026 Artur Polishchuk. Free to install and run via the
271
274
  published npm package (see [LICENSE](LICENSE) for the exact grant); the source is not
272
- open-source and is not licensed for copying, modification, or redistribution.
275
+ open-source and is not licensed for copying, modification, or redistribution.
package/cspell.json CHANGED
@@ -422,6 +422,7 @@
422
422
  "distroless",
423
423
  "dlib",
424
424
  "Doar",
425
+ "docblocks",
425
426
  "Doch",
426
427
  "dockerfiles",
427
428
  "dockerization",
package/dist/cli.js CHANGED
@@ -20,6 +20,8 @@ const COMMAND_TO_CHECK = {
20
20
  outdated: 'Outdated Deps',
21
21
  markdown: 'Markdown',
22
22
  'yaml-lint': 'YAML Lint',
23
+ symlinks: 'Broken Symlinks',
24
+ shellcheck: 'ShellCheck',
23
25
  typecheck: 'TypeScript',
24
26
  'diff-smell': 'Diff Smells',
25
27
  'diff-security': 'Diff Security',
@@ -0,0 +1,8 @@
1
+ import { Check } from '../../types';
2
+ /**
3
+ * Lints changed shell scripts with ShellCheck. Runner-dependency: needs the
4
+ * `shellcheck` binary on the runner (like Go Vet/Clippy/RuboCop), gracefully
5
+ * skipping when it's absent. `-f gcc` gives one line per finding, ending in
6
+ * `[SCxxxx]`, which both a human and the count below can read directly.
7
+ */
8
+ export declare const shellcheckCheck: Check;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shellcheckCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const execa_1 = require("../../../utils/execa");
6
+ const targets_1 = require("../../targets");
7
+ const types_1 = require("../../types");
8
+ const SHELL_GLOBS = ['*.sh', '*.bash', '*.ksh'];
9
+ /**
10
+ * Lints changed shell scripts with ShellCheck. Runner-dependency: needs the
11
+ * `shellcheck` binary on the runner (like Go Vet/Clippy/RuboCop), gracefully
12
+ * skipping when it's absent. `-f gcc` gives one line per finding, ending in
13
+ * `[SCxxxx]`, which both a human and the count below can read directly.
14
+ */
15
+ exports.shellcheckCheck = {
16
+ name: 'ShellCheck',
17
+ phase: 'informational',
18
+ applies(ctx) {
19
+ return ctx.config.shellcheck?.enabled !== false;
20
+ },
21
+ async run(ctx) {
22
+ const targets = await (0, targets_1.resolveTargetFiles)(ctx, SHELL_GLOBS);
23
+ if (targets === null)
24
+ return (0, types_1.skip)('git unavailable');
25
+ if (targets.length === 0)
26
+ return (0, types_1.pass)('no shell scripts changed');
27
+ const minSeverity = ctx.config.shellcheck?.minSeverity;
28
+ const ignore = ctx.config.shellcheck?.ignore ?? [];
29
+ const args = ['-f', 'gcc'];
30
+ if (minSeverity)
31
+ args.push('-S', minSeverity);
32
+ if (ignore.length > 0)
33
+ args.push('-e', ignore.join(','));
34
+ args.push(...targets);
35
+ const result = await (0, execa_1.execa)('shellcheck', args, { cwd: ctx.cwd, reject: false });
36
+ // execa v9 with reject:false does NOT throw on a missing binary — it resolves
37
+ // with `code: 'ENOENT'`. Detect that on the result, not via try/catch.
38
+ if (result.code === 'ENOENT')
39
+ return (0, types_1.skip)('shellcheck not installed');
40
+ if (result.exitCode === 0) {
41
+ utils_1.logger.info('[ShellCheck]: ✅ No issues found');
42
+ return (0, types_1.pass)();
43
+ }
44
+ const lines = [result.stdout, result.stderr]
45
+ .filter(Boolean)
46
+ .join('\n')
47
+ .split('\n')
48
+ .filter(Boolean);
49
+ utils_1.logger.warn(`[ShellCheck]: ⚠️ ${lines.length} issue(s):`);
50
+ lines.slice(0, 10).forEach(l => utils_1.logger.warn(` ${l}`));
51
+ if (lines.length > 10)
52
+ utils_1.logger.warn(` ... and ${lines.length - 10} more`);
53
+ return (0, types_1.warn)(`${lines.length} issue(s) in ${targets.length} file(s)`);
54
+ },
55
+ };
@@ -0,0 +1,16 @@
1
+ import { Check } from '../../types';
2
+ /** True when `fullPath` is a symlink whose target does not exist. */
3
+ export declare function isBrokenSymlink(fullPath: string): boolean;
4
+ /**
5
+ * Flags tracked symlinks whose target does not exist. `fs.existsSync` follows a
6
+ * symlink to its target, so it returns `false` for exactly the broken case while
7
+ * the link itself is still a real, tracked file — that's what makes a plain
8
+ * existence check (as used by `resolveTargetFiles`) unusable here and requires
9
+ * the dedicated `isBrokenSymlink` pass over `fs.lstatSync` above.
10
+ *
11
+ * Whole-repo scan (not delta), like Case Collision: a broken link fails a
12
+ * checkout or build regardless of which PR introduced it, so it's worth
13
+ * catching even when this run's diff never touched it. Informational and warns
14
+ * by default — promote to a gate per repo with `symlinks.severity`.
15
+ */
16
+ export declare const symlinksCheck: Check;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.symlinksCheck = void 0;
7
+ exports.isBrokenSymlink = isBrokenSymlink;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const config_1 = require("../../../config");
11
+ const utils_1 = require("../../../utils");
12
+ const targets_1 = require("../../targets");
13
+ const types_1 = require("../../types");
14
+ const findings_1 = require("../findings");
15
+ /** True when `fullPath` is a symlink whose target does not exist. */
16
+ function isBrokenSymlink(fullPath) {
17
+ let stat;
18
+ try {
19
+ stat = node_fs_1.default.lstatSync(fullPath);
20
+ }
21
+ catch {
22
+ return false; // gone between listing and stat
23
+ }
24
+ return stat.isSymbolicLink() && !node_fs_1.default.existsSync(fullPath);
25
+ }
26
+ /**
27
+ * Flags tracked symlinks whose target does not exist. `fs.existsSync` follows a
28
+ * symlink to its target, so it returns `false` for exactly the broken case while
29
+ * the link itself is still a real, tracked file — that's what makes a plain
30
+ * existence check (as used by `resolveTargetFiles`) unusable here and requires
31
+ * the dedicated `isBrokenSymlink` pass over `fs.lstatSync` above.
32
+ *
33
+ * Whole-repo scan (not delta), like Case Collision: a broken link fails a
34
+ * checkout or build regardless of which PR introduced it, so it's worth
35
+ * catching even when this run's diff never touched it. Informational and warns
36
+ * by default — promote to a gate per repo with `symlinks.severity`.
37
+ */
38
+ exports.symlinksCheck = {
39
+ name: 'Broken Symlinks',
40
+ phase: 'informational',
41
+ applies(ctx) {
42
+ return ctx.config.symlinks?.enabled !== false;
43
+ },
44
+ async run(ctx) {
45
+ const files = await (0, targets_1.listTrackedFiles)(ctx, ['*'], (0, config_1.getSourcePaths)());
46
+ if (files.length === 0)
47
+ return (0, types_1.skip)('no tracked files');
48
+ const ignore = ctx.config.symlinks?.ignore ?? [];
49
+ const broken = [];
50
+ for (const file of files) {
51
+ if (ignore.length > 0 && (0, utils_1.matchesAnyGlob)(file, ignore))
52
+ continue;
53
+ if (isBrokenSymlink(node_path_1.default.join(ctx.cwd, file)))
54
+ broken.push(file);
55
+ }
56
+ if (broken.length === 0)
57
+ return (0, types_1.pass)();
58
+ const sev = (0, findings_1.severityEmitter)(ctx.config.symlinks?.severity, 'warn');
59
+ broken.forEach(f => sev.log(`[Broken Symlinks]: ${sev.icon} ${f} — target does not exist`));
60
+ return sev.emit(`${broken.length} broken symlink(s)`);
61
+ },
62
+ };
@@ -31,6 +31,7 @@ const python_typecheck_1 = require("./checks/languages/python-typecheck");
31
31
  const ruby_lint_1 = require("./checks/languages/ruby-lint");
32
32
  const rust_format_1 = require("./checks/languages/rust-format");
33
33
  const rust_lint_1 = require("./checks/languages/rust-lint");
34
+ const shellcheck_1 = require("./checks/languages/shellcheck");
34
35
  const swift_lint_1 = require("./checks/languages/swift-lint");
35
36
  const typecheck_1 = require("./checks/languages/typecheck");
36
37
  const commitlint_1 = require("./checks/pr/commitlint");
@@ -45,6 +46,7 @@ const duplicate_check_1 = require("./checks/quality/duplicate-check");
45
46
  const license_header_1 = require("./checks/quality/license-header");
46
47
  const markdown_lint_1 = require("./checks/quality/markdown-lint");
47
48
  const spellcheck_1 = require("./checks/quality/spellcheck");
49
+ const symlinks_1 = require("./checks/quality/symlinks");
48
50
  const yaml_lint_1 = require("./checks/quality/yaml-lint");
49
51
  const diff_security_1 = require("./checks/security/diff-security");
50
52
  const dockerfile_security_1 = require("./checks/security/dockerfile-security");
@@ -95,6 +97,7 @@ exports.REGISTRY = [
95
97
  markdown_lint_1.markdownLintCheck,
96
98
  spellcheck_1.spellcheckCheck,
97
99
  yaml_lint_1.yamlLintCheck,
100
+ symlinks_1.symlinksCheck,
98
101
  python_lint_1.pythonLintCheck,
99
102
  python_typecheck_1.pythonTypecheckCheck,
100
103
  dead_code_1.deadCodeCheck,
@@ -103,6 +106,7 @@ exports.REGISTRY = [
103
106
  go_lint_1.goLintCheck,
104
107
  rust_lint_1.rustLintCheck,
105
108
  ruby_lint_1.rubyLintCheck,
109
+ shellcheck_1.shellcheckCheck,
106
110
  // format
107
111
  prettier_1.prettierCheck,
108
112
  cpp_format_1.cppFormatCheck,
@@ -211,6 +211,16 @@ export interface PRCheckMateConfig {
211
211
  configValidation?: {
212
212
  enabled?: boolean;
213
213
  };
214
+ symlinks?: {
215
+ enabled?: boolean;
216
+ severity?: 'error' | 'warn';
217
+ ignore?: string[];
218
+ };
219
+ shellcheck?: {
220
+ enabled?: boolean;
221
+ minSeverity?: 'error' | 'warning' | 'info' | 'style';
222
+ ignore?: string[];
223
+ };
214
224
  yamlLint?: {
215
225
  enabled?: boolean;
216
226
  severity?: 'error' | 'warn';
@@ -34,18 +34,6 @@
34
34
  }
35
35
  ],
36
36
  "import/no-duplicates": "error",
37
- "jsdoc/require-jsdoc": [
38
- "warn",
39
- {
40
- "publicOnly": true,
41
- "require": {
42
- "FunctionDeclaration": true,
43
- "MethodDefinition": true,
44
- "ClassDeclaration": true,
45
- "ArrowFunctionExpression": false
46
- }
47
- }
48
- ],
49
37
  "jsdoc/check-param-names": "warn",
50
38
  "jsdoc/check-tag-names": "warn",
51
39
  "jsdoc/check-types": "warn",
package/eslint.config.mjs CHANGED
@@ -26,6 +26,28 @@ import jsdocPlugin from 'eslint-plugin-jsdoc';
26
26
  * `eslint.rules: { "prettier/prettier": "error" }` or
27
27
  * `severity: { "Prettier": "error" }` in pr-checkmate.json.
28
28
  *
29
+ * FIXERS THAT FABRICATE CONTENT ARE NOT SHIPPED. `jsdoc/require-jsdoc` used to be in
30
+ * this set at `warn`, and it is auto-fixable — but unlike `prettier/prettier`, whose
31
+ * fixer produces the right answer, this one has no answer to produce. It writes an
32
+ * empty stub:
33
+ *
34
+ * \/**
35
+ * *
36
+ * *\/
37
+ *
38
+ * which satisfies the rule permanently. The check runs in write mode by default
39
+ * outside CI, so one run stamped 150 stubs across one client's `src` and reported zero
40
+ * warnings; inside CI the auto-format commit pushes them back to the branch, so
41
+ * deleting them by hand never holds. The rule therefore reported a real gap once, and
42
+ * from then on guaranteed that every public method looked documented while none of it
43
+ * was — the signal destroyed by the same run that raised it.
44
+ *
45
+ * Whether a method is worth explaining is a judgement a fixer cannot make. The three
46
+ * `jsdoc/check-*` rules stay: they validate docblocks that already exist and fix only
47
+ * within them, so they never invent content. Documentation coverage, if a repo wants
48
+ * to enforce it, belongs in review — or in `eslint.rules` of its own pr-checkmate.json,
49
+ * where whoever turns it on knows what the fixer will do.
50
+ *
29
51
  * A JSON file cannot carry this note, hence it lives here; `src/config/.eslintrc.js`
30
52
  * holds the same rule set for the legacy ESLint <9 path and must stay in sync.
31
53
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-checkmate",
3
- "version": "1.23.22",
3
+ "version": "1.23.24",
4
4
  "description": "Security-first PR gate for CI: secret scanning, grype dependency scanning, SBOM — plus linting and formatting across 11 languages",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -120,8 +120,8 @@
120
120
  "tar": "7.5.22",
121
121
  "axios": "1.18.1",
122
122
  "form-data": "4.0.6",
123
- "brace-expansion": "5.0.8",
124
- "fast-uri": "4.1.1",
123
+ "brace-expansion": "5.0.9",
124
+ "fast-uri": "4.1.2",
125
125
  "follow-redirects": "1.16.0",
126
126
  "lodash": "4.18.1",
127
127
  "minimatch": "10.2.5",
@@ -174,6 +174,22 @@
174
174
  "ignore": { "type": "array", "items": { "type": "string" } }
175
175
  }
176
176
  },
177
+ "symlinks": {
178
+ "type": "object",
179
+ "properties": {
180
+ "enabled": { "type": "boolean" },
181
+ "severity": { "enum": ["error", "warn"] },
182
+ "ignore": { "type": "array", "items": { "type": "string" } }
183
+ }
184
+ },
185
+ "shellcheck": {
186
+ "type": "object",
187
+ "properties": {
188
+ "enabled": { "type": "boolean" },
189
+ "minSeverity": { "enum": ["error", "warning", "info", "style"] },
190
+ "ignore": { "type": "array", "items": { "type": "string" } }
191
+ }
192
+ },
177
193
  "circularDeps": {
178
194
  "type": "object",
179
195
  "properties": {
@@ -93,19 +93,9 @@ module.exports = {
93
93
  },
94
94
  ],
95
95
  'import-x/no-duplicates': 'error',
96
- // Documentation (#62)
97
- 'jsdoc/require-jsdoc': [
98
- 'warn',
99
- {
100
- publicOnly: true,
101
- require: {
102
- FunctionDeclaration: true,
103
- MethodDefinition: true,
104
- ClassDeclaration: true,
105
- ArrowFunctionExpression: false,
106
- },
107
- },
108
- ],
96
+ // Documentation (#62). `jsdoc/require-jsdoc` is deliberately absent — see the
97
+ // note in eslint-flat-config.mjs. These three check docblocks that exist and
98
+ // fix only inside them; they never fabricate one.
109
99
  'jsdoc/check-param-names': 'warn',
110
100
  'jsdoc/check-tag-names': 'warn',
111
101
  'jsdoc/check-types': 'warn',
@@ -422,6 +422,7 @@
422
422
  "distroless",
423
423
  "dlib",
424
424
  "Doar",
425
+ "docblocks",
425
426
  "Doch",
426
427
  "dockerfiles",
427
428
  "dockerization",
@@ -34,18 +34,6 @@
34
34
  }
35
35
  ],
36
36
  "import/no-duplicates": "error",
37
- "jsdoc/require-jsdoc": [
38
- "warn",
39
- {
40
- "publicOnly": true,
41
- "require": {
42
- "FunctionDeclaration": true,
43
- "MethodDefinition": true,
44
- "ClassDeclaration": true,
45
- "ArrowFunctionExpression": false
46
- }
47
- }
48
- ],
49
37
  "jsdoc/check-param-names": "warn",
50
38
  "jsdoc/check-tag-names": "warn",
51
39
  "jsdoc/check-types": "warn",
@@ -174,6 +174,22 @@
174
174
  "ignore": { "type": "array", "items": { "type": "string" } }
175
175
  }
176
176
  },
177
+ "symlinks": {
178
+ "type": "object",
179
+ "properties": {
180
+ "enabled": { "type": "boolean" },
181
+ "severity": { "enum": ["error", "warn"] },
182
+ "ignore": { "type": "array", "items": { "type": "string" } }
183
+ }
184
+ },
185
+ "shellcheck": {
186
+ "type": "object",
187
+ "properties": {
188
+ "enabled": { "type": "boolean" },
189
+ "minSeverity": { "enum": ["error", "warning", "info", "style"] },
190
+ "ignore": { "type": "array", "items": { "type": "string" } }
191
+ }
192
+ },
177
193
  "circularDeps": {
178
194
  "type": "object",
179
195
  "properties": {