pr-checkmate 1.22.8 → 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 (129) 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 +9 -0
  5. package/dist/config/constants.js +68 -1
  6. package/dist/core/checks/dependencies/banned-imports.d.ts +2 -0
  7. package/dist/core/checks/dependencies/banned-imports.js +71 -0
  8. package/dist/core/checks/{circular-deps.d.ts → dependencies/circular-deps.d.ts} +1 -1
  9. package/dist/core/checks/{circular-deps.js → dependencies/circular-deps.js} +5 -5
  10. package/dist/core/checks/{dependency-check.d.ts → dependencies/dependency-check.d.ts} +1 -1
  11. package/dist/core/checks/{dependency-check.js → dependencies/dependency-check.js} +3 -3
  12. package/dist/core/checks/{license-check.d.ts → dependencies/license-check.d.ts} +1 -1
  13. package/dist/core/checks/{license-check.js → dependencies/license-check.js} +3 -3
  14. package/dist/core/checks/{npm-audit.d.ts → dependencies/npm-audit.d.ts} +1 -1
  15. package/dist/core/checks/{npm-audit.js → dependencies/npm-audit.js} +3 -3
  16. package/dist/core/checks/{outdated-deps.d.ts → dependencies/outdated-deps.d.ts} +1 -1
  17. package/dist/core/checks/{outdated-deps.js → dependencies/outdated-deps.js} +3 -3
  18. package/dist/core/checks/dependencies/vuln-scan.d.ts +9 -0
  19. package/dist/core/checks/dependencies/vuln-scan.js +41 -0
  20. package/dist/core/checks/findings.d.ts +37 -1
  21. package/dist/core/checks/findings.js +80 -3
  22. package/dist/core/checks/git/custom-rules.d.ts +2 -0
  23. package/dist/core/checks/git/custom-rules.js +102 -0
  24. package/dist/core/checks/{diff-smell.d.ts → git/diff-smell.d.ts} +1 -1
  25. package/dist/core/checks/{diff-smell.js → git/diff-smell.js} +3 -3
  26. package/dist/core/checks/git/large-files.d.ts +2 -0
  27. package/dist/core/checks/git/large-files.js +83 -0
  28. package/dist/core/checks/git/leftover-debug.d.ts +2 -0
  29. package/dist/core/checks/git/leftover-debug.js +77 -0
  30. package/dist/core/checks/git/lockfile-drift.d.ts +2 -0
  31. package/dist/core/checks/git/lockfile-drift.js +114 -0
  32. package/dist/core/checks/git/merge-conflict.d.ts +2 -0
  33. package/dist/core/checks/git/merge-conflict.js +109 -0
  34. package/dist/core/checks/git/missing-tests.d.ts +2 -0
  35. package/dist/core/checks/git/missing-tests.js +92 -0
  36. package/dist/core/checks/git/todo-fixme.d.ts +7 -0
  37. package/dist/core/checks/git/todo-fixme.js +74 -0
  38. package/dist/core/checks/{cpp-format.d.ts → languages/cpp-format.d.ts} +1 -1
  39. package/dist/core/checks/{cpp-format.js → languages/cpp-format.js} +3 -3
  40. package/dist/core/checks/languages/csharp-format.d.ts +13 -0
  41. package/dist/core/checks/languages/csharp-format.js +52 -0
  42. package/dist/core/checks/languages/go-format.d.ts +8 -0
  43. package/dist/core/checks/languages/go-format.js +48 -0
  44. package/dist/core/checks/languages/go-lint.d.ts +8 -0
  45. package/dist/core/checks/languages/go-lint.js +48 -0
  46. package/dist/core/checks/{kotlin-lint.d.ts → languages/kotlin-lint.d.ts} +1 -1
  47. package/dist/core/checks/{kotlin-lint.js → languages/kotlin-lint.js} +13 -17
  48. package/dist/core/checks/{lint.d.ts → languages/lint.d.ts} +1 -1
  49. package/dist/core/checks/{lint.js → languages/lint.js} +5 -5
  50. package/dist/core/checks/languages/php-format.d.ts +9 -0
  51. package/dist/core/checks/languages/php-format.js +49 -0
  52. package/dist/core/checks/{prettier.d.ts → languages/prettier.d.ts} +1 -1
  53. package/dist/core/checks/{prettier.js → languages/prettier.js} +5 -5
  54. package/dist/core/checks/{python-format.d.ts → languages/python-format.d.ts} +1 -1
  55. package/dist/core/checks/{python-format.js → languages/python-format.js} +3 -3
  56. package/dist/core/checks/{python-lint.d.ts → languages/python-lint.d.ts} +1 -1
  57. package/dist/core/checks/{python-lint.js → languages/python-lint.js} +3 -3
  58. package/dist/core/checks/languages/python-typecheck.d.ts +9 -0
  59. package/dist/core/checks/languages/python-typecheck.js +66 -0
  60. package/dist/core/checks/languages/ruby-lint.d.ts +8 -0
  61. package/dist/core/checks/languages/ruby-lint.js +50 -0
  62. package/dist/core/checks/languages/rust-format.d.ts +9 -0
  63. package/dist/core/checks/languages/rust-format.js +45 -0
  64. package/dist/core/checks/languages/rust-lint.d.ts +8 -0
  65. package/dist/core/checks/languages/rust-lint.js +50 -0
  66. package/dist/core/checks/{swift-lint.d.ts → languages/swift-lint.d.ts} +1 -1
  67. package/dist/core/checks/{swift-lint.js → languages/swift-lint.js} +13 -17
  68. package/dist/core/checks/{typecheck.d.ts → languages/typecheck.d.ts} +1 -1
  69. package/dist/core/checks/{typecheck.js → languages/typecheck.js} +3 -3
  70. package/dist/core/checks/{commitlint.d.ts → pr/commitlint.d.ts} +1 -1
  71. package/dist/core/checks/{commitlint.js → pr/commitlint.js} +4 -4
  72. package/dist/core/checks/{pr-body.d.ts → pr/pr-body.d.ts} +1 -1
  73. package/dist/core/checks/{pr-body.js → pr/pr-body.js} +2 -2
  74. package/dist/core/checks/{pr-size-check.d.ts → pr/pr-size-check.d.ts} +1 -1
  75. package/dist/core/checks/{pr-size-check.js → pr/pr-size-check.js} +13 -11
  76. package/dist/core/checks/{pr-title-lint.d.ts → pr/pr-title-lint.d.ts} +1 -1
  77. package/dist/core/checks/{pr-title-lint.js → pr/pr-title-lint.js} +2 -2
  78. package/dist/core/checks/quality/case-collision.d.ts +8 -0
  79. package/dist/core/checks/quality/case-collision.js +40 -0
  80. package/dist/core/checks/quality/config-validation.d.ts +8 -0
  81. package/dist/core/checks/quality/config-validation.js +63 -0
  82. package/dist/core/checks/quality/coverage.d.ts +2 -0
  83. package/dist/core/checks/quality/coverage.js +63 -0
  84. package/dist/core/checks/{dead-code.d.ts → quality/dead-code.d.ts} +1 -1
  85. package/dist/core/checks/{dead-code.js → quality/dead-code.js} +4 -4
  86. package/dist/core/checks/{duplicate-check.d.ts → quality/duplicate-check.d.ts} +1 -1
  87. package/dist/core/checks/{duplicate-check.js → quality/duplicate-check.js} +6 -6
  88. package/dist/core/checks/quality/license-header.d.ts +7 -0
  89. package/dist/core/checks/quality/license-header.js +74 -0
  90. package/dist/core/checks/{markdown-lint.d.ts → quality/markdown-lint.d.ts} +1 -1
  91. package/dist/core/checks/{markdown-lint.js → quality/markdown-lint.js} +5 -5
  92. package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
  93. package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +5 -5
  94. package/dist/core/checks/{diff-security.d.ts → security/diff-security.d.ts} +1 -1
  95. package/dist/core/checks/security/diff-security.js +262 -0
  96. package/dist/core/checks/security/dockerfile-security.d.ts +2 -0
  97. package/dist/core/checks/security/dockerfile-security.js +92 -0
  98. package/dist/core/checks/security/migration-safety.d.ts +2 -0
  99. package/dist/core/checks/security/migration-safety.js +110 -0
  100. package/dist/core/checks/{security.d.ts → security/security.d.ts} +1 -1
  101. package/dist/core/checks/{security.js → security/security.js} +4 -4
  102. package/dist/core/checks/{sensitive-file-guard.d.ts → security/sensitive-file-guard.d.ts} +1 -1
  103. package/dist/core/checks/{sensitive-file-guard.js → security/sensitive-file-guard.js} +4 -4
  104. package/dist/core/checks/security/workflow-security.d.ts +2 -0
  105. package/dist/core/checks/security/workflow-security.js +75 -0
  106. package/dist/core/context.js +14 -0
  107. package/dist/core/registry.js +73 -28
  108. package/dist/core/reporters.d.ts +4 -3
  109. package/dist/core/reporters.js +104 -4
  110. package/dist/core/run-checks.js +21 -3
  111. package/dist/core/targets.d.ts +4 -1
  112. package/dist/core/targets.js +16 -3
  113. package/dist/core/types.d.ts +13 -1
  114. package/dist/init.js +63 -0
  115. package/dist/interfaces/index.d.ts +99 -0
  116. package/dist/scripts/copy-configs.js +1 -0
  117. package/dist/utils/detect-language.d.ts +1 -1
  118. package/dist/utils/detect-language.js +32 -3
  119. package/dist/utils/glob.d.ts +10 -0
  120. package/dist/utils/glob.js +25 -0
  121. package/dist/utils/index.d.ts +1 -0
  122. package/dist/utils/index.js +1 -0
  123. package/package.json +2 -1
  124. package/pr-checkmate.schema.json +166 -0
  125. package/src/config/cspell.json +9 -1
  126. package/src/config/pr-checkmate.schema.json +166 -0
  127. package/dist/core/checks/diff-security.js +0 -145
  128. /package/dist/core/checks/{ruff-shared.d.ts → languages/ruff-shared.d.ts} +0 -0
  129. /package/dist/core/checks/{ruff-shared.js → languages/ruff-shared.js} +0 -0
