pr-checkmate 1.23.24 → 2.1.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 +89 -13
- package/cspell.json +11 -9
- package/dist/cli.js +37 -64
- package/dist/config/constants.d.ts +51 -3
- package/dist/config/constants.js +73 -11
- package/dist/config/pr-checkmate-workflow-docker.yml +79 -0
- package/dist/config/pr-checkmate-workflow.yml +65 -326
- 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 +1 -1
- package/dist/core/checks/languages/shellcheck.js +1 -1
- 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/markdown-lint.js +7 -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/yaml-lint.js +22 -9
- package/dist/core/checks/security/diff-security.js +115 -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/reporters.d.ts +13 -2
- package/dist/core/reporters.js +145 -104
- package/dist/core/run-checks.d.ts +25 -1
- package/dist/core/run-checks.js +124 -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 +284 -263
- package/dist/interfaces/index.d.ts +120 -0
- package/dist/scripts/copy-configs.js +4 -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-default-rules.json +1 -1
- 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 +14 -28
- package/pr-checkmate.schema.json +1062 -104
- package/src/config/cspell.json +11 -9
- package/src/config/eslint-default-rules.json +1 -1
- package/src/config/pr-checkmate-workflow-docker.yml +79 -0
- package/src/config/pr-checkmate-workflow.yml +65 -326
- package/src/config/pr-checkmate.schema.json +1062 -104
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,22 +42,125 @@ async function promptSourcePath() {
|
|
|
213
42
|
]);
|
|
214
43
|
return answers.sourcePath === MANUAL_ENTRY ? answers.manualPath : answers.sourcePath;
|
|
215
44
|
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
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) {
|
|
94
|
+
// Which template a repository gets is decided by whether it actually installs
|
|
95
|
+
// us. Both are correct for their own case and wrong for the other:
|
|
96
|
+
//
|
|
97
|
+
// - a dependency means `npx pr-checkmate` runs the copy the lockfile pins,
|
|
98
|
+
// which is fast and reproducible;
|
|
99
|
+
// - no dependency means `npx` resolves to the newest release and downloads it
|
|
100
|
+
// on every job — 547 MB, about five minutes, and a major version can land
|
|
101
|
+
// without anyone changing a line.
|
|
102
|
+
//
|
|
103
|
+
// Asking the client to notice that difference in a comment is asking them to
|
|
104
|
+
// read a file they scaffolded precisely so they would not have to.
|
|
105
|
+
const source = installsPackage(root)
|
|
106
|
+
? 'pr-checkmate-workflow.yml'
|
|
107
|
+
: 'pr-checkmate-workflow-docker.yml';
|
|
108
|
+
const workflowSource = path_1.default.join(__dirname, 'config', source);
|
|
219
109
|
const workflowDir = path_1.default.join(root, '.github', 'workflows');
|
|
220
110
|
const workflowDest = path_1.default.join(workflowDir, 'pr-checkmate.yml');
|
|
221
111
|
if (!fs_1.default.existsSync(workflowSource)) {
|
|
222
112
|
utils_1.logger.warn('[init]: ⚠️ Workflow template not found, skipping workflow setup');
|
|
223
113
|
return;
|
|
224
114
|
}
|
|
115
|
+
if (fs_1.default.existsSync(workflowDest)) {
|
|
116
|
+
utils_1.logger.info('[init]: ⏭️ .github/workflows/pr-checkmate.yml already exists — left untouched');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
225
119
|
if (!fs_1.default.existsSync(workflowDir)) {
|
|
226
120
|
fs_1.default.mkdirSync(workflowDir, { recursive: true });
|
|
227
121
|
}
|
|
228
|
-
fs_1.default.
|
|
229
|
-
|
|
230
|
-
|
|
122
|
+
const template = fs_1.default.readFileSync(workflowSource, 'utf-8').replace(/__IMAGE_TAG__/g, imageTag());
|
|
123
|
+
const workflow = mode === 'advisory'
|
|
124
|
+
? template.replace(RUN_STEP_ANCHOR, `${ADVISORY_NOTICE}\n${RUN_STEP_ANCHOR}\n${ADVISORY_FLAG}`)
|
|
125
|
+
: template;
|
|
126
|
+
fs_1.default.writeFileSync(workflowDest, workflow);
|
|
127
|
+
utils_1.logger.info(`[init]: ✅ Created .github/workflows/pr-checkmate.yml (${mode === 'strict' ? 'blocking' : 'advisory'})`);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Whether this repository installs pr-checkmate rather than reaching for it on
|
|
131
|
+
* the pipeline. A missing or unreadable package.json answers "no", which is the
|
|
132
|
+
* safe way round: the container variant works either way, while the npm variant
|
|
133
|
+
* assumes an install that may not happen.
|
|
134
|
+
*/
|
|
135
|
+
function installsPackage(root) {
|
|
136
|
+
try {
|
|
137
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf-8'));
|
|
138
|
+
return Boolean(pkg.dependencies?.['pr-checkmate'] ?? pkg.devDependencies?.['pr-checkmate']);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The image tag written into the generated workflow: our own major.
|
|
146
|
+
*
|
|
147
|
+
* Pinned to a major rather than to `latest`, so patches and features arrive on
|
|
148
|
+
* their own while a change that can start failing builds does not. Pinned to a
|
|
149
|
+
* major rather than an exact version, so a client is not left on a release with
|
|
150
|
+
* a known problem because nobody remembered to bump a YAML file.
|
|
151
|
+
*/
|
|
152
|
+
function imageTag() {
|
|
153
|
+
try {
|
|
154
|
+
const manifest = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
155
|
+
const version = JSON.parse(fs_1.default.readFileSync(manifest, 'utf-8')).version;
|
|
156
|
+
return version.split('.')[0] || 'latest';
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return 'latest';
|
|
160
|
+
}
|
|
231
161
|
}
|
|
162
|
+
/** Where the advisory banner is spliced in — the step that runs the checks. */
|
|
163
|
+
const RUN_STEP_ANCHOR = ' - name: Run PR CheckMate';
|
|
232
164
|
/** Create a tsconfig.json scaffold if the consumer repo lacks one. */
|
|
233
165
|
function setupTsConfig(root, sourcePath) {
|
|
234
166
|
const tsconfigPath = path_1.default.join(root, 'tsconfig.json');
|
|
@@ -254,107 +186,196 @@ function setupTsConfig(root, sourcePath) {
|
|
|
254
186
|
fs_1.default.writeFileSync(tsconfigPath, JSON.stringify(newTsConfig, null, 2));
|
|
255
187
|
utils_1.logger.info('[init]: ✅ Created tsconfig.json using your sourcePath');
|
|
256
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* The file `init` writes for a new project — the client's decisions, nothing else.
|
|
191
|
+
*
|
|
192
|
+
* Every other setting is deliberately absent. The config is deep-merged over the
|
|
193
|
+
* built-in defaults when it is loaded, so a key that repeats a default changes
|
|
194
|
+
* nothing: it only lengthens the file and freezes today's value in someone
|
|
195
|
+
* else's repository. Three hundred generated lines also read as a wall of
|
|
196
|
+
* settings to maintain, when there is nothing to maintain.
|
|
197
|
+
*
|
|
198
|
+
* Discoverability moves to two places that cannot go stale — `$schema`, which
|
|
199
|
+
* makes an editor autocomplete every option with its description, and the
|
|
200
|
+
* annotated reference shipped inside the package.
|
|
201
|
+
*/
|
|
202
|
+
/** The one-line answer to "how do I change this" — see {@link buildInitialConfig}. */
|
|
203
|
+
const REPORTING_HINT = [
|
|
204
|
+
'Results post to the PR/MR automatically.',
|
|
205
|
+
'GitHub: keep `permissions: pull-requests: write` AND pass the token to the step',
|
|
206
|
+
'(`env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}`) — Actions does not provide it',
|
|
207
|
+
'on its own, and without it the comment is silently skipped. The generated',
|
|
208
|
+
'workflow already does both.',
|
|
209
|
+
'GitLab: create a token (Project → Settings → Access Tokens, scope `api`) and add',
|
|
210
|
+
'it as CI/CD variable GITLAB_TOKEN.',
|
|
211
|
+
'Bitbucket: create a token (Repo → Settings → Access tokens, scope',
|
|
212
|
+
'pull-requests:write) and add it as repo variable BITBUCKET_TOKEN.',
|
|
213
|
+
'The token lives in YOUR CI variables, never in this file. Without it checks',
|
|
214
|
+
'still run and print to the log — only the comment is skipped.',
|
|
215
|
+
].join(' ');
|
|
216
|
+
/**
|
|
217
|
+
* Every check that fails the run out of the box, written into the generated
|
|
218
|
+
* config at its real default.
|
|
219
|
+
*
|
|
220
|
+
* Not a copy of a default for its own sake — this one is the gate's teeth, and
|
|
221
|
+
* the client is entitled to see which checks can stop their merge without
|
|
222
|
+
* reading our source or discovering it from a red pipeline. Written as `"error"`
|
|
223
|
+
* so the edit is in place: change the value, or delete the line to inherit
|
|
224
|
+
* whatever we ship next.
|
|
225
|
+
*
|
|
226
|
+
* Two things are deliberately absent. `Custom Rules` and `Banned Imports` can
|
|
227
|
+
* block too, but only for rules the client writes themselves, so listing them
|
|
228
|
+
* would suggest a gate that is inert until they act. And `Dockerfile Security`
|
|
229
|
+
* looks like it belongs here and does not: every one of its patterns is rated
|
|
230
|
+
* `warn`, so it never fails on its own.
|
|
231
|
+
*
|
|
232
|
+
* Keep in step with the checks' own defaults — `blocking-checks.test.ts` pins
|
|
233
|
+
* every name here to the registry, which catches a rename but not a change of
|
|
234
|
+
* heart about what should block.
|
|
235
|
+
*/
|
|
236
|
+
exports.BLOCKING_BY_DEFAULT = [
|
|
237
|
+
'Security Scan',
|
|
238
|
+
'Sensitive Files',
|
|
239
|
+
'Diff Security',
|
|
240
|
+
'Workflow Security',
|
|
241
|
+
'Merge Conflict',
|
|
242
|
+
'NPM Audit',
|
|
243
|
+
'License Check',
|
|
244
|
+
'Dependencies',
|
|
245
|
+
'TypeScript',
|
|
246
|
+
'ESLint',
|
|
247
|
+
'YAML Lint',
|
|
248
|
+
];
|
|
249
|
+
const HOW_TO = [
|
|
250
|
+
'Only what differs from the defaults belongs here — everything else is implicit',
|
|
251
|
+
'and stays current on upgrade, so this file stays short.',
|
|
252
|
+
'Turn a check off: "severity": { "Spellcheck": "off" }',
|
|
253
|
+
'Make one advisory: "severity": { "Sensitive Files": "warn" }',
|
|
254
|
+
'Make everything advisory while adopting: "severityDefaults": { "failOn": "warn" }',
|
|
255
|
+
'Skip a file or folder: "excludePaths": ["src/generated/**", "vendor/legacy.ts"]',
|
|
256
|
+
'Check names are the ones printed in the report.',
|
|
257
|
+
`Every option, annotated, with its default: ${config_1.FULL_CONFIG_URL}`,
|
|
258
|
+
`Docs: ${config_1.DOCS_URL}`,
|
|
259
|
+
].join(' ');
|
|
260
|
+
function buildInitialConfig(sourcePath, languages) {
|
|
261
|
+
return {
|
|
262
|
+
$schema: SCHEMA_PATH,
|
|
263
|
+
'// how-to': HOW_TO,
|
|
264
|
+
commands: config_1.DEFAULT_COMMANDS,
|
|
265
|
+
'// reporting': REPORTING_HINT,
|
|
266
|
+
sourcePath,
|
|
267
|
+
// Which checks can stop a merge — named, but NOT written as values.
|
|
268
|
+
//
|
|
269
|
+
// Writing `"Workflow Security": "error"` reads like a statement of the
|
|
270
|
+
// default and is not one: the map promotes as well as demotes, so an entry
|
|
271
|
+
// of "error" turns that check's ADVISORY findings into failures too. On a
|
|
272
|
+
// real client repository that flipped 54 warnings into a red build. The
|
|
273
|
+
// names belong in a comment; the map stays empty until the client means
|
|
274
|
+
// something by it.
|
|
275
|
+
'// severity': [
|
|
276
|
+
`These checks fail a run by default: ${exports.BLOCKING_BY_DEFAULT.join(', ')}.`,
|
|
277
|
+
'To stand one down, add it below with "warn" (stays in the report, stops',
|
|
278
|
+
'blocking) or "off" (does not run at all).',
|
|
279
|
+
'Adding a check with "error" is not a restatement of the default — it also',
|
|
280
|
+
"PROMOTES that check's advisory findings to failures, which is stricter.",
|
|
281
|
+
].join(' '),
|
|
282
|
+
severity: {},
|
|
283
|
+
// The knobs people reach for first.
|
|
284
|
+
//
|
|
285
|
+
// Two shapes appear here, and the difference is not cosmetic. A list whose
|
|
286
|
+
// values are APPENDED to the defaults is seeded empty: empty changes nothing,
|
|
287
|
+
// so it cannot freeze today's default in someone else's repository. A list
|
|
288
|
+
// that REPLACES its default is written out in full instead — seeded empty it
|
|
289
|
+
// would silently switch the default off, and left out entirely it would be
|
|
290
|
+
// invisible, which is how a repository ends up spellchecking its own
|
|
291
|
+
// CHANGELOG and reporting every test fixture as duplication.
|
|
292
|
+
//
|
|
293
|
+
// Written-out lists are a deliberate trade: they stop tracking the package,
|
|
294
|
+
// so a later release that adds a directory to the defaults will not reach a
|
|
295
|
+
// config that already spells the list out. That is the price of being able
|
|
296
|
+
// to see and edit the value, and it is the one people asked for.
|
|
297
|
+
excludePaths: [],
|
|
298
|
+
// REPLACES the default. Copied out so the list is editable in place —
|
|
299
|
+
// note that ESLint does not read it; ESLint honours `eslint.ignorePatterns`.
|
|
300
|
+
ignoreDirs: [...config_1.IGNORED_DIRS],
|
|
301
|
+
// Spelling: `words` and `ignorePaths` are both APPENDED to the bundled lists,
|
|
302
|
+
// so both stay empty. What the bundled list already skips is spelled out in
|
|
303
|
+
// the sibling comment rather than duplicated here.
|
|
304
|
+
'// cspell': `Already skipped by default: ${config_1.GENERATED_DOC_FILES.join(', ')}, test and mock paths, lockfiles, minified bundles, and every directory in "ignoreDirs". Anything listed below is ADDED to that.`,
|
|
305
|
+
cspell: { words: [], ignorePaths: [] },
|
|
306
|
+
// `max-len` is written out at its default rather than left implicit: it is
|
|
307
|
+
// the rule people hit first and tune first, and a line limit is a house
|
|
308
|
+
// style, not a defect — arguing with it through documentation wastes
|
|
309
|
+
// everyone's time when the number could just be sitting here, editable.
|
|
310
|
+
eslint: { rules: { 'max-len': ['warn', { code: 300 }] }, ignorePatterns: [] },
|
|
311
|
+
markdownlint: { ignores: [] },
|
|
312
|
+
dependency: { ignore: [] },
|
|
313
|
+
circularDeps: { ignore: [] },
|
|
314
|
+
yamlLint: { ignore: [] },
|
|
315
|
+
// The three lists that REPLACE their defaults, written out. Each one exists
|
|
316
|
+
// because test code trips a check that production code does not: suites
|
|
317
|
+
// repeat their setup, the runner is the only consumer of what they export,
|
|
318
|
+
// and a security test's whole job may be to call the dangerous function.
|
|
319
|
+
// Delete an entry to have that check start reporting on tests again.
|
|
320
|
+
duplicate: { ignore: [...config_1.DUPLICATE_IGNORE_DEFAULTS] },
|
|
321
|
+
deadCode: { ignoreFiles: [...config_1.DEAD_CODE_IGNORE_DEFAULTS] },
|
|
322
|
+
diffSecurity: { ignorePaths: [...config_1.DIFF_SECURITY_IGNORE_DEFAULTS] },
|
|
323
|
+
documentation: config_1.documentation,
|
|
324
|
+
languages,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
257
327
|
/** First-run setup: prompt, write config, and scaffold workflow + tsconfig. */
|
|
258
328
|
async function createConfig(root) {
|
|
259
329
|
const sourcePath = await promptSourcePath();
|
|
330
|
+
const gateMode = await promptGateMode();
|
|
260
331
|
const languages = await (0, utils_1.detectLanguages)(root);
|
|
261
332
|
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));
|
|
333
|
+
const config = buildInitialConfig(sourcePath, languages);
|
|
334
|
+
fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`);
|
|
287
335
|
utils_1.logger.info(`[init]: ✅ Created pr-checkmate.json with sourcePath: ${sourcePath}`);
|
|
288
336
|
utils_1.logger.info(`[init]: 💡 You can now customize ESLint rules, Prettier settings,
|
|
289
337
|
and cspell words in pr-checkmate.json`);
|
|
290
|
-
setupWorkflow(root);
|
|
338
|
+
setupWorkflow(root, gateMode);
|
|
291
339
|
setupTsConfig(root, sourcePath);
|
|
292
340
|
}
|
|
293
|
-
function isPlainObject(value) {
|
|
294
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
295
|
-
}
|
|
296
341
|
/**
|
|
297
|
-
*
|
|
298
|
-
* user is meant to add and remove entries, exactly like an array.
|
|
342
|
+
* Re-run setup: refresh the detected languages and leave everything else alone.
|
|
299
343
|
*
|
|
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.
|
|
344
|
+
* It used to merge current defaults into the client's file, which is why the
|
|
345
|
+
* update notice insisted `init` must not be skipped after an upgrade. Defaults
|
|
346
|
+
* now apply when the config is loaded, so there is nothing to top up.
|
|
305
347
|
*/
|
|
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
348
|
async function updateConfig(root) {
|
|
341
349
|
utils_1.logger.info('[init]: pr-checkmate.json already exists — updating it.');
|
|
342
350
|
try {
|
|
343
351
|
const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
|
|
344
352
|
const json = JSON.parse(raw);
|
|
345
|
-
|
|
353
|
+
// Re-detection has to obey the config it is about to rewrite: a client who
|
|
354
|
+
// tuned `detect` (or `ignoreDirs`) precisely because detection was wrong
|
|
355
|
+
// would otherwise get the wrong answer written back over the right one on
|
|
356
|
+
// the next `init`.
|
|
357
|
+
const languages = await (0, utils_1.detectLanguages)(root, json.ignoreDirs, json.detect);
|
|
346
358
|
utils_1.logger.info(`[init]: 🔍 Detected languages: ${languages.length ? languages.join(', ') : 'none'}`);
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
359
|
+
// The client's file is left exactly as they wrote it. Defaults are no longer
|
|
360
|
+
// copied into it, because they never needed to be: they are merged in when
|
|
361
|
+
// the config is loaded, so a new default reaches every repository on upgrade
|
|
362
|
+
// without anyone editing anything. Writing them here only grew the file and
|
|
363
|
+
// made `init` a step nobody could safely skip after a release.
|
|
364
|
+
//
|
|
365
|
+
// `languages` is still refreshed — it is a fact about the repository, not a
|
|
366
|
+
// preference. `$schema` and the how-to hint are added when missing, because
|
|
367
|
+
// an existing config is exactly the case where nobody has seen them: the
|
|
368
|
+
// people who most need to know how to switch a check off are the ones whose
|
|
369
|
+
// file predates the advice.
|
|
350
370
|
const updated = {
|
|
351
|
-
|
|
352
|
-
//
|
|
371
|
+
$schema: json.$schema ?? SCHEMA_PATH,
|
|
372
|
+
'// how-to': json['// how-to'] ?? HOW_TO,
|
|
373
|
+
...json,
|
|
353
374
|
languages,
|
|
354
375
|
};
|
|
355
|
-
fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
|
|
356
|
-
utils_1.logger.info('[init]: 🔧
|
|
357
|
-
utils_1.logger.info(
|
|
376
|
+
fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
|
|
377
|
+
utils_1.logger.info('[init]: 🔧 Refreshed the detected languages in pr-checkmate.json.');
|
|
378
|
+
utils_1.logger.info(`[init]: 💡 Everything else is left as you wrote it — defaults apply on their own. Options: ${config_1.DOCS_URL}`);
|
|
358
379
|
}
|
|
359
380
|
catch (e) {
|
|
360
381
|
utils_1.logger.error(`[init]: ❌ Failed to update pr-checkmate.json: ${e}`);
|