pr-checkmate 1.23.23 → 2.0.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.
- package/README.md +68 -15
- package/cspell.json +3 -8
- package/dist/cli.js +37 -62
- package/dist/config/constants.d.ts +21 -3
- package/dist/config/constants.js +33 -6
- package/dist/config/pr-checkmate-workflow.yml +33 -327
- package/dist/core/checks/dependencies/banned-imports.js +23 -24
- package/dist/core/checks/dependencies/circular-deps.js +1 -0
- package/dist/core/checks/dependencies/dependency-check.js +1 -0
- package/dist/core/checks/dependencies/grype-scan.js +1 -0
- package/dist/core/checks/dependencies/npm-audit.js +6 -1
- package/dist/core/checks/dependencies/vuln-scan.js +1 -0
- package/dist/core/checks/findings.d.ts +198 -19
- package/dist/core/checks/findings.js +474 -22
- package/dist/core/checks/git/custom-rules.js +21 -16
- package/dist/core/checks/git/diff-smell.d.ts +17 -0
- package/dist/core/checks/git/diff-smell.js +43 -24
- package/dist/core/checks/git/large-files.js +14 -2
- package/dist/core/checks/git/leftover-debug.js +23 -14
- package/dist/core/checks/git/lockfile-drift.js +2 -1
- package/dist/core/checks/git/merge-conflict.js +19 -8
- package/dist/core/checks/git/missing-tests.js +22 -6
- package/dist/core/checks/git/todo-fixme.js +10 -9
- package/dist/core/checks/languages/shellcheck.d.ts +8 -0
- package/dist/core/checks/languages/shellcheck.js +55 -0
- package/dist/core/checks/languages/typecheck.js +1 -0
- package/dist/core/checks/pr/pr-size-check.js +2 -1
- package/dist/core/checks/quality/config-validation.d.ts +14 -4
- package/dist/core/checks/quality/config-validation.js +119 -17
- package/dist/core/checks/quality/dead-code.js +1 -0
- package/dist/core/checks/quality/duplicate-check.js +1 -0
- package/dist/core/checks/quality/license-header.js +12 -2
- package/dist/core/checks/quality/spellcheck.d.ts +50 -0
- package/dist/core/checks/quality/spellcheck.js +141 -13
- package/dist/core/checks/quality/symlinks.d.ts +16 -0
- package/dist/core/checks/quality/symlinks.js +62 -0
- package/dist/core/checks/quality/yaml-lint.js +22 -9
- package/dist/core/checks/security/diff-security.js +111 -28
- package/dist/core/checks/security/dockerfile-security.js +23 -20
- package/dist/core/checks/security/migration-safety.js +19 -17
- package/dist/core/checks/security/sensitive-file-guard.js +37 -5
- package/dist/core/checks/security/workflow-security.d.ts +10 -0
- package/dist/core/checks/security/workflow-security.js +48 -26
- package/dist/core/commands.d.ts +27 -0
- package/dist/core/commands.js +91 -0
- package/dist/core/context.d.ts +12 -3
- package/dist/core/context.js +118 -16
- package/dist/core/install/cache.d.ts +39 -0
- package/dist/core/install/cache.js +99 -0
- package/dist/core/install/doctor.d.ts +35 -0
- package/dist/core/install/doctor.js +145 -0
- package/dist/core/install/ensure.d.ts +60 -0
- package/dist/core/install/ensure.js +169 -0
- package/dist/core/install/lock.d.ts +33 -0
- package/dist/core/install/lock.js +99 -0
- package/dist/core/install/profiles.d.ts +64 -0
- package/dist/core/install/profiles.js +170 -0
- package/dist/core/registry.js +4 -0
- package/dist/core/reporters.d.ts +13 -2
- package/dist/core/reporters.js +145 -104
- package/dist/core/run-checks.js +91 -14
- package/dist/core/targets.js +26 -4
- package/dist/core/types.d.ts +49 -1
- package/dist/init.d.ts +29 -14
- package/dist/init.js +204 -262
- package/dist/interfaces/index.d.ts +130 -0
- package/dist/scripts/generate-install-manifests.d.ts +39 -0
- package/dist/scripts/generate-install-manifests.js +121 -0
- package/dist/scripts/publish-github.js +18 -4
- package/dist/utils/detect-language.d.ts +37 -4
- package/dist/utils/detect-language.js +113 -19
- package/dist/utils/http.d.ts +59 -0
- package/dist/utils/http.js +119 -0
- package/dist/utils/logger.d.ts +52 -6
- package/dist/utils/logger.js +125 -33
- package/dist/utils/pool.d.ts +49 -0
- package/dist/utils/pool.js +124 -0
- package/dist/utils/update-notifier.d.ts +9 -0
- package/dist/utils/update-notifier.js +57 -3
- package/eslint.config.mjs +14 -2
- package/install-manifests/commitlint/package-lock.json +1028 -0
- package/install-manifests/commitlint/package.json +9 -0
- package/install-manifests/core/package-lock.json +1335 -0
- package/install-manifests/core/package.json +11 -0
- package/install-manifests/cpp/package-lock.json +29 -0
- package/install-manifests/cpp/package.json +8 -0
- package/install-manifests/duplicate/package-lock.json +113 -0
- package/install-manifests/duplicate/package.json +8 -0
- package/install-manifests/js/package-lock.json +5099 -0
- package/install-manifests/js/package.json +26 -0
- package/install-manifests/markdown/package-lock.json +1281 -0
- package/install-manifests/markdown/package.json +8 -0
- package/install-manifests/python/package-lock.json +21 -0
- package/install-manifests/python/package.json +8 -0
- package/install-manifests/spellcheck/package-lock.json +1047 -0
- package/install-manifests/spellcheck/package.json +8 -0
- package/package.json +13 -28
- package/pr-checkmate.schema.json +1070 -96
- package/src/config/cspell.json +3 -8
- package/src/config/pr-checkmate-workflow.yml +33 -327
- package/src/config/pr-checkmate.schema.json +1070 -96
package/dist/init.d.ts
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
+
/** How strictly the generated workflow treats a failed run. */
|
|
2
|
+
type GateMode = 'strict' | 'advisory';
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Write the bundled GitHub Actions workflow into the consumer repo.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
* Never overwrites an existing file. `init` is not a one-time command — the
|
|
7
|
+
* update notice asks people to re-run it after every upgrade — so clobbering
|
|
8
|
+
* would quietly discard whatever they had tuned, including the gate setting they
|
|
9
|
+
* were just asked about.
|
|
10
|
+
*/
|
|
11
|
+
export declare function setupWorkflow(root: string, mode: GateMode): void;
|
|
12
|
+
/**
|
|
13
|
+
* Every check that fails the run out of the box, written into the generated
|
|
14
|
+
* config at its real default.
|
|
15
|
+
*
|
|
16
|
+
* Not a copy of a default for its own sake — this one is the gate's teeth, and
|
|
17
|
+
* the client is entitled to see which checks can stop their merge without
|
|
18
|
+
* reading our source or discovering it from a red pipeline. Written as `"error"`
|
|
19
|
+
* so the edit is in place: change the value, or delete the line to inherit
|
|
20
|
+
* whatever we ship next.
|
|
10
21
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
22
|
+
* Two things are deliberately absent. `Custom Rules` and `Banned Imports` can
|
|
23
|
+
* block too, but only for rules the client writes themselves, so listing them
|
|
24
|
+
* would suggest a gate that is inert until they act. And `Dockerfile Security`
|
|
25
|
+
* looks like it belongs here and does not: every one of its patterns is rated
|
|
26
|
+
* `warn`, so it never fails on its own.
|
|
13
27
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* {@link REPLACED_OBJECT_PATHS}. Internal to the recursion; callers pass nothing.
|
|
28
|
+
* Keep in step with the checks' own defaults — `blocking-checks.test.ts` pins
|
|
29
|
+
* every name here to the registry, which catches a rename but not a change of
|
|
30
|
+
* heart about what should block.
|
|
18
31
|
*/
|
|
19
|
-
export declare
|
|
32
|
+
export declare const BLOCKING_BY_DEFAULT: readonly ["Security Scan", "Sensitive Files", "Diff Security", "Workflow Security", "Merge Conflict", "NPM Audit", "License Check", "Dependencies", "TypeScript", "ESLint", "YAML Lint"];
|
|
33
|
+
export declare function buildInitialConfig(sourcePath: string, languages: string[]): Record<string, unknown>;
|
|
20
34
|
/**
|
|
21
35
|
* Initialize (or refresh) pr-checkmate configuration in the current project.
|
|
22
36
|
*/
|
|
23
37
|
export declare function init(): Promise<void>;
|
|
38
|
+
export {};
|
package/dist/init.js
CHANGED
|
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.BLOCKING_BY_DEFAULT = void 0;
|
|
7
|
+
exports.setupWorkflow = setupWorkflow;
|
|
8
|
+
exports.buildInitialConfig = buildInitialConfig;
|
|
7
9
|
exports.init = init;
|
|
8
10
|
const fs_1 = __importDefault(require("fs"));
|
|
9
11
|
const path_1 = __importDefault(require("path"));
|
|
@@ -12,184 +14,11 @@ const config_1 = require("./config");
|
|
|
12
14
|
const utils_1 = require("./utils");
|
|
13
15
|
const CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
17
|
+
* Points editors at the bundled JSON schema. With the generated config reduced
|
|
18
|
+
* to the client's own decisions, this line is what makes every other option
|
|
19
|
+
* discoverable: the editor autocompletes each key and shows its description.
|
|
16
20
|
*/
|
|
17
|
-
const
|
|
18
|
-
// Points editors (VS Code, etc.) at the bundled JSON schema for autocomplete
|
|
19
|
-
// and inline validation of this file.
|
|
20
|
-
$schema: './node_modules/pr-checkmate/pr-checkmate.schema.json',
|
|
21
|
-
// Available CLI commands — kept at the very top as a quick reference.
|
|
22
|
-
// Informational only; PR CheckMate ignores this at runtime.
|
|
23
|
-
commands: config_1.DEFAULT_COMMANDS,
|
|
24
|
-
// Human-readable hints. JSON has no comments, so these `//`-prefixed keys are
|
|
25
|
-
// just informational strings — PR CheckMate ignores them at runtime.
|
|
26
|
-
'// reporting': [
|
|
27
|
-
'Results post to the PR/MR automatically.',
|
|
28
|
-
'GitHub: no setup — the runner injects GITHUB_TOKEN; just keep `permissions: pull-requests: write` in the workflow.',
|
|
29
|
-
'GitLab: create a token (Project → Settings → Access Tokens, scope `api`) and add it as CI/CD variable GITLAB_TOKEN.',
|
|
30
|
-
'Bitbucket: create a token (Repo → Settings → Access tokens, scope pull-requests:write) and add it as repo variable BITBUCKET_TOKEN.',
|
|
31
|
-
'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.',
|
|
32
|
-
].join(' '),
|
|
33
|
-
sourcePath: '.',
|
|
34
|
-
// Directories excluded from every check (matched on any path segment). Edit
|
|
35
|
-
// freely — add your own generated/vendored dirs or remove ones you want scanned.
|
|
36
|
-
ignoreDirs: config_1.IGNORED_DIRS,
|
|
37
|
-
// ESLint — `rules` starts EMPTY on purpose and holds overrides only, each merged
|
|
38
|
-
// over the bundled default for that one rule.
|
|
39
|
-
//
|
|
40
|
-
// It used to be written out in full (all 27 rules with their severities) so every
|
|
41
|
-
// rule was visible and editable in this file. That copy then won over the bundled
|
|
42
|
-
// defaults forever: `eslint.rules` is in REPLACED_OBJECT_PATHS, so a later `init`
|
|
43
|
-
// keeps the user's block wholesale, and improvements to the defaults never reached
|
|
44
|
-
// a repo that had ever run `init`. Downgrading `prettier/prettier` from error to
|
|
45
|
-
// warn — so formatting stops failing pipelines — is exactly such an improvement,
|
|
46
|
-
// and it stopped at the config boundary for every existing client.
|
|
47
|
-
//
|
|
48
|
-
// The full list with defaults lives in the ESLint check documentation and in the
|
|
49
|
-
// package's own eslint-default-rules.json; discoverability there costs nothing,
|
|
50
|
-
// un-updatable defaults cost every client an edit.
|
|
51
|
-
eslint: {
|
|
52
|
-
rules: {},
|
|
53
|
-
ignorePatterns: [],
|
|
54
|
-
},
|
|
55
|
-
// Prettier customization
|
|
56
|
-
prettier: {
|
|
57
|
-
rules: {},
|
|
58
|
-
},
|
|
59
|
-
// Spellcheck (cspell). `enabled: false` turns the check off — that part always
|
|
60
|
-
// applies. The lists feed the cspell config pr-checkmate generates, so they are
|
|
61
|
-
// ignored when the repo has a cspell.json of its own; all of them are appended to
|
|
62
|
-
// the bundled defaults. `ignorePaths` takes globs to skip (e.g. "CHANGELOG.md",
|
|
63
|
-
// "generated/**"), anchored to the repo root so a pattern never matches a
|
|
64
|
-
// directory above the checkout.
|
|
65
|
-
cspell: {
|
|
66
|
-
enabled: true,
|
|
67
|
-
words: [],
|
|
68
|
-
ignorePaths: [],
|
|
69
|
-
ignoreRegExpList: [],
|
|
70
|
-
},
|
|
71
|
-
// Markdownlint customization
|
|
72
|
-
markdownlint: {
|
|
73
|
-
rules: {},
|
|
74
|
-
ignores: [],
|
|
75
|
-
},
|
|
76
|
-
// YAML lint (bundled `yaml` parser). Reports syntax errors, duplicate keys,
|
|
77
|
-
// and tab/indentation problems in .yml/.yaml files. Blocking by default
|
|
78
|
-
// (severity "error"); set severity "warn" to make it advisory, or enabled
|
|
79
|
-
// false to disable. `ignore` takes glob patterns to skip (e.g. "charts/**").
|
|
80
|
-
yamlLint: {
|
|
81
|
-
enabled: true,
|
|
82
|
-
ignore: [],
|
|
83
|
-
},
|
|
84
|
-
// TypeScript type check
|
|
85
|
-
typecheck: {
|
|
86
|
-
enabled: true,
|
|
87
|
-
},
|
|
88
|
-
// Diff code smell detection
|
|
89
|
-
diffSmell: {
|
|
90
|
-
enabled: true,
|
|
91
|
-
},
|
|
92
|
-
// PR description validation
|
|
93
|
-
prBody: {
|
|
94
|
-
enabled: true,
|
|
95
|
-
minLength: 20,
|
|
96
|
-
requireTicket: true,
|
|
97
|
-
},
|
|
98
|
-
// Duplicate code detection (jscpd)
|
|
99
|
-
duplicate: {
|
|
100
|
-
enabled: true,
|
|
101
|
-
minLines: 5,
|
|
102
|
-
minTokens: 50,
|
|
103
|
-
// Test files/folders are excluded by default; edit to customise.
|
|
104
|
-
ignore: config_1.DUPLICATE_IGNORE_DEFAULTS,
|
|
105
|
-
// Only source-code formats are scanned. Data formats (yaml, json, markdown)
|
|
106
|
-
// are intentionally absent — add them here to detect duplication in them too.
|
|
107
|
-
formats: config_1.DUPLICATE_CODE_FORMATS,
|
|
108
|
-
},
|
|
109
|
-
// Dependency usage check (knip)
|
|
110
|
-
dependency: {
|
|
111
|
-
enabled: true,
|
|
112
|
-
// Dependency names to skip (e.g. CLI-only tools invoked as binaries,
|
|
113
|
-
// which knip can't see as imports and would report as unused).
|
|
114
|
-
ignore: [],
|
|
115
|
-
},
|
|
116
|
-
// Circular-import check (dpdm, bundled — nothing needed on the runner). Uses
|
|
117
|
-
// the project's own dpdm when it has one. Advisory by default (severity
|
|
118
|
-
// "warn"): most repos have long-standing cycles through barrel files, so see
|
|
119
|
-
// yours first, then set severity "error" to gate merges on them. enabled false
|
|
120
|
-
// disables the check; `ignore` takes glob patterns kept out of the analysis
|
|
121
|
-
// (generated or vendored code, or barrels whose cycles you tolerate). When the
|
|
122
|
-
// analyzer cannot run, the check skips rather than passing or failing.
|
|
123
|
-
circularDeps: {
|
|
124
|
-
enabled: true,
|
|
125
|
-
severity: 'warn',
|
|
126
|
-
ignore: [],
|
|
127
|
-
timeoutMs: 120000,
|
|
128
|
-
},
|
|
129
|
-
// Unused-exports check (ts-unused-exports)
|
|
130
|
-
deadCode: {
|
|
131
|
-
enabled: true,
|
|
132
|
-
// Regex sources matched against file paths; matching files are dropped from
|
|
133
|
-
// the report. Defaults to test/mock files, whose exports are used only by the
|
|
134
|
-
// test runner. Add barrels/entry points here to exempt intentional exports.
|
|
135
|
-
ignoreFiles: config_1.DEAD_CODE_IGNORE_DEFAULTS,
|
|
136
|
-
},
|
|
137
|
-
// User-defined rules: regex patterns run against added diff lines. The escape
|
|
138
|
-
// hatch to enforce any project rule without code. Example entry:
|
|
139
|
-
// { "name": "no-focused-tests", "pattern": "\\b(describe|it)\\.only\\(",
|
|
140
|
-
// "message": "focused test left in", "severity": "error",
|
|
141
|
-
// "include": ["src/**"], "exclude": ["**/*.test.*"] }
|
|
142
|
-
customRules: [],
|
|
143
|
-
// Universal per-check severity override, keyed by the check's display name (as
|
|
144
|
-
// shown in the report). "error" fails the run, "warn" is advisory, "off"
|
|
145
|
-
// disables the check. Empty by default — every check uses its built-in
|
|
146
|
-
// severity. Examples: { "Merge Conflict": "warn", "ESLint": "error" }.
|
|
147
|
-
severity: {},
|
|
148
|
-
// Git / PR-diff heuristics (delta-aware). Merge Conflict is blocking by
|
|
149
|
-
// default; the rest are advisory — promote any of them with `severity`.
|
|
150
|
-
mergeConflict: { enabled: true },
|
|
151
|
-
leftoverDebug: { enabled: true },
|
|
152
|
-
lockfileDrift: { enabled: true },
|
|
153
|
-
largeFiles: { enabled: true, maxSizeKb: 500, allow: [] },
|
|
154
|
-
missingTests: { enabled: true },
|
|
155
|
-
todoFixme: { enabled: true, requireTicket: false },
|
|
156
|
-
migrationSafety: { enabled: true },
|
|
157
|
-
// Security (regex diff scans + sensitive-file guard). Secret scan / npm audit
|
|
158
|
-
// live under `security`.
|
|
159
|
-
diffSecurity: { enabled: true },
|
|
160
|
-
sensitiveFileGuard: { enabled: true },
|
|
161
|
-
workflowSecurity: { enabled: true }, // GitHub Actions: injection, unpinned actions
|
|
162
|
-
dockerfileSecurity: { enabled: true }, // Dockerfile: :latest, USER root, curl|bash, …
|
|
163
|
-
caseCollision: { enabled: true }, // files differing only by case
|
|
164
|
-
vulnScan: { enabled: false }, // cross-language dep vulns (osv-scanner, runner-dependency)
|
|
165
|
-
// dep vulns (grype, non-blocking, runner-dependency; skips if grype absent).
|
|
166
|
-
// Never walks `ignoreDirs`; `timeoutMs` caps the scan so it cannot stall a commit.
|
|
167
|
-
grypeScan: { enabled: true, minSeverity: 'medium', timeoutMs: 120000 },
|
|
168
|
-
// Test-coverage presence. Opt-in: enable once your CI produces a coverage
|
|
169
|
-
// report (lcov/cobertura/jacoco/…); warns if none is found.
|
|
170
|
-
coverage: { enabled: false },
|
|
171
|
-
// PR metadata / dependency hygiene.
|
|
172
|
-
prSize: { enabled: true, maxFiles: 50, maxLines: 1000 },
|
|
173
|
-
commitlint: { enabled: true },
|
|
174
|
-
// Dependencies you want to forbid. Example: { "name": "moment", "message": "use date-fns" }.
|
|
175
|
-
bannedImports: [],
|
|
176
|
-
// License/copyright header on new source files. Opt-in — set enabled + pattern.
|
|
177
|
-
licenseHeader: { enabled: false, pattern: '', lines: 10 },
|
|
178
|
-
outdatedDeps: { enabled: true, ignore: [] },
|
|
179
|
-
// Per-language checks. Bundled (no client install): TypeScript, Python (Ruff),
|
|
180
|
-
// C++ (clang-format). Runner-dependency (need the tool on the runner, skip
|
|
181
|
-
// gracefully if absent): Swift, Kotlin, Go, Rust, C#, Ruby, PHP.
|
|
182
|
-
python: { enabled: true, typecheck: true },
|
|
183
|
-
cpp: { enabled: true },
|
|
184
|
-
swift: { enabled: true },
|
|
185
|
-
kotlin: { enabled: true },
|
|
186
|
-
go: { enabled: true },
|
|
187
|
-
rust: { enabled: true },
|
|
188
|
-
csharp: { enabled: true },
|
|
189
|
-
ruby: { enabled: true },
|
|
190
|
-
php: { enabled: true },
|
|
191
|
-
documentation: config_1.documentation,
|
|
192
|
-
};
|
|
21
|
+
const SCHEMA_PATH = './node_modules/pr-checkmate/pr-checkmate.schema.json';
|
|
193
22
|
const MANUAL_ENTRY = 'Enter manually';
|
|
194
23
|
/** Prompt the user for the project source path, allowing a manual entry. */
|
|
195
24
|
async function promptSourcePath() {
|
|
@@ -213,8 +42,55 @@ async function promptSourcePath() {
|
|
|
213
42
|
]);
|
|
214
43
|
return answers.sourcePath === MANUAL_ENTRY ? answers.manualPath : answers.sourcePath;
|
|
215
44
|
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Ask whether the generated workflow should block merges.
|
|
47
|
+
*
|
|
48
|
+
* Two levers decide strictness and they are not interchangeable: this one says
|
|
49
|
+
* whether a failed run means anything to GitHub at all, while `severity` in
|
|
50
|
+
* pr-checkmate.json says which findings count as a failure. The recommended way
|
|
51
|
+
* into an existing codebase is a strict workflow plus a permissive config — the
|
|
52
|
+
* gate is real from day one, and checks are promoted one at a time.
|
|
53
|
+
*/
|
|
54
|
+
async function promptGateMode() {
|
|
55
|
+
const { mode } = await inquirer_1.default.prompt([
|
|
56
|
+
{
|
|
57
|
+
type: 'select',
|
|
58
|
+
name: 'mode',
|
|
59
|
+
message: 'Should a failed check block the pull request?',
|
|
60
|
+
default: 'strict',
|
|
61
|
+
choices: [
|
|
62
|
+
{ name: 'Yes — block the merge (recommended)', value: 'strict' },
|
|
63
|
+
{ name: 'No — report only, never block', value: 'advisory' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
return mode;
|
|
68
|
+
}
|
|
69
|
+
/** The banner an advisory workflow carries, so it reads as a decision, not an accident. */
|
|
70
|
+
const ADVISORY_NOTICE = [
|
|
71
|
+
' # ADVISORY MODE — this run cannot block a merge.',
|
|
72
|
+
' #',
|
|
73
|
+
' # Chosen at `pr-checkmate init`. Reasonable while adopting the tool on an',
|
|
74
|
+
' # existing codebase, but it also means a detected secret does not stop the',
|
|
75
|
+
' # PR. To make this a real gate, delete the `continue-on-error` line below.',
|
|
76
|
+
' #',
|
|
77
|
+
' # Prefer the opposite arrangement: keep the gate real and relax individual',
|
|
78
|
+
' # checks in pr-checkmate.json instead —',
|
|
79
|
+
' # "severityDefaults": { "failOn": "warn" } everything advisory',
|
|
80
|
+
' # "severity": { "Security Scan": "error" } promote one back to a gate',
|
|
81
|
+
' # That way whatever you have already made clean stays enforced.',
|
|
82
|
+
].join('\n');
|
|
83
|
+
/** Indented to sit inside the step, not beside it — YAML is unforgiving here. */
|
|
84
|
+
const ADVISORY_FLAG = ' continue-on-error: true';
|
|
85
|
+
/**
|
|
86
|
+
* Write the bundled GitHub Actions workflow into the consumer repo.
|
|
87
|
+
*
|
|
88
|
+
* Never overwrites an existing file. `init` is not a one-time command — the
|
|
89
|
+
* update notice asks people to re-run it after every upgrade — so clobbering
|
|
90
|
+
* would quietly discard whatever they had tuned, including the gate setting they
|
|
91
|
+
* were just asked about.
|
|
92
|
+
*/
|
|
93
|
+
function setupWorkflow(root, mode) {
|
|
218
94
|
const workflowSource = path_1.default.join(__dirname, 'config', 'pr-checkmate-workflow.yml');
|
|
219
95
|
const workflowDir = path_1.default.join(root, '.github', 'workflows');
|
|
220
96
|
const workflowDest = path_1.default.join(workflowDir, 'pr-checkmate.yml');
|
|
@@ -222,13 +98,22 @@ function setupWorkflow(root) {
|
|
|
222
98
|
utils_1.logger.warn('[init]: ⚠️ Workflow template not found, skipping workflow setup');
|
|
223
99
|
return;
|
|
224
100
|
}
|
|
101
|
+
if (fs_1.default.existsSync(workflowDest)) {
|
|
102
|
+
utils_1.logger.info('[init]: ⏭️ .github/workflows/pr-checkmate.yml already exists — left untouched');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
225
105
|
if (!fs_1.default.existsSync(workflowDir)) {
|
|
226
106
|
fs_1.default.mkdirSync(workflowDir, { recursive: true });
|
|
227
107
|
}
|
|
228
|
-
fs_1.default.
|
|
229
|
-
|
|
230
|
-
|
|
108
|
+
const template = fs_1.default.readFileSync(workflowSource, 'utf-8');
|
|
109
|
+
const workflow = mode === 'advisory'
|
|
110
|
+
? template.replace(RUN_STEP_ANCHOR, `${ADVISORY_NOTICE}\n${RUN_STEP_ANCHOR}\n${ADVISORY_FLAG}`)
|
|
111
|
+
: template;
|
|
112
|
+
fs_1.default.writeFileSync(workflowDest, workflow);
|
|
113
|
+
utils_1.logger.info(`[init]: ✅ Created .github/workflows/pr-checkmate.yml (${mode === 'strict' ? 'blocking' : 'advisory'})`);
|
|
231
114
|
}
|
|
115
|
+
/** Where the advisory banner is spliced in — the step that runs the checks. */
|
|
116
|
+
const RUN_STEP_ANCHOR = ' - name: Run PR CheckMate';
|
|
232
117
|
/** Create a tsconfig.json scaffold if the consumer repo lacks one. */
|
|
233
118
|
function setupTsConfig(root, sourcePath) {
|
|
234
119
|
const tsconfigPath = path_1.default.join(root, 'tsconfig.json');
|
|
@@ -254,107 +139,164 @@ function setupTsConfig(root, sourcePath) {
|
|
|
254
139
|
fs_1.default.writeFileSync(tsconfigPath, JSON.stringify(newTsConfig, null, 2));
|
|
255
140
|
utils_1.logger.info('[init]: ✅ Created tsconfig.json using your sourcePath');
|
|
256
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The file `init` writes for a new project — the client's decisions, nothing else.
|
|
144
|
+
*
|
|
145
|
+
* Every other setting is deliberately absent. The config is deep-merged over the
|
|
146
|
+
* built-in defaults when it is loaded, so a key that repeats a default changes
|
|
147
|
+
* nothing: it only lengthens the file and freezes today's value in someone
|
|
148
|
+
* else's repository. Three hundred generated lines also read as a wall of
|
|
149
|
+
* settings to maintain, when there is nothing to maintain.
|
|
150
|
+
*
|
|
151
|
+
* Discoverability moves to two places that cannot go stale — `$schema`, which
|
|
152
|
+
* makes an editor autocomplete every option with its description, and the
|
|
153
|
+
* annotated reference shipped inside the package.
|
|
154
|
+
*/
|
|
155
|
+
/** The one-line answer to "how do I change this" — see {@link buildInitialConfig}. */
|
|
156
|
+
const REPORTING_HINT = [
|
|
157
|
+
'Results post to the PR/MR automatically.',
|
|
158
|
+
'GitHub: keep `permissions: pull-requests: write` AND pass the token to the step',
|
|
159
|
+
'(`env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}`) — Actions does not provide it',
|
|
160
|
+
'on its own, and without it the comment is silently skipped. The generated',
|
|
161
|
+
'workflow already does both.',
|
|
162
|
+
'GitLab: create a token (Project → Settings → Access Tokens, scope `api`) and add',
|
|
163
|
+
'it as CI/CD variable GITLAB_TOKEN.',
|
|
164
|
+
'Bitbucket: create a token (Repo → Settings → Access tokens, scope',
|
|
165
|
+
'pull-requests:write) and add it as repo variable BITBUCKET_TOKEN.',
|
|
166
|
+
'The token lives in YOUR CI variables, never in this file. Without it checks',
|
|
167
|
+
'still run and print to the log — only the comment is skipped.',
|
|
168
|
+
].join(' ');
|
|
169
|
+
/**
|
|
170
|
+
* Every check that fails the run out of the box, written into the generated
|
|
171
|
+
* config at its real default.
|
|
172
|
+
*
|
|
173
|
+
* Not a copy of a default for its own sake — this one is the gate's teeth, and
|
|
174
|
+
* the client is entitled to see which checks can stop their merge without
|
|
175
|
+
* reading our source or discovering it from a red pipeline. Written as `"error"`
|
|
176
|
+
* so the edit is in place: change the value, or delete the line to inherit
|
|
177
|
+
* whatever we ship next.
|
|
178
|
+
*
|
|
179
|
+
* Two things are deliberately absent. `Custom Rules` and `Banned Imports` can
|
|
180
|
+
* block too, but only for rules the client writes themselves, so listing them
|
|
181
|
+
* would suggest a gate that is inert until they act. And `Dockerfile Security`
|
|
182
|
+
* looks like it belongs here and does not: every one of its patterns is rated
|
|
183
|
+
* `warn`, so it never fails on its own.
|
|
184
|
+
*
|
|
185
|
+
* Keep in step with the checks' own defaults — `blocking-checks.test.ts` pins
|
|
186
|
+
* every name here to the registry, which catches a rename but not a change of
|
|
187
|
+
* heart about what should block.
|
|
188
|
+
*/
|
|
189
|
+
exports.BLOCKING_BY_DEFAULT = [
|
|
190
|
+
'Security Scan',
|
|
191
|
+
'Sensitive Files',
|
|
192
|
+
'Diff Security',
|
|
193
|
+
'Workflow Security',
|
|
194
|
+
'Merge Conflict',
|
|
195
|
+
'NPM Audit',
|
|
196
|
+
'License Check',
|
|
197
|
+
'Dependencies',
|
|
198
|
+
'TypeScript',
|
|
199
|
+
'ESLint',
|
|
200
|
+
'YAML Lint',
|
|
201
|
+
];
|
|
202
|
+
const HOW_TO = [
|
|
203
|
+
'Only what differs from the defaults belongs here — everything else is implicit',
|
|
204
|
+
'and stays current on upgrade, so this file stays short.',
|
|
205
|
+
'Turn a check off: "severity": { "Spellcheck": "off" }',
|
|
206
|
+
'Make one advisory: "severity": { "Sensitive Files": "warn" }',
|
|
207
|
+
'Make everything advisory while adopting: "severityDefaults": { "failOn": "warn" }',
|
|
208
|
+
'Skip a file or folder: "excludePaths": ["src/generated/**", "vendor/legacy.ts"]',
|
|
209
|
+
'Check names are the ones printed in the report.',
|
|
210
|
+
`Every option, annotated, with its default: ${config_1.FULL_CONFIG_URL}`,
|
|
211
|
+
`Docs: ${config_1.DOCS_URL}`,
|
|
212
|
+
].join(' ');
|
|
213
|
+
function buildInitialConfig(sourcePath, languages) {
|
|
214
|
+
return {
|
|
215
|
+
$schema: SCHEMA_PATH,
|
|
216
|
+
'// how-to': HOW_TO,
|
|
217
|
+
commands: config_1.DEFAULT_COMMANDS,
|
|
218
|
+
'// reporting': REPORTING_HINT,
|
|
219
|
+
sourcePath,
|
|
220
|
+
// What can stop a merge, and how to stand any of it down. "warn" keeps the
|
|
221
|
+
// finding in the report without failing the run; "off" removes the check.
|
|
222
|
+
severity: Object.fromEntries(exports.BLOCKING_BY_DEFAULT.map(name => [name, 'error'])),
|
|
223
|
+
// The knobs people reach for first, seeded EMPTY.
|
|
224
|
+
//
|
|
225
|
+
// Empty is the whole point: an empty container changes nothing, so it cannot
|
|
226
|
+
// freeze today's default in someone else's repository, but it puts the edit
|
|
227
|
+
// one line away instead of behind a lookup. Which blocks appear here is
|
|
228
|
+
// decided by what actually makes noise in real repositories — spelling,
|
|
229
|
+
// lint rules, generated files, knip's guesses about unused dependencies.
|
|
230
|
+
//
|
|
231
|
+
// Three blocks are deliberately NOT here, because for them an empty value is
|
|
232
|
+
// not neutral — it REPLACES a non-empty default and makes things worse:
|
|
233
|
+
// ignoreDirs → seeded empty, nothing is ignored at all
|
|
234
|
+
// duplicate.ignore → test boilerplate starts reporting as duplication
|
|
235
|
+
// deadCode.ignoreFiles → test helpers start reporting as unused exports
|
|
236
|
+
// Anyone who needs those sets them deliberately, with the defaults in view.
|
|
237
|
+
excludePaths: [],
|
|
238
|
+
cspell: { words: [], ignorePaths: [] },
|
|
239
|
+
eslint: { rules: {}, ignorePatterns: [] },
|
|
240
|
+
markdownlint: { ignores: [] },
|
|
241
|
+
dependency: { ignore: [] },
|
|
242
|
+
circularDeps: { ignore: [] },
|
|
243
|
+
yamlLint: { ignore: [] },
|
|
244
|
+
documentation: config_1.documentation,
|
|
245
|
+
languages,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
257
248
|
/** First-run setup: prompt, write config, and scaffold workflow + tsconfig. */
|
|
258
249
|
async function createConfig(root) {
|
|
259
250
|
const sourcePath = await promptSourcePath();
|
|
251
|
+
const gateMode = await promptGateMode();
|
|
260
252
|
const languages = await (0, utils_1.detectLanguages)(root);
|
|
261
253
|
utils_1.logger.info(`[init]: 🔍 Detected languages: ${languages.length ? languages.join(', ') : 'none'}`);
|
|
262
|
-
const config =
|
|
263
|
-
|
|
264
|
-
sourcePath,
|
|
265
|
-
languages,
|
|
266
|
-
};
|
|
267
|
-
// Omit config blocks for languages not present in the repo — a cleaner,
|
|
268
|
-
// tailored file. This does NOT disable them: the key is simply absent, which
|
|
269
|
-
// still defaults to enabled, and runtime language detection gates whether each
|
|
270
|
-
// check runs. So a language added later auto-activates without editing config.
|
|
271
|
-
const LANGUAGE_TOGGLE_KEYS = [
|
|
272
|
-
'python',
|
|
273
|
-
'cpp',
|
|
274
|
-
'swift',
|
|
275
|
-
'kotlin',
|
|
276
|
-
'go',
|
|
277
|
-
'rust',
|
|
278
|
-
'csharp',
|
|
279
|
-
'ruby',
|
|
280
|
-
'php',
|
|
281
|
-
];
|
|
282
|
-
for (const key of LANGUAGE_TOGGLE_KEYS) {
|
|
283
|
-
if (!languages.includes(key))
|
|
284
|
-
delete config[key];
|
|
285
|
-
}
|
|
286
|
-
fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
254
|
+
const config = buildInitialConfig(sourcePath, languages);
|
|
255
|
+
fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`);
|
|
287
256
|
utils_1.logger.info(`[init]: ✅ Created pr-checkmate.json with sourcePath: ${sourcePath}`);
|
|
288
257
|
utils_1.logger.info(`[init]: 💡 You can now customize ESLint rules, Prettier settings,
|
|
289
258
|
and cspell words in pr-checkmate.json`);
|
|
290
|
-
setupWorkflow(root);
|
|
259
|
+
setupWorkflow(root, gateMode);
|
|
291
260
|
setupTsConfig(root, sourcePath);
|
|
292
261
|
}
|
|
293
|
-
function isPlainObject(value) {
|
|
294
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
295
|
-
}
|
|
296
262
|
/**
|
|
297
|
-
*
|
|
298
|
-
* user is meant to add and remove entries, exactly like an array.
|
|
263
|
+
* Re-run setup: refresh the detected languages and leave everything else alone.
|
|
299
264
|
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* `init` would silently reinstate rules and start failing the next PR on them. The
|
|
304
|
-
* documented way to switch a rule off is `"off"`, which survives either way.
|
|
265
|
+
* It used to merge current defaults into the client's file, which is why the
|
|
266
|
+
* update notice insisted `init` must not be skipped after an upgrade. Defaults
|
|
267
|
+
* now apply when the config is loaded, so there is nothing to top up.
|
|
305
268
|
*/
|
|
306
|
-
const REPLACED_OBJECT_PATHS = new Set(['eslint.rules', 'prettier.rules', 'markdownlint.rules']);
|
|
307
|
-
/**
|
|
308
|
-
* Merge current defaults into an existing config, recursively.
|
|
309
|
-
*
|
|
310
|
-
* The user's value always wins, so hand-tuned settings survive an `init` re-run,
|
|
311
|
-
* while every new default field — at any nesting depth — gets added. Arrays are
|
|
312
|
-
* replaced wholesale rather than concatenated: lists like `ignoreDirs` or
|
|
313
|
-
* `cspell.words` are meant to be edited, and merging them would resurrect entries
|
|
314
|
-
* the user deliberately removed. {@link REPLACED_OBJECT_PATHS} extends that rule to
|
|
315
|
-
* the object-shaped lists.
|
|
316
|
-
*
|
|
317
|
-
* Informational `//`-prefixed keys are the one exception — they are documentation,
|
|
318
|
-
* not settings, so the refreshed default text wins whenever a default exists.
|
|
319
|
-
*
|
|
320
|
-
* @param defaults Current defaults for this level of the config tree.
|
|
321
|
-
* @param user The existing pr-checkmate.json content for the same level.
|
|
322
|
-
* @param path Dot-joined position of `defaults` in the config tree, used to match
|
|
323
|
-
* {@link REPLACED_OBJECT_PATHS}. Internal to the recursion; callers pass nothing.
|
|
324
|
-
*/
|
|
325
|
-
function mergeWithDefaults(defaults, user, path = '') {
|
|
326
|
-
const merged = { ...defaults };
|
|
327
|
-
for (const [key, userValue] of Object.entries(user)) {
|
|
328
|
-
const defaultValue = defaults[key];
|
|
329
|
-
const keyPath = path ? `${path}.${key}` : key;
|
|
330
|
-
if (key.startsWith('//') && defaultValue !== undefined)
|
|
331
|
-
continue;
|
|
332
|
-
merged[key] =
|
|
333
|
-
isPlainObject(defaultValue) && isPlainObject(userValue) && !REPLACED_OBJECT_PATHS.has(keyPath)
|
|
334
|
-
? mergeWithDefaults(defaultValue, userValue, keyPath)
|
|
335
|
-
: userValue;
|
|
336
|
-
}
|
|
337
|
-
return merged;
|
|
338
|
-
}
|
|
339
|
-
/** Re-run setup: merge the existing config with current defaults. */
|
|
340
269
|
async function updateConfig(root) {
|
|
341
270
|
utils_1.logger.info('[init]: pr-checkmate.json already exists — updating it.');
|
|
342
271
|
try {
|
|
343
272
|
const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
|
|
344
273
|
const json = JSON.parse(raw);
|
|
345
|
-
|
|
274
|
+
// Re-detection has to obey the config it is about to rewrite: a client who
|
|
275
|
+
// tuned `detect` (or `ignoreDirs`) precisely because detection was wrong
|
|
276
|
+
// would otherwise get the wrong answer written back over the right one on
|
|
277
|
+
// the next `init`.
|
|
278
|
+
const languages = await (0, utils_1.detectLanguages)(root, json.ignoreDirs, json.detect);
|
|
346
279
|
utils_1.logger.info(`[init]: 🔍 Detected languages: ${languages.length ? languages.join(', ') : 'none'}`);
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
280
|
+
// The client's file is left exactly as they wrote it. Defaults are no longer
|
|
281
|
+
// copied into it, because they never needed to be: they are merged in when
|
|
282
|
+
// the config is loaded, so a new default reaches every repository on upgrade
|
|
283
|
+
// without anyone editing anything. Writing them here only grew the file and
|
|
284
|
+
// made `init` a step nobody could safely skip after a release.
|
|
285
|
+
//
|
|
286
|
+
// `languages` is still refreshed — it is a fact about the repository, not a
|
|
287
|
+
// preference. `$schema` and the how-to hint are added when missing, because
|
|
288
|
+
// an existing config is exactly the case where nobody has seen them: the
|
|
289
|
+
// people who most need to know how to switch a check off are the ones whose
|
|
290
|
+
// file predates the advice.
|
|
350
291
|
const updated = {
|
|
351
|
-
|
|
352
|
-
//
|
|
292
|
+
$schema: json.$schema ?? SCHEMA_PATH,
|
|
293
|
+
'// how-to': json['// how-to'] ?? HOW_TO,
|
|
294
|
+
...json,
|
|
353
295
|
languages,
|
|
354
296
|
};
|
|
355
|
-
fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
|
|
356
|
-
utils_1.logger.info('[init]: 🔧
|
|
357
|
-
utils_1.logger.info(
|
|
297
|
+
fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
|
|
298
|
+
utils_1.logger.info('[init]: 🔧 Refreshed the detected languages in pr-checkmate.json.');
|
|
299
|
+
utils_1.logger.info(`[init]: 💡 Everything else is left as you wrote it — defaults apply on their own. Options: ${config_1.DOCS_URL}`);
|
|
358
300
|
}
|
|
359
301
|
catch (e) {
|
|
360
302
|
utils_1.logger.error(`[init]: ❌ Failed to update pr-checkmate.json: ${e}`);
|