@@ -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);
@@ -6,7 +6,10 @@ import type { CheckContext } from './types';
6
6
  * `null` when git itself is unusable (caller should `skip`).
7
7
  *
8
8
  * Only ACMR files are returned (Added/Copied/Modified/Renamed) — deletions are
9
- * excluded, so every returned path exists on disk and is safe to read.
9
+ * excluded. Results are additionally filtered to paths that still exist on disk,
10
+ * so a rename/move within the diff range (where the old path shows up but no
11
+ * longer exists) can never hand a missing path to a downstream tool like ESLint
12
+ * or Prettier, which crash hard on a non-existent pattern.
10
13
  */
11
14
  export declare function resolveTargetFiles(ctx: CheckContext, globs: string[]): Promise<string[] | null>;
12
15
  /**
@@ -1,8 +1,13 @@
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.resolveTargetFiles = resolveTargetFiles;
4
7
  exports.resolveScopedTargetFiles = resolveScopedTargetFiles;
5
8
  exports.listTrackedFiles = listTrackedFiles;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
6
11
  const config_1 = require("../config");
7
12
  const utils_1 = require("../utils");
8
13
  const execa_1 = require("../utils/execa");
@@ -25,7 +30,10 @@ function excludeIgnoredDirs(files) {
25
30
  * `null` when git itself is unusable (caller should `skip`).
26
31
  *
27
32
  * Only ACMR files are returned (Added/Copied/Modified/Renamed) — deletions are
28
- * excluded, so every returned path exists on disk and is safe to read.
33
+ * excluded. Results are additionally filtered to paths that still exist on disk,
34
+ * so a rename/move within the diff range (where the old path shows up but no
35
+ * longer exists) can never hand a missing path to a downstream tool like ESLint
36
+ * or Prettier, which crash hard on a non-existent pattern.
29
37
  */
