pr-checkmate 1.22.7 → 1.23.0

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.
Files changed (140) hide show
  1. package/README.md +101 -333
  2. package/cspell.json +9 -1
  3. package/dist/cli.js +32 -2
  4. package/dist/config/constants.d.ts +25 -1
  5. package/dist/config/constants.js +157 -38
  6. package/dist/config/index.d.ts +15 -2
  7. package/dist/config/index.js +28 -7
  8. package/dist/config/loader.js +5 -1
  9. package/dist/core/checks/dependencies/banned-imports.d.ts +2 -0
  10. package/dist/core/checks/dependencies/banned-imports.js +71 -0
  11. package/dist/core/checks/{circular-deps.d.ts → dependencies/circular-deps.d.ts} +1 -1
  12. package/dist/core/checks/{circular-deps.js → dependencies/circular-deps.js} +9 -10
  13. package/dist/core/checks/{dependency-check.d.ts → dependencies/dependency-check.d.ts} +1 -1
  14. package/dist/core/checks/{dependency-check.js → dependencies/dependency-check.js} +3 -3
  15. package/dist/core/checks/{license-check.d.ts → dependencies/license-check.d.ts} +1 -1
  16. package/dist/core/checks/{license-check.js → dependencies/license-check.js} +3 -3
  17. package/dist/core/checks/{npm-audit.d.ts → dependencies/npm-audit.d.ts} +1 -1
  18. package/dist/core/checks/{npm-audit.js → dependencies/npm-audit.js} +3 -3
  19. package/dist/core/checks/{outdated-deps.d.ts → dependencies/outdated-deps.d.ts} +1 -1
  20. package/dist/core/checks/{outdated-deps.js → dependencies/outdated-deps.js} +3 -3
  21. package/dist/core/checks/dependencies/vuln-scan.d.ts +9 -0
  22. package/dist/core/checks/dependencies/vuln-scan.js +41 -0
  23. package/dist/core/checks/findings.d.ts +37 -1
  24. package/dist/core/checks/findings.js +80 -3
  25. package/dist/core/checks/git/custom-rules.d.ts +2 -0
  26. package/dist/core/checks/git/custom-rules.js +102 -0
  27. package/dist/core/checks/{diff-smell.d.ts → git/diff-smell.d.ts} +1 -1
  28. package/dist/core/checks/{diff-smell.js → git/diff-smell.js} +3 -3
  29. package/dist/core/checks/git/large-files.d.ts +2 -0
  30. package/dist/core/checks/git/large-files.js +83 -0
  31. package/dist/core/checks/git/leftover-debug.d.ts +2 -0
  32. package/dist/core/checks/git/leftover-debug.js +77 -0
  33. package/dist/core/checks/git/lockfile-drift.d.ts +2 -0
  34. package/dist/core/checks/git/lockfile-drift.js +114 -0
  35. package/dist/core/checks/git/merge-conflict.d.ts +2 -0
  36. package/dist/core/checks/git/merge-conflict.js +109 -0
  37. package/dist/core/checks/git/missing-tests.d.ts +2 -0
  38. package/dist/core/checks/git/missing-tests.js +92 -0
  39. package/dist/core/checks/git/todo-fixme.d.ts +7 -0
  40. package/dist/core/checks/git/todo-fixme.js +74 -0
  41. package/dist/core/checks/{cpp-format.d.ts → languages/cpp-format.d.ts} +1 -1
  42. package/dist/core/checks/{cpp-format.js → languages/cpp-format.js} +3 -3
  43. package/dist/core/checks/languages/csharp-format.d.ts +13 -0
  44. package/dist/core/checks/languages/csharp-format.js +52 -0
  45. package/dist/core/checks/languages/go-format.d.ts +8 -0
  46. package/dist/core/checks/languages/go-format.js +48 -0
  47. package/dist/core/checks/languages/go-lint.d.ts +8 -0
  48. package/dist/core/checks/languages/go-lint.js +48 -0
  49. package/dist/core/checks/{kotlin-lint.d.ts → languages/kotlin-lint.d.ts} +1 -1
  50. package/dist/core/checks/{kotlin-lint.js → languages/kotlin-lint.js} +13 -17
  51. package/dist/core/checks/{lint.d.ts → languages/lint.d.ts} +1 -1
  52. package/dist/core/checks/{lint.js → languages/lint.js} +25 -14
  53. package/dist/core/checks/languages/php-format.d.ts +9 -0
  54. package/dist/core/checks/languages/php-format.js +49 -0
  55. package/dist/core/checks/{prettier.d.ts → languages/prettier.d.ts} +1 -1
  56. package/dist/core/checks/{prettier.js → languages/prettier.js} +10 -10
  57. package/dist/core/checks/{python-format.d.ts → languages/python-format.d.ts} +1 -1
  58. package/dist/core/checks/{python-format.js → languages/python-format.js} +3 -3
  59. package/dist/core/checks/{python-lint.d.ts → languages/python-lint.d.ts} +1 -1
  60. package/dist/core/checks/{python-lint.js → languages/python-lint.js} +3 -3
  61. package/dist/core/checks/languages/python-typecheck.d.ts +9 -0
  62. package/dist/core/checks/languages/python-typecheck.js +66 -0
  63. package/dist/core/checks/languages/ruby-lint.d.ts +8 -0
  64. package/dist/core/checks/languages/ruby-lint.js +50 -0
  65. package/dist/core/checks/languages/rust-format.d.ts +9 -0
  66. package/dist/core/checks/languages/rust-format.js +45 -0
  67. package/dist/core/checks/languages/rust-lint.d.ts +8 -0
  68. package/dist/core/checks/languages/rust-lint.js +50 -0
  69. package/dist/core/checks/{swift-lint.d.ts → languages/swift-lint.d.ts} +1 -1
  70. package/dist/core/checks/{swift-lint.js → languages/swift-lint.js} +13 -17
  71. package/dist/core/checks/{typecheck.d.ts → languages/typecheck.d.ts} +1 -1
  72. package/dist/core/checks/{typecheck.js → languages/typecheck.js} +3 -3
  73. package/dist/core/checks/{commitlint.d.ts → pr/commitlint.d.ts} +1 -1
  74. package/dist/core/checks/{commitlint.js → pr/commitlint.js} +4 -4
  75. package/dist/core/checks/{pr-body.d.ts → pr/pr-body.d.ts} +1 -1
  76. package/dist/core/checks/{pr-body.js → pr/pr-body.js} +2 -2
  77. package/dist/core/checks/{pr-size-check.d.ts → pr/pr-size-check.d.ts} +1 -1
  78. package/dist/core/checks/{pr-size-check.js → pr/pr-size-check.js} +13 -11
  79. package/dist/core/checks/{pr-title-lint.d.ts → pr/pr-title-lint.d.ts} +1 -1
  80. package/dist/core/checks/{pr-title-lint.js → pr/pr-title-lint.js} +2 -2
  81. package/dist/core/checks/quality/case-collision.d.ts +8 -0
  82. package/dist/core/checks/quality/case-collision.js +40 -0
  83. package/dist/core/checks/quality/config-validation.d.ts +8 -0
  84. package/dist/core/checks/quality/config-validation.js +63 -0
  85. package/dist/core/checks/quality/coverage.d.ts +2 -0
  86. package/dist/core/checks/quality/coverage.js +63 -0
  87. package/dist/core/checks/{dead-code.d.ts → quality/dead-code.d.ts} +1 -1
  88. package/dist/core/checks/{dead-code.js → quality/dead-code.js} +4 -4
  89. package/dist/core/checks/{duplicate-check.d.ts → quality/duplicate-check.d.ts} +1 -1
  90. package/dist/core/checks/{duplicate-check.js → quality/duplicate-check.js} +11 -15
  91. package/dist/core/checks/quality/license-header.d.ts +7 -0
  92. package/dist/core/checks/quality/license-header.js +74 -0
  93. package/dist/core/checks/{markdown-lint.d.ts → quality/markdown-lint.d.ts} +1 -1
  94. package/dist/core/checks/{markdown-lint.js → quality/markdown-lint.js} +8 -21
  95. package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
  96. package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +11 -19
  97. package/dist/core/checks/{diff-security.d.ts → security/diff-security.d.ts} +1 -1
  98. package/dist/core/checks/security/diff-security.js +262 -0
  99. package/dist/core/checks/security/dockerfile-security.d.ts +2 -0
  100. package/dist/core/checks/security/dockerfile-security.js +92 -0
  101. package/dist/core/checks/security/migration-safety.d.ts +2 -0
  102. package/dist/core/checks/security/migration-safety.js +110 -0
  103. package/dist/core/checks/{security.d.ts → security/security.d.ts} +1 -1
  104. package/dist/core/checks/{security.js → security/security.js} +4 -4
  105. package/dist/core/checks/{sensitive-file-guard.d.ts → security/sensitive-file-guard.d.ts} +1 -1
  106. package/dist/core/checks/{sensitive-file-guard.js → security/sensitive-file-guard.js} +4 -4
  107. package/dist/core/checks/security/workflow-security.d.ts +2 -0
  108. package/dist/core/checks/security/workflow-security.js +75 -0
  109. package/dist/core/context.js +54 -10
  110. package/dist/core/registry.js +73 -28
  111. package/dist/core/reporters.d.ts +4 -3
  112. package/dist/core/reporters.js +104 -4
  113. package/dist/core/run-checks.js +21 -3
  114. package/dist/core/targets.d.ts +17 -1
  115. package/dist/core/targets.js +74 -4
  116. package/dist/core/types.d.ts +13 -1
  117. package/dist/index.d.ts +1 -1
  118. package/dist/index.js +3 -1
  119. package/dist/init.js +86 -8
  120. package/dist/interfaces/index.d.ts +101 -1
  121. package/dist/scripts/copy-configs.js +6 -1
  122. package/dist/utils/detect-language.d.ts +1 -1
  123. package/dist/utils/detect-language.js +32 -3
  124. package/dist/utils/glob.d.ts +10 -0
  125. package/dist/utils/glob.js +25 -0
  126. package/dist/utils/index.d.ts +1 -1
  127. package/dist/utils/index.js +1 -1
  128. package/eslint-default-rules.json +62 -0
  129. package/eslint.config.mjs +6 -66
  130. package/package.json +3 -1
  131. package/pr-checkmate.schema.json +166 -0
  132. package/src/config/cspell.json +9 -1
  133. package/src/config/eslint-default-rules.json +62 -0
  134. package/src/config/pr-checkmate.schema.json +166 -0
  135. package/dist/core/checks/diff-security.js +0 -145
  136. package/dist/utils/delta.d.ts +0 -5
  137. package/dist/utils/delta.js +0 -53
  138. package/src/config/eslint.config.mjs +0 -180
  139. /package/dist/core/checks/{ruff-shared.d.ts → languages/ruff-shared.d.ts} +0 -0
  140. /package/dist/core/checks/{ruff-shared.js → languages/ruff-shared.js} +0 -0
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.migrationSafetyCheck = 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 findings_1 = require("../findings");
9
+ /**
10
+ * Paths treated as database migrations. A client overrides via
11
+ * `migrationSafety.paths` (REPLACES this list).
12
+ */
13
+ const DEFAULT_MIGRATION_GLOBS = [
14
+ '**/migrations/**',
15
+ '**/migrate/**',
16
+ '**/db/migrate/**', // Rails
17
+ '**/versions/**', // Alembic
18
+ '**/*.sql',
19
+ ];
20
+ /** Destructive / locking operations, matched on added diff lines. */
21
+ const DDL_PATTERNS = [
22
+ { pattern: /^\+(?!\+).*\bDROP\s+TABLE\b/i, label: 'DROP TABLE — irreversible data loss' },
23
+ { pattern: /^\+(?!\+).*\bDROP\s+COLUMN\b/i, label: 'DROP COLUMN — irreversible data loss' },
24
+ { pattern: /^\+(?!\+).*\bTRUNCATE\b/i, label: 'TRUNCATE — irreversible data loss' },
25
+ {
26
+ pattern: /^\+(?!\+).*\bRENAME\s+(?:COLUMN|TABLE)\b/i,
27
+ label: 'RENAME — breaks code still reading the old name',
28
+ },
29
+ { pattern: /^\+(?!\+).*\bDROP\s+(?:CONSTRAINT|INDEX)\b/i, label: 'DROP CONSTRAINT/INDEX' },
30
+ {
31
+ pattern: /^\+(?!\+).*\bALTER\s+COLUMN\b.*\bSET\s+NOT\s+NULL\b/i,
32
+ label: 'SET NOT NULL — locks the table, rejects existing NULL rows',
33
+ },
34
+ // Alembic (Python) operations.
35
+ { pattern: /^\+(?!\+).*\bop\.drop_column\b/, label: 'op.drop_column — irreversible data loss' },
36
+ { pattern: /^\+(?!\+).*\bop\.drop_table\b/, label: 'op.drop_table — irreversible data loss' },
37
+ { pattern: /^\+(?!\+).*\bop\.drop_constraint\b/, label: 'op.drop_constraint' },
38
+ ];
39
+ const NOT_NULL_NO_DEFAULT = 'ADD COLUMN … NOT NULL without DEFAULT — fails/locks on a non-empty table';
40
+ /**
41
+ * A new NOT NULL column with no DEFAULT: the classic migration that breaks on a
42
+ * populated table. Split out because it's a conjunction (ADD COLUMN + NOT NULL,
43
+ * and NO DEFAULT on the line), not a single keyword.
44
+ */
45
+ function isAddNotNullNoDefault(line) {
46
+ return (/^\+(?!\+)/.test(line) &&
47
+ /\bADD\s+COLUMN\b/i.test(line) &&
48
+ /\bNOT\s+NULL\b/i.test(line) &&
49
+ !/\bDEFAULT\b/i.test(line));
50
+ }
51
+ /** Bucket dangerous added lines by risk label, honouring the opt-out directive. */
52
+ function scan(stdout, findings) {
53
+ for (const line of stdout.split('\n')) {
54
+ if (line.includes('pr-checkmate-ignore'))
55
+ continue;
56
+ for (const { pattern, label } of DDL_PATTERNS) {
57
+ if (pattern.test(line))
58
+ (findings[label] ?? (findings[label] = [])).push(line.slice(1).trim().substring(0, 120));
59
+ }
60
+ if (isAddNotNullNoDefault(line)) {
61
+ (findings[NOT_NULL_NO_DEFAULT] ?? (findings[NOT_NULL_NO_DEFAULT] = [])).push(line.slice(1).trim().substring(0, 120));
62
+ }
63
+ }
64
+ }
65
+ exports.migrationSafetyCheck = {
66
+ name: 'Migration Safety',
67
+ phase: 'informational',
68
+ applies(ctx) {
69
+ return ctx.config.migrationSafety?.enabled !== false && Boolean(ctx.baseSha);
70
+ },
71
+ async run(ctx) {
72
+ // Not sourcePath-scoped: migrations commonly live outside `src` (db/, alembic/).
73
+ // ignoreDirs still applies via resolveTargetFiles.
74
+ const files = await (0, targets_1.resolveTargetFiles)(ctx, ['*']);
75
+ if (files === null)
76
+ return (0, types_1.skip)('git unavailable');
77
+ const globs = ctx.config.migrationSafety?.paths ?? DEFAULT_MIGRATION_GLOBS;
78
+ const migrationFiles = files.filter(f => (0, utils_1.matchesAnyGlob)(f, globs));
79
+ if (migrationFiles.length === 0)
80
+ return (0, types_1.pass)();
81
+ try {
82
+ const { stdout } = await (0, execa_1.execa)('git', [
83
+ 'diff',
84
+ '--unified=0',
85
+ ctx.baseSha,
86
+ ctx.headSha ?? 'HEAD',
87
+ '--',
88
+ ...migrationFiles,
89
+ ], { cwd: ctx.cwd });
90
+ const findings = {};
91
+ scan(stdout, findings);
92
+ if ((0, findings_1.bucketTotal)(findings) === 0)
93
+ return (0, types_1.pass)();
94
+ const sev = (0, findings_1.severityEmitter)(ctx.config.migrationSafety?.severity, 'warn');
95
+ (0, findings_1.logFindings)(findings, {
96
+ prefix: 'Migration Safety',
97
+ sampleLimit: 2,
98
+ isError: sev.icon === '❌',
99
+ });
100
+ const summary = Object.entries(findings)
101
+ .map(([label, arr]) => `${arr.length}× ${label.split(' — ')[0]}`)
102
+ .join(', ');
103
+ return sev.emit(summary);
104
+ }
105
+ catch (err) {
106
+ utils_1.logger.warn(`[Migration Safety]: ⚠️ Could not analyze diff\n${err}`);
107
+ return (0, types_1.skip)('diff unavailable');
108
+ }
109
+ },
110
+ };
@@ -1,4 +1,4 @@
1
- import { Check } from '../types';
1
+ import { Check } from '../../types';
2
2
  /**
3
3
  * Secret scanning via the bundled gitleaks package. Language-agnostic — runs on
4
4
  * every repo (Node, Python, C++, mobile). Secrets found → fail (blocking).
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.securityCheck = void 0;
4
- const config_1 = require("../../config");
5
- const utils_1 = require("../../utils");
6
- const execa_1 = require("../../utils/execa");
7
- const types_1 = require("../types");
4
+ const config_1 = require("../../../config");
5
+ const utils_1 = require("../../../utils");
6
+ const execa_1 = require("../../../utils/execa");
7
+ const types_1 = require("../../types");
8
8
  /**
9
9
  * Secret scanning via the bundled gitleaks package. Language-agnostic — runs on
10
10
  * every repo (Node, Python, C++, mobile). Secrets found → fail (blocking).
@@ -1,2 +1,2 @@
1
- import { Check } from '../types';
1
+ import { Check } from '../../types';
2
2
  export declare const sensitiveFileGuardCheck: Check;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sensitiveFileGuardCheck = void 0;
4
- const utils_1 = require("../../utils");
5
- const execa_1 = require("../../utils/execa");
6
- const types_1 = require("../types");
7
- const findings_1 = require("./findings");
4
+ const utils_1 = require("../../../utils");
5
+ const execa_1 = require("../../../utils/execa");
6
+ const types_1 = require("../../types");
7
+ const findings_1 = require("../findings");
8
8
  const SENSITIVE_CATEGORIES = [
9
9
  {
10
10
  label: 'Environment files (.env)',
@@ -0,0 +1,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const workflowSecurityCheck: Check;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workflowSecurityCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const types_1 = require("../../types");
6
+ const findings_1 = require("../findings");
7
+ /**
8
+ * GitHub Actions workflow risks, matched on added diff lines of workflow files.
9
+ * These are the high-frequency supply-chain / injection footguns.
10
+ */
11
+ const WORKFLOW_PATTERNS = [
12
+ {
13
+ // Untrusted PR-controlled input interpolated into an expression — the classic
14
+ // `run:` script-injection vector when it reaches a shell.
15
+ pattern: /^\+(?!\+).*\$\{\{\s*github\.event\.(?:issue|pull_request|comment|review|discussion|head_commit|commits)[^}]*\.(?:title|body|head_ref|ref|label|name|email|message|url)/,
16
+ label: 'untrusted github.event input in expression (script injection risk)',
17
+ severity: 'error',
18
+ },
19
+ {
20
+ pattern: /^\+(?!\+).*\$\{\{\s*github\.head_ref\s*\}\}/,
21
+ label: 'github.head_ref interpolation (injection risk — use an env var)',
22
+ severity: 'warn',
23
+ },
24
+ {
25
+ // Third-party action not pinned to a full 40-char commit SHA.
26
+ pattern: /^\+(?!\+).*\buses:\s*['"]?[\w.-]+\/[\w.-]+@(?![0-9a-fA-F]{40}\b)[\w./-]+/,
27
+ label: 'action not pinned to a full commit SHA (supply-chain risk)',
28
+ severity: 'warn',
29
+ },
30
+ {
31
+ pattern: /^\+(?!\+).*\bpull_request_target\b/,
32
+ label: 'pull_request_target trigger (runs with repo secrets on untrusted PRs)',
33
+ severity: 'warn',
34
+ },
35
+ ];
36
+ function scanDiff(stdout) {
37
+ const errors = {};
38
+ const warnings = {};
39
+ for (const line of stdout.split('\n')) {
40
+ for (const { pattern, label, severity } of WORKFLOW_PATTERNS) {
41
+ if (pattern.test(line)) {
42
+ const bucket = severity === 'error' ? errors : warnings;
43
+ (bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
44
+ }
45
+ }
46
+ }
47
+ return { errors, warnings };
48
+ }
49
+ exports.workflowSecurityCheck = {
50
+ name: 'Workflow Security',
51
+ phase: 'informational',
52
+ applies(ctx) {
53
+ return ctx.config.workflowSecurity?.enabled !== false && Boolean(ctx.baseSha);
54
+ },
55
+ async run(ctx) {
56
+ try {
57
+ const stdout = await (0, findings_1.diffAddedLines)(ctx, [
58
+ '.github/workflows/*.yml',
59
+ '.github/workflows/*.yaml',
60
+ ]);
61
+ const { errors, warnings } = scanDiff(stdout);
62
+ return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
63
+ prefix: 'Workflow Security',
64
+ errorSummary: n => `${n} issue(s)`,
65
+ warningSummary: n => `${n} warning(s)`,
66
+ sampleLimit: 3,
67
+ ignore: ctx.config.workflowSecurity?.ignore,
68
+ });
69
+ }
70
+ catch (err) {
71
+ utils_1.logger.warn(`[Workflow Security]: ⚠️ Could not analyze diff\n${err}`);
72
+ return (0, types_1.skip)('diff unavailable');
73
+ }
74
+ },
75
+ };
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.buildContext = buildContext;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
4
8
  const loader_1 = require("../config/loader");
5
9
  const utils_1 = require("../utils");
6
10
  const detect_language_1 = require("../utils/detect-language");
@@ -10,7 +14,35 @@ const execa_1 = require("../utils/execa");
10
14
  * Prefers explicit opts, then GitHub PR event, then Bitbucket PR env, then git HEAD^.
11
15
  * Returns undefined SHAs when nothing usable is found → checks fall back to full scan.
12
16
  */
17
+ /**
18
+ * Resolve a git ref to a concrete commit SHA, or undefined when it can't be
19
+ * resolved in this clone. `--verify --quiet` emits nothing (instead of echoing
20
+ * the literal ref) and exits non-zero for a missing ref — e.g. `HEAD^` on a
21
+ * single-commit repo, or a base branch not fetched in a shallow CI clone. That
22
+ * way an unresolvable base yields `undefined` and callers fall back to a full
23
+ * scan, rather than diffing against a garbage ref and silently checking nothing.
24
+ */
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
+ }
13
32
  async function resolveDiffRange(cwd, opts) {
33
+ // Pre-commit mode: diff HEAD against the current index. `git write-tree`
34
+ // materialises the staged index as a real tree object, so every check that
35
+ // diffs baseSha..headSha sees exactly the staged changes — no per-check code.
36
+ if (opts.staged) {
37
+ try {
38
+ const head = (await (0, execa_1.execa)('git', ['rev-parse', 'HEAD'], { cwd })).stdout.trim();
39
+ const tree = (await (0, execa_1.execa)('git', ['write-tree'], { cwd })).stdout.trim();
40
+ return { baseSha: head, headSha: tree };
41
+ }
42
+ catch {
43
+ return {};
44
+ }
45
+ }
14
46
  if (opts.baseSha || opts.headSha) {
15
47
  return { baseSha: opts.baseSha, headSha: opts.headSha };
16
48
  }
@@ -22,17 +54,28 @@ async function resolveDiffRange(cwd, opts) {
22
54
  catch {
23
55
  return {};
24
56
  }
25
- // Bitbucket exposes the PR destination branch; GitHub delta is handled by utils/delta.
26
- const bitbucketTarget = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
27
- try {
28
- const ref = bitbucketTarget ? `origin/${bitbucketTarget}` : 'HEAD^';
29
- const { stdout } = await (0, execa_1.execa)('git', ['rev-parse', ref], { cwd, reject: false });
30
- const baseSha = stdout.trim();
31
- return { baseSha: baseSha || undefined, headSha };
32
- }
33
- catch {
34
- return { headSha };
57
+ // GitHub Actions: the PR event payload carries the base branch SHA. Read it
58
+ // directly so delta works without relying on any client-set variable
59
+ // (GITHUB_EVENT_* are injected by the runner).
60
+ if (process.env.GITHUB_EVENT_NAME === 'pull_request' && process.env.GITHUB_EVENT_PATH) {
61
+ try {
62
+ const event = JSON.parse(node_fs_1.default.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf-8'));
63
+ const eventBase = event?.pull_request?.base?.sha;
64
+ // Only use it if the commit is actually present in this clone; otherwise
65
+ // fall through so a shallow clone degrades to a full scan, not 0 files.
66
+ const baseSha = eventBase ? await resolveRef(cwd, eventBase) : undefined;
67
+ if (baseSha)
68
+ return { baseSha, headSha };
69
+ }
70
+ catch {
71
+ // Malformed/unreadable event — fall through to the git-based resolution.
72
+ }
35
73
  }
74
+ // Bitbucket exposes the PR destination branch as an env var (also runner-set).
75
+ const bitbucketTarget = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
76
+ const ref = bitbucketTarget ? `origin/${bitbucketTarget}` : 'HEAD^';
77
+ const baseSha = await resolveRef(cwd, ref);
78
+ return { baseSha, headSha };
36
79
  }
37
80
  /**
38
81
  * Assemble the shared CheckContext once per run. Language detection is skipped
@@ -51,5 +94,6 @@ async function buildContext(opts = {}) {
51
94
  write: opts.write ?? false,
52
95
  baseSha,
53
96
  headSha,
97
+ cache: new Map(),
54
98
  };
55
99
  }
@@ -1,40 +1,62 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.REGISTRY = void 0;
4
- // blocking — a failure here makes the whole run fail (report.ok === false)
5
- const circular_deps_1 = require("./checks/circular-deps");
6
- const commitlint_1 = require("./checks/commitlint");
7
- const cpp_format_1 = require("./checks/cpp-format");
8
- const dead_code_1 = require("./checks/dead-code");
9
- const dependency_check_1 = require("./checks/dependency-check");
10
- const diff_security_1 = require("./checks/diff-security");
11
- const diff_smell_1 = require("./checks/diff-smell");
12
- const duplicate_check_1 = require("./checks/duplicate-check");
13
- const kotlin_lint_1 = require("./checks/kotlin-lint");
14
- const license_check_1 = require("./checks/license-check");
15
- const lint_1 = require("./checks/lint");
16
- const markdown_lint_1 = require("./checks/markdown-lint");
17
- const npm_audit_1 = require("./checks/npm-audit");
18
- // informational — warn/skip only, never fail the run
19
- const outdated_deps_1 = require("./checks/outdated-deps");
20
- const pr_body_1 = require("./checks/pr-body");
21
- const pr_size_check_1 = require("./checks/pr-size-check");
22
- const pr_title_lint_1 = require("./checks/pr-title-lint");
23
- const prettier_1 = require("./checks/prettier");
24
- const python_format_1 = require("./checks/python-format");
25
- const python_lint_1 = require("./checks/python-lint");
26
- const security_1 = require("./checks/security");
27
- const sensitive_file_guard_1 = require("./checks/sensitive-file-guard");
28
- const spellcheck_1 = require("./checks/spellcheck");
29
- const swift_lint_1 = require("./checks/swift-lint");
30
- // format — runs last, may write files
31
- const typecheck_1 = require("./checks/typecheck");
4
+ const banned_imports_1 = require("./checks/dependencies/banned-imports");
5
+ const circular_deps_1 = require("./checks/dependencies/circular-deps");
6
+ const dependency_check_1 = require("./checks/dependencies/dependency-check");
7
+ const license_check_1 = require("./checks/dependencies/license-check");
8
+ const npm_audit_1 = require("./checks/dependencies/npm-audit");
9
+ const outdated_deps_1 = require("./checks/dependencies/outdated-deps");
10
+ const vuln_scan_1 = require("./checks/dependencies/vuln-scan");
11
+ const custom_rules_1 = require("./checks/git/custom-rules");
12
+ const diff_smell_1 = require("./checks/git/diff-smell");
13
+ const large_files_1 = require("./checks/git/large-files");
14
+ const leftover_debug_1 = require("./checks/git/leftover-debug");
15
+ const lockfile_drift_1 = require("./checks/git/lockfile-drift");
16
+ const merge_conflict_1 = require("./checks/git/merge-conflict");
17
+ const missing_tests_1 = require("./checks/git/missing-tests");
18
+ const todo_fixme_1 = require("./checks/git/todo-fixme");
19
+ const cpp_format_1 = require("./checks/languages/cpp-format");
20
+ const csharp_format_1 = require("./checks/languages/csharp-format");
21
+ const go_format_1 = require("./checks/languages/go-format");
22
+ const go_lint_1 = require("./checks/languages/go-lint");
23
+ const kotlin_lint_1 = require("./checks/languages/kotlin-lint");
24
+ const lint_1 = require("./checks/languages/lint");
25
+ const php_format_1 = require("./checks/languages/php-format");
26
+ const prettier_1 = require("./checks/languages/prettier");
27
+ const python_format_1 = require("./checks/languages/python-format");
28
+ const python_lint_1 = require("./checks/languages/python-lint");
29
+ const python_typecheck_1 = require("./checks/languages/python-typecheck");
30
+ const ruby_lint_1 = require("./checks/languages/ruby-lint");
31
+ const rust_format_1 = require("./checks/languages/rust-format");
32
+ const rust_lint_1 = require("./checks/languages/rust-lint");
33
+ const swift_lint_1 = require("./checks/languages/swift-lint");
34
+ const typecheck_1 = require("./checks/languages/typecheck");
35
+ const commitlint_1 = require("./checks/pr/commitlint");
36
+ const pr_body_1 = require("./checks/pr/pr-body");
37
+ const pr_size_check_1 = require("./checks/pr/pr-size-check");
38
+ const pr_title_lint_1 = require("./checks/pr/pr-title-lint");
39
+ const case_collision_1 = require("./checks/quality/case-collision");
40
+ const config_validation_1 = require("./checks/quality/config-validation");
41
+ const coverage_1 = require("./checks/quality/coverage");
42
+ const dead_code_1 = require("./checks/quality/dead-code");
43
+ const duplicate_check_1 = require("./checks/quality/duplicate-check");
44
+ const license_header_1 = require("./checks/quality/license-header");
45
+ const markdown_lint_1 = require("./checks/quality/markdown-lint");
46
+ const spellcheck_1 = require("./checks/quality/spellcheck");
47
+ const diff_security_1 = require("./checks/security/diff-security");
48
+ const dockerfile_security_1 = require("./checks/security/dockerfile-security");
49
+ const migration_safety_1 = require("./checks/security/migration-safety");
50
+ const security_1 = require("./checks/security/security");
51
+ const sensitive_file_guard_1 = require("./checks/security/sensitive-file-guard");
52
+ const workflow_security_1 = require("./checks/security/workflow-security");
32
53
  /**
33
54
  * Every check, in declaration order. The orchestrator groups them by `phase`
34
55
  * (blocking → informational → format) and runs each phase's checks in parallel.
35
56
  */
36
57
  exports.REGISTRY = [
37
58
  // blocking
59
+ merge_conflict_1.mergeConflictCheck,
38
60
  security_1.securityCheck,
39
61
  typecheck_1.typecheckCheck,
40
62
  lint_1.lintCheck,
@@ -45,6 +67,21 @@ exports.REGISTRY = [
45
67
  // informational
46
68
  diff_smell_1.diffSmellCheck,
47
69
  diff_security_1.diffSecurityCheck,
70
+ workflow_security_1.workflowSecurityCheck,
71
+ dockerfile_security_1.dockerfileSecurityCheck,
72
+ case_collision_1.caseCollisionCheck,
73
+ vuln_scan_1.vulnScanCheck,
74
+ custom_rules_1.customRulesCheck,
75
+ banned_imports_1.bannedImportsCheck,
76
+ license_header_1.licenseHeaderCheck,
77
+ config_validation_1.configValidationCheck,
78
+ leftover_debug_1.leftoverDebugCheck,
79
+ lockfile_drift_1.lockfileDriftCheck,
80
+ large_files_1.largeFilesCheck,
81
+ todo_fixme_1.todoFixmeCheck,
82
+ missing_tests_1.missingTestsCheck,
83
+ coverage_1.coverageCheck,
84
+ migration_safety_1.migrationSafetyCheck,
48
85
  sensitive_file_guard_1.sensitiveFileGuardCheck,
49
86
  pr_body_1.prBodyCheck,
50
87
  duplicate_check_1.duplicateCheck,
@@ -55,11 +92,19 @@ exports.REGISTRY = [
55
92
  markdown_lint_1.markdownLintCheck,
56
93
  spellcheck_1.spellcheckCheck,
57
94
  python_lint_1.pythonLintCheck,
95
+ python_typecheck_1.pythonTypecheckCheck,
58
96
  dead_code_1.deadCodeCheck,
59
97
  swift_lint_1.swiftLintCheck,
60
98
  kotlin_lint_1.kotlinLintCheck,
99
+ go_lint_1.goLintCheck,
100
+ rust_lint_1.rustLintCheck,
101
+ ruby_lint_1.rubyLintCheck,
61
102
  // format
62
103
  prettier_1.prettierCheck,
63
104
  cpp_format_1.cppFormatCheck,
64
105
  python_format_1.pythonFormatCheck,
106
+ go_format_1.goFormatCheck,
107
+ rust_format_1.rustFormatCheck,
108
+ csharp_format_1.csharpFormatCheck,
109
+ php_format_1.phpFormatCheck,
65
110
  ];
@@ -1,6 +1,7 @@
1
- import type { CheckReport } from './types';
1
+ import type { CheckReport, ReporterKind } from './types';
2
2
  /**
3
3
  * Emit the report through the selected channel.
4
- * none: nothing · stdout: console table · github: table + upsert PR comment.
4
+ * none: nothing · stdout: console table · github/gitlab/bitbucket: table +
5
+ * upsert the PR/MR comment · auto: detect the platform from the environment.
5
6
  */
6
- export declare function report(kind: 'none' | 'stdout' | 'github', result: CheckReport): Promise<void>;
7
+ export declare function report(kind: ReporterKind, result: CheckReport): Promise<void>;
@@ -110,15 +110,115 @@ async function upsertGithubComment(body) {
110
110
  utils_1.logger.warn(`[reporter]: ⚠️ Could not post GitHub comment: ${err}`);
111
111
  }
112
112
  }
113
+ /**
114
+ * Upsert a GitLab MR note (find our marker → update, else create). Uses the
115
+ * standard CI variables plus a `GITLAB_TOKEN` with `api` scope. Skips gracefully
116
+ * when not in an MR pipeline or the token is missing.
117
+ */
118
+ async function upsertGitlabNote(body) {
119
+ const base = process.env.CI_API_V4_URL;
120
+ const project = process.env.CI_PROJECT_ID;
121
+ const iid = process.env.CI_MERGE_REQUEST_IID;
122
+ const token = process.env.GITLAB_TOKEN;
123
+ if (!base || !project || !iid || !token) {
124
+ utils_1.logger.info('[reporter]: Skipping GitLab note (not an MR pipeline or GITLAB_TOKEN missing)');
125
+ return;
126
+ }
127
+ const notes = `${base}/projects/${project}/merge_requests/${iid}/notes`;
128
+ const headers = { 'PRIVATE-TOKEN': token, 'Content-Type': 'application/json' };
129
+ try {
130
+ const listRes = await fetch(notes, { headers });
131
+ if (listRes.ok) {
132
+ const existing = (await listRes.json()).find(n => typeof n.body === 'string' && n.body.includes(COMMENT_MARKER));
133
+ if (existing) {
134
+ await fetch(`${notes}/${existing.id}`, {
135
+ method: 'PUT',
136
+ headers,
137
+ body: JSON.stringify({ body }),
138
+ });
139
+ utils_1.logger.info('[reporter]: ✅ Updated PR CheckMate note on GitLab MR');
140
+ return;
141
+ }
142
+ }
143
+ await fetch(notes, { method: 'POST', headers, body: JSON.stringify({ body }) });
144
+ utils_1.logger.info('[reporter]: ✅ Posted PR CheckMate summary to GitLab MR');
145
+ }
146
+ catch (err) {
147
+ utils_1.logger.warn(`[reporter]: ⚠️ Could not post GitLab note: ${err}`);
148
+ }
149
+ }
150
+ /**
151
+ * Upsert a Bitbucket PR comment (find our marker → update, else create). Uses
152
+ * Bitbucket Pipelines variables plus a `BITBUCKET_TOKEN` (repo/PR write). Skips
153
+ * gracefully when not in a PR pipeline or the token is missing.
154
+ */
155
+ async function upsertBitbucketComment(body) {
156
+ const fullName = process.env.BITBUCKET_REPO_FULL_NAME;
157
+ const prId = process.env.BITBUCKET_PR_ID;
158
+ const token = process.env.BITBUCKET_TOKEN;
159
+ if (!fullName || !prId || !token) {
160
+ utils_1.logger.info('[reporter]: Skipping Bitbucket comment (not a PR pipeline or BITBUCKET_TOKEN missing)');
161
+ return;
162
+ }
163
+ const commentsUrl = `https://api.bitbucket.org/2.0/repositories/${fullName}/pullrequests/${prId}/comments`;
164
+ const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
165
+ try {
166
+ const listRes = await fetch(`${commentsUrl}?pagelen=100`, { headers });
167
+ if (listRes.ok) {
168
+ const values = (await listRes.json()).values;
169
+ const existing = values?.find(c => c.content?.raw?.includes(COMMENT_MARKER));
170
+ if (existing) {
171
+ await fetch(`${commentsUrl}/${existing.id}`, {
172
+ method: 'PUT',
173
+ headers,
174
+ body: JSON.stringify({ content: { raw: body } }),
175
+ });
176
+ utils_1.logger.info('[reporter]: ✅ Updated PR CheckMate comment on Bitbucket PR');
177
+ return;
178
+ }
179
+ }
180
+ await fetch(commentsUrl, {
181
+ method: 'POST',
182
+ headers,
183
+ body: JSON.stringify({ content: { raw: body } }),
184
+ });
185
+ utils_1.logger.info('[reporter]: ✅ Posted PR CheckMate summary to Bitbucket PR');
186
+ }
187
+ catch (err) {
188
+ utils_1.logger.warn(`[reporter]: ⚠️ Could not post Bitbucket comment: ${err}`);
189
+ }
190
+ }
191
+ /** Detect the CI platform from the environment, or null when unknown/local. */
192
+ function detectPlatform() {
193
+ if (process.env.GITHUB_ACTIONS || process.env.GITHUB_EVENT_NAME)
194
+ return 'github';
195
+ if (process.env.GITLAB_CI)
196
+ return 'gitlab';
197
+ if (process.env.BITBUCKET_PIPELINE_UUID || process.env.BITBUCKET_PR_ID)
198
+ return 'bitbucket';
199
+ return null;
200
+ }
201
+ /** Post the markdown report to a specific platform's PR/MR. */
202
+ async function postToPlatform(platform, result) {
203
+ const body = buildMarkdownReport(result);
204
+ if (platform === 'github')
205
+ return upsertGithubComment(body);
206
+ if (platform === 'gitlab')
207
+ return upsertGitlabNote(body);
208
+ return upsertBitbucketComment(body);
209
+ }
113
210
  /**
114
211
  * Emit the report through the selected channel.
115
- * none: nothing · stdout: console table · github: table + upsert PR comment.
212
+ * none: nothing · stdout: console table · github/gitlab/bitbucket: table +
213
+ * upsert the PR/MR comment · auto: detect the platform from the environment.
116
214
  */
117
215
  async function report(kind, result) {
118
216
  if (kind === 'none')
119
217
  return;
120
218
  printStdout(result);
121
- if (kind === 'github' && result.results.length > 0) {
122
- await upsertGithubComment(buildMarkdownReport(result));
123
- }
219
+ if (result.results.length === 0)
220
+ return;
221
+ const platform = kind === 'auto' ? detectPlatform() : kind === 'stdout' ? null : kind;
222
+ if (platform)
223
+ await postToPlatform(platform, result);
124
224
  }
@@ -50,6 +50,20 @@ function selectChecks(checks, opts) {
50
50
  function errMessage(err) {
51
51
  return err instanceof Error ? err.message.split('\n')[0].slice(0, 160) : String(err);
52
52
  }
53
+ /**
54
+ * Apply the universal `config.severity` override for a check. Only remaps
55
+ * between warn↔fail (pass/skip are never changed); `off` is handled earlier by
56
+ * excluding the check from the run entirely. This is what lets a repo turn any
57
+ * advisory check into a gate, or downgrade a blocking one, by name.
58
+ */
59
+ function applySeverity(name, status, ctx) {
60
+ const sev = ctx.config.severity?.[name];
61
+ if (sev === 'error' && status === 'warn')
62
+ return 'fail';
63
+ if (sev === 'warn' && status === 'fail')
64
+ return 'warn';
65
+ return status;
66
+ }
53
67
  /** Run one check: gate on `applies`, execute, translate throws into `fail`. */
54
68
  async function runOne(check, ctx, collector) {
55
69
  try {
@@ -63,12 +77,13 @@ async function runOne(check, ctx, collector) {
63
77
  return;
64
78
  }
65
79
  const outcome = await check.run(ctx);
66
- collector.register(check.name, outcome.status, outcome.details);
80
+ collector.register(check.name, applySeverity(check.name, outcome.status, ctx), outcome.details);
67
81
  }
68
82
  catch (err) {
69
83
  // An unexpected throw in a blocking check fails the run; in informational/format
70
84
  // phases it only warns — those phases never block, matching the legacy pipeline.
71
- const status = check.phase === 'blocking' ? 'fail' : 'warn';
85
+ const base = check.phase === 'blocking' ? 'fail' : 'warn';
86
+ const status = applySeverity(check.name, base, ctx);
72
87
  utils_1.logger.error(`[runChecks]: ${status === 'fail' ? '❌' : '⚠️'} ${check.name} threw: ${err}`);
73
88
  collector.register(check.name, status, errMessage(err));
74
89
  }
@@ -82,7 +97,10 @@ async function runOne(check, ctx, collector) {
82
97
  */
83
98
  async function runChecks(opts = {}) {
84
99
  const ctx = await (0, context_1.buildContext)(opts);
85
- const selected = selectChecks(resolveChecks(opts), opts);
100
+ // `severity: { "<name>": "off" }` disables any check by name, universally —
101
+ // a superset of the per-check `enabled: false` flags.
102
+ const off = ctx.config.severity ?? {};
103
+ const selected = selectChecks(resolveChecks(opts), opts).filter(c => off[c.name] !== 'off');
86
104
  const collector = new result_collector_1.ResultCollector();
87
105
  for (const phase of types_1.PHASES) {
88
106
  const inPhase = selected.filter(c => c.phase === phase);