30
38
  async function resolveTargetFiles(ctx, globs) {
31
39
  try {
@@ -40,11 +48,16 @@ async function resolveTargetFiles(ctx, globs) {
40
48
  ...globs,
41
49
  ]
42
50
  : ['ls-files', ...globs];
43
- const { stdout } = await (0, execa_1.execa)('git', args, { cwd: ctx.cwd, reject: false });
51
+ const cacheKey = `t:${args.join('')}`;
52
+ let stdout = ctx.cache?.get(cacheKey);
53
+ if (stdout === undefined) {
54
+ stdout = (await (0, execa_1.execa)('git', args, { cwd: ctx.cwd, reject: false })).stdout;
55
+ ctx.cache?.set(cacheKey, stdout);
56
+ }
44
57
  const files = excludeIgnoredDirs(stdout
45
58
  .split('\n')
46
59
  .map(f => f.trim())
47
- .filter(Boolean));
60
+ .filter(Boolean)).filter(f => node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, f)));
48
61
  utils_1.logger.info(`[targets]: ${ctx.baseSha ? '📂 Delta' : 'Full scan'}: ${files.length} file(s) for ${globs.join(', ')}`);
49
62
  return files;
50
63
  }
@@ -24,6 +24,12 @@ export interface CheckContext {
24
24
  /** Diff range for delta-aware checks; undefined → full-repo scan. */
25
25
  baseSha?: string;
26
26
  headSha?: string;
27
+ /**
28
+ * Per-run memoization cache for identical git invocations (many checks diff
29
+ * the same range). Created once per `runChecks`; safe because git state is
30
+ * stable within a run. Keyed by the git args.
31
+ */
32
+ cache?: Map<string, string>;
27
33
  }
28
34
  /** Outcome of a single check — the name is attached by the orchestrator. */
29
35
  export interface CheckOutcome {
@@ -42,7 +48,7 @@ export interface Check {
42
48
  applies(ctx: CheckContext): boolean | Promise<boolean>;
43
49
  run(ctx: CheckContext): Promise<CheckOutcome>;
44
50
  }
45
- export type ReporterKind = 'none' | 'stdout' | 'github';
51
+ export type ReporterKind = 'none' | 'stdout' | 'github' | 'gitlab' | 'bitbucket' | 'auto';
46
52
  /** Custom reporter: receives the final report to emit however the consumer wants. */
47
53
  export type ReporterFn = (report: CheckReport) => void | Promise<void>;
48
54
  export interface RunOptions {
@@ -78,6 +84,12 @@ export interface RunOptions {
78
84
  config?: PRCheckMateConfig;
79
85
  baseSha?: string;
80
86
  headSha?: string;
87
+ /**
88
+ * Pre-commit / local mode: check the staged changes (index vs HEAD) instead of
89
+ * a PR range. Implemented by diffing HEAD against the index tree
90
+ * (`git write-tree`), so every delta-aware check works unchanged.
91
+ */
92
+ staged?: boolean;
81
93
  }
82
94
  export interface CheckReport {
83
95
  results: CheckResult[];
package/dist/init.js CHANGED
@@ -28,6 +28,18 @@ function loadDefaultEslintRules() {
28
28
  * Default configuration structure with customization options
29
29
  */
30
30
  const DEFAULT_CONFIG = {
31
+ // Points editors (VS Code, etc.) at the bundled JSON schema for autocomplete
32
+ // and inline validation of this file.
33
+ $schema: './node_modules/pr-checkmate/pr-checkmate.schema.json',
34
+ // Human-readable hints. JSON has no comments, so these `//`-prefixed keys are
35
+ // just informational strings — PR CheckMate ignores them at runtime.
36
+ '// reporting': [
37
+ 'Results post to the PR/MR automatically.',
38
+ 'GitHub: no setup — the runner injects GITHUB_TOKEN; just keep `permissions: pull-requests: write` in the workflow.',
39
+ 'GitLab: create a token (Project → Settings → Access Tokens, scope `api`) and add it as CI/CD variable GITLAB_TOKEN.',
40
+ 'Bitbucket: create a token (Repo → Settings → Access tokens, scope pull-requests:write) and add it as repo variable BITBUCKET_TOKEN.',
41
+ 'The token lives in YOUR CI variables, never in this file. Without it, checks still run and print to the log — only the comment is skipped.',
42
+ ].join(' '),
31
43
  sourcePath: '.',
32
44
  // Directories excluded from every check (matched on any path segment). Edit
33
45
  // freely — add your own generated/vendored dirs or remove ones you want scanned.
@@ -94,6 +106,57 @@ const DEFAULT_CONFIG = {
94
106
  // test runner. Add barrels/entry points here to exempt intentional exports.
95
107
  ignoreFiles: config_1.DEAD_CODE_IGNORE_DEFAULTS,
96
108
  },
109
+ // User-defined rules: regex patterns run against added diff lines. The escape
110
+ // hatch to enforce any project rule without code. Example entry:
111
+ // { "name": "no-focused-tests", "pattern": "\\b(describe|it)\\.only\\(",
112
+ // "message": "focused test left in", "severity": "error",
113
+ // "include": ["src/**"], "exclude": ["**/*.test.*"] }
114
+ customRules: [],
115
+ // Universal per-check severity override, keyed by the check's display name (as
116
+ // shown in the report). "error" fails the run, "warn" is advisory, "off"
117
+ // disables the check. Empty by default — every check uses its built-in
118
+ // severity. Examples: { "Merge Conflict": "warn", "ESLint": "error" }.
119
+ severity: {},
120
+ // Git / PR-diff heuristics (delta-aware). Merge Conflict is blocking by
121
+ // default; the rest are advisory — promote any of them with `severity`.
122
+ mergeConflict: { enabled: true },
123
+ leftoverDebug: { enabled: true },
124
+ lockfileDrift: { enabled: true },
125
+ largeFiles: { enabled: true, maxSizeKb: 500, allow: [] },
126
+ missingTests: { enabled: true },
127
+ todoFixme: { enabled: true, requireTicket: false },
128
+ migrationSafety: { enabled: true },
129
+ // Security (regex diff scans + sensitive-file guard). Secret scan / npm audit
130
+ // live under `security`.
131
+ diffSecurity: { enabled: true },
132
+ sensitiveFileGuard: { enabled: true },
133
+ workflowSecurity: { enabled: true }, // GitHub Actions: injection, unpinned actions
134
+ dockerfileSecurity: { enabled: true }, // Dockerfile: :latest, USER root, curl|bash, …
135
+ caseCollision: { enabled: true }, // files differing only by case
136
+ vulnScan: { enabled: false }, // cross-language dep vulns (osv-scanner, runner-dependency)
137
+ // Test-coverage presence. Opt-in: enable once your CI produces a coverage
138
+ // report (lcov/cobertura/jacoco/…); warns if none is found.
139
+ coverage: { enabled: false },
140
+ // PR metadata / dependency hygiene.
141
+ prSize: { enabled: true, maxFiles: 50, maxLines: 1000 },
142
+ commitlint: { enabled: true },
143
+ // Dependencies you want to forbid. Example: { "name": "moment", "message": "use date-fns" }.
144
+ bannedImports: [],
145
+ // License/copyright header on new source files. Opt-in — set enabled + pattern.
146
+ licenseHeader: { enabled: false, pattern: '', lines: 10 },
147
+ outdatedDeps: { enabled: true, ignore: [] },
148
+ // Per-language checks. Bundled (no client install): TypeScript, Python (Ruff),
149
+ // C++ (clang-format). Runner-dependency (need the tool on the runner, skip
150
+ // gracefully if absent): Swift, Kotlin, Go, Rust, C#, Ruby, PHP.
151
+ python: { enabled: true, typecheck: true },
152
+ cpp: { enabled: true },
153
+ swift: { enabled: true },
154
+ kotlin: { enabled: true },
155
+ go: { enabled: true },
156
+ rust: { enabled: true },
157
+ csharp: { enabled: true },
158
+ ruby: { enabled: true },
159
+ php: { enabled: true },
97
160
  documentation: config_1.documentation,
98
161
  };
99
162
  const MANUAL_ENTRY = 'Enter manually';
@@ -24,6 +24,16 @@ export interface PRCheckMateConfig {
24
24
  sourcePath: string | string[];
25
25
  ignoreDirs?: string[];
26
26
  languages?: Language[];
27
+ customRules?: Array<{
28
+ name: string;
29
+ pattern: string;
30
+ message?: string;
31
+ severity?: 'error' | 'warn';
32
+ include?: string[];
33
+ exclude?: string[];
34
+ flags?: string;
35
+ }>;
36
+ severity?: Record<string, 'error' | 'warn' | 'off'>;
27
37
  lint?: {
28
38
  rules?: Record<string, any>;
29
39
  ignorePatterns?: string[];
@@ -60,10 +70,52 @@ export interface PRCheckMateConfig {
60
70
  lint?: boolean;
61
71
  format?: boolean;
62
72
  };
73
+ typecheck?: boolean;
74
+ typeChecker?: 'mypy' | 'pyright';
63
75
  };
64
76
  diffSmell?: {
65
77
  enabled?: boolean;
66
78
  };
79
+ prSize?: {
80
+ enabled?: boolean;
81
+ maxFiles?: number;
82
+ maxLines?: number;
83
+ };
84
+ leftoverDebug?: {
85
+ enabled?: boolean;
86
+ severity?: 'error' | 'warn';
87
+ };
88
+ todoFixme?: {
89
+ enabled?: boolean;
90
+ severity?: 'error' | 'warn';
91
+ requireTicket?: boolean;
92
+ keywords?: string[];
93
+ };
94
+ lockfileDrift?: {
95
+ enabled?: boolean;
96
+ severity?: 'error' | 'warn';
97
+ };
98
+ missingTests?: {
99
+ enabled?: boolean;
100
+ severity?: 'error' | 'warn';
101
+ ignore?: string[];
102
+ };
103
+ migrationSafety?: {
104
+ enabled?: boolean;
105
+ severity?: 'error' | 'warn';
106
+ paths?: string[];
107
+ };
108
+ coverage?: {
109
+ enabled?: boolean;
110
+ severity?: 'error' | 'warn';
111
+ reportPath?: string | string[];
112
+ };
113
+ largeFiles?: {
114
+ enabled?: boolean;
115
+ severity?: 'error' | 'warn';
116
+ maxSizeKb?: number;
117
+ allow?: string[];
118
+ };
67
119
  prBody?: {
68
120
  enabled?: boolean;
69
121
  minLength?: number;
@@ -122,6 +174,38 @@ export interface PRCheckMateConfig {
122
174
  };
123
175
  diffSecurity?: {
124
176
  enabled?: boolean;
177
+ ignore?: string[];
178
+ };
179
+ mergeConflict?: {
180
+ enabled?: boolean;
181
+ severity?: 'error' | 'warn';
182
+ };
183
+ workflowSecurity?: {
184
+ enabled?: boolean;
185
+ ignore?: string[];
186
+ };
187
+ dockerfileSecurity?: {
188
+ enabled?: boolean;
189
+ ignore?: string[];
190
+ };
191
+ caseCollision?: {
192
+ enabled?: boolean;
193
+ };
194
+ configValidation?: {
195
+ enabled?: boolean;
196
+ };
197
+ vulnScan?: {
198
+ enabled?: boolean;
199
+ };
200
+ bannedImports?: Array<{
201
+ name: string;
202
+ message?: string;
203
+ severity?: 'error' | 'warn';
204
+ }>;
205
+ licenseHeader?: {
206
+ enabled?: boolean;
207
+ pattern?: string;
208
+ lines?: number;
125
209
  };
126
210
  sensitiveFileGuard?: {
127
211
  enabled?: boolean;
@@ -132,6 +216,21 @@ export interface PRCheckMateConfig {
132
216
  kotlin?: {
133
217
  enabled?: boolean;
134
218
  };
219
+ go?: {
220
+ enabled?: boolean;
221
+ };
222
+ rust?: {
223
+ enabled?: boolean;
224
+ };
225
+ csharp?: {
226
+ enabled?: boolean;
227
+ };
228
+ ruby?: {
229
+ enabled?: boolean;
230
+ };
231
+ php?: {
232
+ enabled?: boolean;
233
+ };
135
234
  deadCode?: {
136
235
  enabled?: boolean;
137
236
  /**
@@ -17,6 +17,7 @@ const configFiles = [
17
17
  { source: 'src/config/cspell.json', dest: 'cspell.json' },
18
18
  { source: 'tsconfig.json', dest: 'tsconfig.json' },
19
19
  { source: 'src/config/pr-checkmate-workflow.yml', dest: 'dist/config/pr-checkmate-workflow.yml' },
20
+ { source: 'src/config/pr-checkmate.schema.json', dest: 'pr-checkmate.schema.json' },
20
21
  ];
21
22
  /**
22
23
  *
@@ -1,4 +1,4 @@
1
- export type Language = 'typescript' | 'javascript' | 'python' | 'cpp' | 'swift' | 'kotlin';
1
+ export type Language = 'typescript' | 'javascript' | 'python' | 'cpp' | 'swift' | 'kotlin' | 'go' | 'rust' | 'csharp' | 'ruby' | 'php';
2
2
  /**
3
3
  * Detects which languages a repository uses, combining marker files (definitive)
4
4
  * with tracked source-file counts (breadth). A language is reported when it has a