pr-checkmate 2.0.0 → 2.1.2
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 +29 -2
- package/cspell.json +8 -1
- package/dist/config/constants.d.ts +37 -0
- package/dist/config/constants.js +53 -6
- package/dist/config/pr-checkmate-workflow-docker.yml +79 -0
- package/dist/config/pr-checkmate-workflow.yml +33 -0
- package/dist/core/checks/dependencies/dependency-check.js +1 -1
- package/dist/core/checks/dependencies/license-check.js +1 -6
- package/dist/core/checks/dependencies/npm-audit.js +1 -1
- package/dist/core/checks/dependencies/outdated-deps.js +1 -6
- package/dist/core/checks/quality/duplicate-check.js +33 -2
- package/dist/core/checks/quality/markdown-lint.js +7 -2
- package/dist/core/checks/security/diff-security.js +5 -1
- package/dist/core/install/doctor.d.ts +2 -2
- package/dist/core/install/doctor.js +8 -16
- package/dist/core/reporters.js +8 -2
- package/dist/core/run-checks.d.ts +25 -1
- package/dist/core/run-checks.js +37 -4
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.js +28 -0
- package/dist/init.js +97 -18
- package/dist/scripts/copy-configs.js +4 -0
- package/dist/utils/detect-language.js +11 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/package-version.d.ts +4 -0
- package/dist/utils/package-version.js +41 -0
- package/dist/utils/update-notifier.js +5 -7
- package/eslint-default-rules.json +1 -1
- package/package.json +2 -1
- package/src/config/cspell.json +8 -1
- 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 +33 -0
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|

|
|
11
11
|

|
|
12
12
|
[](https://pr-checkmate-docs.pages.dev)
|
|
13
|
+
[](https://github.com/users/Mybono/packages/container/package/pr-checkmate)
|
|
13
14
|
|
|
14
15
|
> PR CheckMate is a security-first PR gate: secret scanning, dependency
|
|
15
16
|
> vulnerability checks, and a signed SBOM + independent VirusTotal scan on
|
|
@@ -121,13 +122,39 @@ npx pr-checkmate all --full
|
|
|
121
122
|
npx pr-checkmate lint --full
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
Only three checks genuinely need a diff and drop out of a full scan — PR Size, Missing
|
|
126
|
+
Tests and Lockfile Drift. Everything else, the whole security family included, scans the
|
|
127
|
+
working tree when there is no diff to read. The
|
|
126
128
|
[per-check reference](https://github.com/Mybono/pr_checkmate_public/blob/main/documentation/checks/INDEX.md)
|
|
127
129
|
spells out the rules under "Review scope".
|
|
128
130
|
|
|
129
131
|
### Running in CI without installing
|
|
130
132
|
|
|
133
|
+
**Use the container.** With no `node_modules` to reuse, a cold `npx` downloads 547 MB and
|
|
134
|
+
takes about five minutes — on every job. The image carries the whole toolchain already:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
docker run --rm -v "$PWD:/repo" --user "$(id -u):$(id -g)" \
|
|
138
|
+
ghcr.io/mybono/pr-checkmate:latest all
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
161 MB pulled once and cached by the runner, and the checks that need a binary on the
|
|
142
|
+
machine — ShellCheck among them — are inside, so they stop silently skipping. Pin the
|
|
143
|
+
version tag (`:2.0.0`) for a reproducible pipeline; `--user` keeps files that formatters
|
|
144
|
+
rewrite owned by you rather than by root.
|
|
145
|
+
|
|
146
|
+
Verify what you pulled — the image carries SLSA provenance and an SBOM, attached to
|
|
147
|
+
the image itself in the registry:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
docker buildx imagetools inspect ghcr.io/mybono/pr-checkmate:2 \
|
|
151
|
+
--format '{{ json .Provenance }}'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
[All tags →](https://github.com/users/Mybono/packages/container/package/pr-checkmate)
|
|
155
|
+
|
|
156
|
+
The `npx` route still works, and the rest of this section is about making it comfortable.
|
|
157
|
+
|
|
131
158
|
Plenty of repos never add pr-checkmate to `package.json` and call it on the fly in CI
|
|
132
159
|
instead (`npx pr-checkmate`). That works, but there is no `node_modules` to generate a
|
|
133
160
|
config from, so the run silently falls back to the defaults with no file to tune.
|
package/cspell.json
CHANGED
|
@@ -1752,7 +1752,14 @@
|
|
|
1752
1752
|
"/**/yarn.lock",
|
|
1753
1753
|
"/**/pnpm-lock.yaml",
|
|
1754
1754
|
"/**/*.min.js",
|
|
1755
|
-
"/**/*.min.css"
|
|
1755
|
+
"/**/*.min.css",
|
|
1756
|
+
"/CHANGELOG.md",
|
|
1757
|
+
"/**/__tests__/**",
|
|
1758
|
+
"/**/__mocks__/**",
|
|
1759
|
+
"/**/*.test.*",
|
|
1760
|
+
"/**/*.spec.*",
|
|
1761
|
+
"/**/test/**",
|
|
1762
|
+
"/**/tests/**"
|
|
1756
1763
|
],
|
|
1757
1764
|
"enableFiletypes": ["typescript", "javascript", "markdown", "json"],
|
|
1758
1765
|
"ignoreRegExpList": ["/https?:\\/\\/[^\\s]+/g", "/\\b[0-9a-f]{7,40}\\b/gi"]
|
|
@@ -17,6 +17,18 @@ export declare const IGNORED_DIRS: string[];
|
|
|
17
17
|
* nested report globs that are not plain directory names.
|
|
18
18
|
*/
|
|
19
19
|
export declare const IGNORE_PATTERNS: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Where test code lives, as globs.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately NOT part of {@link IGNORED_DIRS}: that list hides a directory from
|
|
24
|
+
* every check, and test code still wants linting, type-checking and formatting.
|
|
25
|
+
* What test code does not want is the handful of checks whose findings are noise
|
|
26
|
+
* on it — duplication (setup boilerplate repeats by design), unused exports (the
|
|
27
|
+
* runner consumes them), spelling (fixture identifiers) and the diff security
|
|
28
|
+
* scan (a test's whole job may be to call the dangerous thing). Each of those
|
|
29
|
+
* defaults to this list, and each lets a client replace it.
|
|
30
|
+
*/
|
|
31
|
+
export declare const TEST_PATH_GLOBS: string[];
|
|
20
32
|
/**
|
|
21
33
|
* Test files/folders excluded from duplicate-code detection by default.
|
|
22
34
|
* Test suites legitimately repeat setup/arrange/assert boilerplate, so counting
|
|
@@ -24,6 +36,24 @@ export declare const IGNORE_PATTERNS: string[];
|
|
|
24
36
|
* override this list via `duplicate.ignore` in pr-checkmate.json.
|
|
25
37
|
*/
|
|
26
38
|
export declare const DUPLICATE_IGNORE_DEFAULTS: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Paths the diff security scan skips by default.
|
|
41
|
+
*
|
|
42
|
+
* A security test corpus is built out of the exact constructs the scan hunts for
|
|
43
|
+
* — `eval()`, `document.write()`, `__proto__` — so scanning it turns every added
|
|
44
|
+
* test case into a finding, and under `severity: error` into a blocked merge.
|
|
45
|
+
* Clients can override via `diffSecurity.ignorePaths` (which REPLACES this list).
|
|
46
|
+
*/
|
|
47
|
+
export declare const DIFF_SECURITY_IGNORE_DEFAULTS: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Files written by a release tool rather than by a person.
|
|
50
|
+
*
|
|
51
|
+
* `CHANGELOG.md` is assembled from commit subjects, so every typo anyone has ever
|
|
52
|
+
* committed reappears in it — 39 of 42 spellcheck findings in one real repository
|
|
53
|
+
* came from that one file, none of them actionable: the text cannot be corrected
|
|
54
|
+
* without rewriting git history, and the next release regenerates it anyway.
|
|
55
|
+
*/
|
|
56
|
+
export declare const GENERATED_DOC_FILES: string[];
|
|
27
57
|
/**
|
|
28
58
|
* Files excluded from the dead-code (unused-exports) report by default. These are
|
|
29
59
|
* REGEX sources (matched against each reported file path), not globs — dead-code's
|
|
@@ -40,6 +70,13 @@ export declare const CONFIG_FILE_PATTERNS: string[];
|
|
|
40
70
|
* config/CI YAML and JSON out of the report — they are data, not code, and their
|
|
41
71
|
* repetition is not meaningful duplication. This mirrors the code-only file set the
|
|
42
72
|
* check already uses when scanning a git diff, so full-tree and diff scans agree.
|
|
73
|
+
*
|
|
74
|
+
* One entry per language pr-checkmate detects, all of them verified against the
|
|
75
|
+
* jscpd binary rather than assumed: the list used to stop at C++ while the check
|
|
76
|
+
* only ever handed jscpd `.ts`/`.js` files, so the Go, Rust, C#, Ruby and PHP
|
|
77
|
+
* halves were unreachable and the Python one was decoration. Copy-paste is a
|
|
78
|
+
* language-independent smell, and the tokenizer behind this check has always been
|
|
79
|
+
* able to see it — the file list was the only thing in the way.
|
|
43
80
|
*/
|
|
44
81
|
export declare const DUPLICATE_CODE_FORMATS: string[];
|
|
45
82
|
/**
|
package/dist/config/constants.js
CHANGED
|
@@ -3,7 +3,7 @@ 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.KNOWN_CONFIG_KEYS = exports.CONFIG_FILE = exports.documentation = exports.FULL_CONFIG_URL = exports.DOCS_URL = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.DUPLICATE_CODE_FORMATS = exports.CONFIG_FILE_PATTERNS = exports.DEAD_CODE_IGNORE_DEFAULTS = exports.DUPLICATE_IGNORE_DEFAULTS = exports.IGNORE_PATTERNS = exports.IGNORED_DIRS = void 0;
|
|
6
|
+
exports.KNOWN_CONFIG_KEYS = exports.CONFIG_FILE = exports.documentation = exports.FULL_CONFIG_URL = exports.DOCS_URL = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.DUPLICATE_CODE_FORMATS = exports.CONFIG_FILE_PATTERNS = exports.DEAD_CODE_IGNORE_DEFAULTS = exports.GENERATED_DOC_FILES = exports.DIFF_SECURITY_IGNORE_DEFAULTS = exports.DUPLICATE_IGNORE_DEFAULTS = exports.TEST_PATH_GLOBS = exports.IGNORE_PATTERNS = exports.IGNORED_DIRS = void 0;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
/**
|
|
9
9
|
* Build output, dependency, tooling, and report directories that are never
|
|
@@ -86,6 +86,11 @@ exports.IGNORED_DIRS = [
|
|
|
86
86
|
'allure-results',
|
|
87
87
|
'playwright-report',
|
|
88
88
|
'.wdio',
|
|
89
|
+
// Generated knowledge-graph output. Committed by repos that keep the graph in
|
|
90
|
+
// version control, and machine-written from end to end: a multi-megabyte
|
|
91
|
+
// `graph.json` trips the large-file check, and `GRAPH_REPORT.md` carries every
|
|
92
|
+
// identifier in the codebase past the spellchecker. Neither is anyone's source.
|
|
93
|
+
'graphify-out',
|
|
89
94
|
];
|
|
90
95
|
/**
|
|
91
96
|
* Glob form of the ignore list, for tools that take glob ignores directly
|
|
@@ -110,12 +115,17 @@ exports.IGNORE_PATTERNS = [
|
|
|
110
115
|
'**/audit-report/**',
|
|
111
116
|
];
|
|
112
117
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
118
|
+
* Where test code lives, as globs.
|
|
119
|
+
*
|
|
120
|
+
* Deliberately NOT part of {@link IGNORED_DIRS}: that list hides a directory from
|
|
121
|
+
* every check, and test code still wants linting, type-checking and formatting.
|
|
122
|
+
* What test code does not want is the handful of checks whose findings are noise
|
|
123
|
+
* on it — duplication (setup boilerplate repeats by design), unused exports (the
|
|
124
|
+
* runner consumes them), spelling (fixture identifiers) and the diff security
|
|
125
|
+
* scan (a test's whole job may be to call the dangerous thing). Each of those
|
|
126
|
+
* defaults to this list, and each lets a client replace it.
|
|
117
127
|
*/
|
|
118
|
-
exports.
|
|
128
|
+
exports.TEST_PATH_GLOBS = [
|
|
119
129
|
'**/__tests__/**',
|
|
120
130
|
'**/__mocks__/**',
|
|
121
131
|
'**/*.test.*',
|
|
@@ -123,6 +133,31 @@ exports.DUPLICATE_IGNORE_DEFAULTS = [
|
|
|
123
133
|
'**/test/**',
|
|
124
134
|
'**/tests/**',
|
|
125
135
|
];
|
|
136
|
+
/**
|
|
137
|
+
* Test files/folders excluded from duplicate-code detection by default.
|
|
138
|
+
* Test suites legitimately repeat setup/arrange/assert boilerplate, so counting
|
|
139
|
+
* them as clones is noise that drowns out real production duplication. Clients can
|
|
140
|
+
* override this list via `duplicate.ignore` in pr-checkmate.json.
|
|
141
|
+
*/
|
|
142
|
+
exports.DUPLICATE_IGNORE_DEFAULTS = [...exports.TEST_PATH_GLOBS];
|
|
143
|
+
/**
|
|
144
|
+
* Paths the diff security scan skips by default.
|
|
145
|
+
*
|
|
146
|
+
* A security test corpus is built out of the exact constructs the scan hunts for
|
|
147
|
+
* — `eval()`, `document.write()`, `__proto__` — so scanning it turns every added
|
|
148
|
+
* test case into a finding, and under `severity: error` into a blocked merge.
|
|
149
|
+
* Clients can override via `diffSecurity.ignorePaths` (which REPLACES this list).
|
|
150
|
+
*/
|
|
151
|
+
exports.DIFF_SECURITY_IGNORE_DEFAULTS = [...exports.TEST_PATH_GLOBS];
|
|
152
|
+
/**
|
|
153
|
+
* Files written by a release tool rather than by a person.
|
|
154
|
+
*
|
|
155
|
+
* `CHANGELOG.md` is assembled from commit subjects, so every typo anyone has ever
|
|
156
|
+
* committed reappears in it — 39 of 42 spellcheck findings in one real repository
|
|
157
|
+
* came from that one file, none of them actionable: the text cannot be corrected
|
|
158
|
+
* without rewriting git history, and the next release regenerates it anyway.
|
|
159
|
+
*/
|
|
160
|
+
exports.GENERATED_DOC_FILES = ['CHANGELOG.md'];
|
|
126
161
|
/**
|
|
127
162
|
* Files excluded from the dead-code (unused-exports) report by default. These are
|
|
128
163
|
* REGEX sources (matched against each reported file path), not globs — dead-code's
|
|
@@ -157,6 +192,13 @@ exports.CONFIG_FILE_PATTERNS = [
|
|
|
157
192
|
* config/CI YAML and JSON out of the report — they are data, not code, and their
|
|
158
193
|
* repetition is not meaningful duplication. This mirrors the code-only file set the
|
|
159
194
|
* check already uses when scanning a git diff, so full-tree and diff scans agree.
|
|
195
|
+
*
|
|
196
|
+
* One entry per language pr-checkmate detects, all of them verified against the
|
|
197
|
+
* jscpd binary rather than assumed: the list used to stop at C++ while the check
|
|
198
|
+
* only ever handed jscpd `.ts`/`.js` files, so the Go, Rust, C#, Ruby and PHP
|
|
199
|
+
* halves were unreachable and the Python one was decoration. Copy-paste is a
|
|
200
|
+
* language-independent smell, and the tokenizer behind this check has always been
|
|
201
|
+
* able to see it — the file list was the only thing in the way.
|
|
160
202
|
*/
|
|
161
203
|
exports.DUPLICATE_CODE_FORMATS = [
|
|
162
204
|
'javascript',
|
|
@@ -168,6 +210,11 @@ exports.DUPLICATE_CODE_FORMATS = [
|
|
|
168
210
|
'swift',
|
|
169
211
|
'c',
|
|
170
212
|
'cpp',
|
|
213
|
+
'csharp',
|
|
214
|
+
'go',
|
|
215
|
+
'php',
|
|
216
|
+
'ruby',
|
|
217
|
+
'rust',
|
|
171
218
|
];
|
|
172
219
|
/**
|
|
173
220
|
* File extensions to check
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: PR CheckMate
|
|
2
|
+
|
|
3
|
+
# The container variant, scaffolded because this repository does not carry
|
|
4
|
+
# pr-checkmate as a dependency.
|
|
5
|
+
#
|
|
6
|
+
# The alternative would be `npx pr-checkmate`, and with no node_modules to reuse
|
|
7
|
+
# that resolves to whatever is newest on the registry and downloads it again on
|
|
8
|
+
# every job: 547 MB and about five minutes, measured. Worse, a major release
|
|
9
|
+
# would land on you without a line changing here — and a major release of a gate
|
|
10
|
+
# can start failing builds that passed yesterday.
|
|
11
|
+
#
|
|
12
|
+
# The image is pulled once and cached by the runner, and the tag below pins the
|
|
13
|
+
# major so nothing moves under you.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
pull_request:
|
|
17
|
+
branches: [main, master, develop]
|
|
18
|
+
types: [opened, synchronize, reopened]
|
|
19
|
+
|
|
20
|
+
# Least privilege. `pull-requests: write` is what lets the run upsert its summary
|
|
21
|
+
# comment on the PR; drop it and the checks still run, they just report to the log.
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
pull-requests: write
|
|
25
|
+
|
|
26
|
+
# Push three times in a minute and the first two runs become answers to questions
|
|
27
|
+
# nobody is asking any more — they still hold a runner, and they still race to
|
|
28
|
+
# upsert the summary comment, so a stale verdict can land on top of a fresh one.
|
|
29
|
+
# Keyed per PR, so runs on different pull requests never cancel each other.
|
|
30
|
+
concurrency:
|
|
31
|
+
group: pr-checkmate-${{ github.workflow }}-${{ github.ref }}
|
|
32
|
+
cancel-in-progress: true
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
pr-checkmate:
|
|
36
|
+
name: PR CheckMate
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
# A hung step otherwise runs to GitHub's six-hour default before anyone hears
|
|
39
|
+
# about it. Every check has its own internal timeout; this is the backstop.
|
|
40
|
+
timeout-minutes: 20
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- name: Checkout code
|
|
44
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
45
|
+
with:
|
|
46
|
+
# Required, not optional. Without the full history the PR base commit is
|
|
47
|
+
# not in the clone, no diff range can be built, and every diff-based
|
|
48
|
+
# check skips itself. The run then says `Scan Scope: … set
|
|
49
|
+
# fetch-depth: 0`, which is this line.
|
|
50
|
+
fetch-depth: 0
|
|
51
|
+
|
|
52
|
+
# One job runs the whole registry. Which checks run is decided in
|
|
53
|
+
# pr-checkmate.json (`severity`), not by splitting this into jobs.
|
|
54
|
+
#
|
|
55
|
+
# `--user` is not a detail: without it every file a formatter rewrites comes
|
|
56
|
+
# back owned by root, and the next step in your pipeline fails on
|
|
57
|
+
# permissions long after this one reported success.
|
|
58
|
+
#
|
|
59
|
+
# The environment variables are passed through by name rather than by value:
|
|
60
|
+
# the run reads them to find the PR, and interpolating them into the command
|
|
61
|
+
# line would put their contents into a shell.
|
|
62
|
+
- name: Run PR CheckMate
|
|
63
|
+
env:
|
|
64
|
+
# Not injected automatically: without this the run cannot authenticate,
|
|
65
|
+
# and the summary comment silently never reaches the PR.
|
|
66
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
67
|
+
run: |
|
|
68
|
+
docker run --rm \
|
|
69
|
+
-v "$PWD:/repo" \
|
|
70
|
+
--user "$(id -u):$(id -g)" \
|
|
71
|
+
-e GITHUB_TOKEN \
|
|
72
|
+
-e GITHUB_REPOSITORY \
|
|
73
|
+
-e GITHUB_EVENT_NAME \
|
|
74
|
+
-e GITHUB_EVENT_PATH \
|
|
75
|
+
-e GITHUB_REF \
|
|
76
|
+
-e GITHUB_SHA \
|
|
77
|
+
-e CI \
|
|
78
|
+
-v "$GITHUB_EVENT_PATH:$GITHUB_EVENT_PATH:ro" \
|
|
79
|
+
ghcr.io/mybono/pr-checkmate:__IMAGE_TAG__ all
|
|
@@ -11,10 +11,23 @@ permissions:
|
|
|
11
11
|
contents: read
|
|
12
12
|
pull-requests: write
|
|
13
13
|
|
|
14
|
+
# Push three times in a minute and the first two runs become answers to questions
|
|
15
|
+
# nobody is asking any more — they still hold a runner, and they still race to
|
|
16
|
+
# upsert the summary comment, so a stale verdict can land on top of a fresh one.
|
|
17
|
+
# Keyed per PR, so runs on different pull requests never cancel each other.
|
|
18
|
+
concurrency:
|
|
19
|
+
group: pr-checkmate-${{ github.workflow }}-${{ github.ref }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
14
22
|
jobs:
|
|
15
23
|
pr-checkmate:
|
|
16
24
|
name: PR CheckMate
|
|
17
25
|
runs-on: ubuntu-latest
|
|
26
|
+
# A hung step otherwise runs to GitHub's six-hour default before anyone hears
|
|
27
|
+
# about it. Every check here has its own internal timeout; this is the backstop
|
|
28
|
+
# for the ones that cannot fail themselves — a wedged network fetch, a tool
|
|
29
|
+
# waiting on stdin. Raise it for a large monorepo.
|
|
30
|
+
timeout-minutes: 20
|
|
18
31
|
steps:
|
|
19
32
|
- name: Checkout code
|
|
20
33
|
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
@@ -48,9 +61,29 @@ jobs:
|
|
|
48
61
|
restore-keys: |
|
|
49
62
|
pr-checkmate-${{ runner.os }}-
|
|
50
63
|
|
|
64
|
+
# Grype keeps its own vulnerability database, and it is the single largest
|
|
65
|
+
# download in the whole run: 134 MB compressed, 1.9 GB unpacked, refreshed
|
|
66
|
+
# daily upstream. Without this step every job pays for it again. The key is
|
|
67
|
+
# dated rather than content-hashed, because the thing that changes is the
|
|
68
|
+
# upstream database, not anything in this repository — a stale entry is
|
|
69
|
+
# restored by the prefix and grype updates it in place.
|
|
70
|
+
- name: Cache Grype vulnerability database
|
|
71
|
+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
72
|
+
with:
|
|
73
|
+
path: ~/.cache/grype
|
|
74
|
+
key: grype-db-${{ runner.os }}-${{ github.run_id }}
|
|
75
|
+
restore-keys: |
|
|
76
|
+
grype-db-${{ runner.os }}-
|
|
77
|
+
|
|
51
78
|
# One job runs the whole registry. Splitting it per check would repeat the
|
|
52
79
|
# checkout and install for every one of them, and the selection of what runs
|
|
53
80
|
# belongs in pr-checkmate.json (`severity`), not in copied YAML.
|
|
81
|
+
#
|
|
82
|
+
# This template is the one `init` scaffolds when pr-checkmate IS a
|
|
83
|
+
# dependency: `npm ci` above installs it, so npx runs that copy at the
|
|
84
|
+
# version your lockfile pins. A repository that does not install it gets
|
|
85
|
+
# the container variant instead, because bare `npx` would resolve to the
|
|
86
|
+
# newest release and download it on every job.
|
|
54
87
|
- name: Run PR CheckMate
|
|
55
88
|
env:
|
|
56
89
|
# Not injected automatically: without this the run cannot authenticate,
|
|
@@ -106,7 +106,7 @@ exports.dependencyCheck = {
|
|
|
106
106
|
applies(ctx) {
|
|
107
107
|
if (ctx.config.dependency?.enabled === false)
|
|
108
108
|
return false;
|
|
109
|
-
return
|
|
109
|
+
return (0, types_1.isNpmProject)(ctx);
|
|
110
110
|
},
|
|
111
111
|
async run(ctx) {
|
|
112
112
|
// Resolve the bundled knip binary and run it via the current node — never
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.licenseCheck = void 0;
|
|
7
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
9
4
|
const utils_1 = require("../../../utils");
|
|
10
5
|
const execa_1 = require("../../../utils/execa");
|
|
11
6
|
const types_1 = require("../../types");
|
|
@@ -19,7 +14,7 @@ exports.licenseCheck = {
|
|
|
19
14
|
name: 'License Check',
|
|
20
15
|
phase: 'blocking',
|
|
21
16
|
applies(ctx) {
|
|
22
|
-
return
|
|
17
|
+
return (0, types_1.isNpmProject)(ctx);
|
|
23
18
|
},
|
|
24
19
|
async run(ctx) {
|
|
25
20
|
const allowPackages = new Set(ctx.config.licenseCheck?.allowPackages ?? []);
|
|
@@ -34,7 +34,7 @@ exports.npmAuditCheck = {
|
|
|
34
34
|
applies(ctx) {
|
|
35
35
|
if (ctx.config.security?.['npm-audit']?.enabled === false)
|
|
36
36
|
return false;
|
|
37
|
-
return
|
|
37
|
+
return (0, types_1.isNpmProject)(ctx);
|
|
38
38
|
},
|
|
39
39
|
async run(ctx) {
|
|
40
40
|
// `high`, not `moderate`. A moderate CVE in a direct dependency often has no
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.outdatedDepsCheck = void 0;
|
|
7
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
9
4
|
const utils_1 = require("../../../utils");
|
|
10
5
|
const execa_1 = require("../../../utils/execa");
|
|
11
6
|
const types_1 = require("../../types");
|
|
@@ -16,7 +11,7 @@ exports.outdatedDepsCheck = {
|
|
|
16
11
|
applies(ctx) {
|
|
17
12
|
if (ctx.config.outdatedDeps?.enabled === false)
|
|
18
13
|
return false;
|
|
19
|
-
return
|
|
14
|
+
return (0, types_1.isNpmProject)(ctx);
|
|
20
15
|
},
|
|
21
16
|
async run(ctx) {
|
|
22
17
|
// `npm outdated` queries the registry for every dependency and can take a
|
|
@@ -7,6 +7,37 @@ const utils_1 = require("../../../utils");
|
|
|
7
7
|
const execa_1 = require("../../../utils/execa");
|
|
8
8
|
const targets_1 = require("../../targets");
|
|
9
9
|
const types_1 = require("../../types");
|
|
10
|
+
/**
|
|
11
|
+
* The files to hand jscpd, per language.
|
|
12
|
+
*
|
|
13
|
+
* This map is why the check reads `ctx.languages` at all. It used to pass a fixed
|
|
14
|
+
* `*.ts,*.tsx,*.js,*.jsx` and gate itself on TypeScript/JavaScript, so a Python
|
|
15
|
+
* repository got `⏭️ Duplicate Code: no relevant files` — not because it had no
|
|
16
|
+
* duplication, but because nothing ever looked. The formats list in `constants.ts`
|
|
17
|
+
* named Python and C++ all along, which made the report's own claim untrue.
|
|
18
|
+
*
|
|
19
|
+
* Extensions mirror `detect-language.ts` so a language that decides the check runs
|
|
20
|
+
* is a language whose files it actually scans, with one deliberate addition: `.c`
|
|
21
|
+
* and `.h` under `cpp`, since a C project detects as `cpp` and the `c` format is
|
|
22
|
+
* one jscpd supports.
|
|
23
|
+
*/
|
|
24
|
+
const LANGUAGE_GLOBS = {
|
|
25
|
+
typescript: ['*.ts', '*.tsx'],
|
|
26
|
+
javascript: ['*.js', '*.jsx', '*.mjs', '*.cjs'],
|
|
27
|
+
python: ['*.py'],
|
|
28
|
+
cpp: ['*.cpp', '*.cc', '*.cxx', '*.c++', '*.hpp', '*.hh', '*.hxx', '*.c', '*.h'],
|
|
29
|
+
swift: ['*.swift'],
|
|
30
|
+
kotlin: ['*.kt', '*.kts', '*.java'],
|
|
31
|
+
go: ['*.go'],
|
|
32
|
+
rust: ['*.rs'],
|
|
33
|
+
csharp: ['*.cs'],
|
|
34
|
+
ruby: ['*.rb'],
|
|
35
|
+
php: ['*.php'],
|
|
36
|
+
};
|
|
37
|
+
/** The scan globs for one run: every detected language jscpd can tokenise. */
|
|
38
|
+
function globsFor(languages) {
|
|
39
|
+
return [...new Set(languages.flatMap(lang => LANGUAGE_GLOBS[lang] ?? []))];
|
|
40
|
+
}
|
|
10
41
|
// Always excluded regardless of client config — scanning these never yields
|
|
11
42
|
// meaningful production duplication. Config files (eslint/*.config.*) are data,
|
|
12
43
|
// not logic, and are commonly shipped in two formats (e.g. .eslintrc + flat
|
|
@@ -36,12 +67,12 @@ exports.duplicateCheck = {
|
|
|
36
67
|
applies(ctx) {
|
|
37
68
|
if (ctx.config.duplicate?.enabled === false)
|
|
38
69
|
return false;
|
|
39
|
-
return ctx.languages.
|
|
70
|
+
return globsFor(ctx.languages).length > 0;
|
|
40
71
|
},
|
|
41
72
|
async run(ctx) {
|
|
42
73
|
const minLines = ctx.config.duplicate?.minLines ?? 5;
|
|
43
74
|
const minTokens = ctx.config.duplicate?.minTokens ?? 50;
|
|
44
|
-
const targetPaths = await (0, targets_1.resolveScopedTargetFiles)(ctx,
|
|
75
|
+
const targetPaths = await (0, targets_1.resolveScopedTargetFiles)(ctx, globsFor(ctx.languages), (0, config_1.getSourcePaths)());
|
|
45
76
|
if (targetPaths === null)
|
|
46
77
|
return (0, types_1.skip)('git unavailable');
|
|
47
78
|
if (targetPaths.length === 0)
|
|
@@ -72,15 +72,20 @@ exports.markdownLintCheck = {
|
|
|
72
72
|
'coverage/**',
|
|
73
73
|
'graphify-out/**',
|
|
74
74
|
'.claude/**',
|
|
75
|
+
// Release-tool output, not prose anyone edits — see GENERATED_DOC_FILES.
|
|
76
|
+
...config_1.GENERATED_DOC_FILES,
|
|
75
77
|
...(userConfig.ignores ?? []),
|
|
76
78
|
];
|
|
77
79
|
// markdownlint-cli2 has no --ignore-path flag; exclusions are expressed as
|
|
78
80
|
// negated globs (`#pattern`). A bare directory name also needs `#name/**`
|
|
79
|
-
// to exclude its contents.
|
|
81
|
+
// to exclude its contents — but a filename does not, and `#CHANGELOG.md/**`
|
|
82
|
+
// is a glob that can never match. `path.extname` is what tells them apart,
|
|
83
|
+
// and it reads a leading dot as part of the name, so `.claude` is still
|
|
84
|
+
// treated as the directory it is.
|
|
80
85
|
const ignoreGlobs = defaultIgnorePatterns.flatMap(pattern => {
|
|
81
86
|
const clean = pattern.replace(/\/+$/, '');
|
|
82
87
|
const globs = [`#${clean}`];
|
|
83
|
-
if (!clean.includes('*'))
|
|
88
|
+
if (!clean.includes('*') && !node_path_1.default.extname(clean))
|
|
84
89
|
globs.push(`#${clean}/**`);
|
|
85
90
|
return globs;
|
|
86
91
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.diffSecurityCheck = void 0;
|
|
4
|
+
const config_1 = require("../../../config");
|
|
4
5
|
const utils_1 = require("../../../utils");
|
|
5
6
|
const targets_1 = require("../../targets");
|
|
6
7
|
const types_1 = require("../../types");
|
|
@@ -293,7 +294,10 @@ exports.diffSecurityCheck = {
|
|
|
293
294
|
const found = (0, findings_1.emptyBuckets)();
|
|
294
295
|
const scanOpts = {
|
|
295
296
|
comments: cfg.commentHandling ?? 'language',
|
|
296
|
-
ignorePaths
|
|
297
|
+
// Setting `ignorePaths` REPLACES the test-path defaults rather than adding
|
|
298
|
+
// to them, matching `duplicate.ignore` and `deadCode.ignoreFiles`. A client
|
|
299
|
+
// who wants their tests scanned says so by listing something else here.
|
|
300
|
+
ignorePaths: cfg.ignorePaths ?? config_1.DIFF_SECURITY_IGNORE_DEFAULTS,
|
|
297
301
|
};
|
|
298
302
|
const extra = compileExtraPatterns(ctx);
|
|
299
303
|
const disabled = cfg.disablePatterns ?? [];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type LanguageUsage } from '../../utils/detect-language';
|
|
2
|
+
import { packageVersion } from '../../utils/package-version';
|
|
2
3
|
import { type EnsureResult } from './ensure';
|
|
3
4
|
import { type ProfileDecision } from './profiles';
|
|
4
|
-
|
|
5
|
-
export declare function packageVersion(): string;
|
|
5
|
+
export { packageVersion };
|
|
6
6
|
/**
|
|
7
7
|
* Everything the install layer knows about this repository, in one shot.
|
|
8
8
|
*
|
|
@@ -3,7 +3,7 @@ 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.packageVersion =
|
|
6
|
+
exports.packageVersion = void 0;
|
|
7
7
|
exports.planInstall = planInstall;
|
|
8
8
|
exports.installProfiles = installProfiles;
|
|
9
9
|
exports.runDoctor = runDoctor;
|
|
@@ -13,19 +13,11 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
13
13
|
const loader_1 = require("../../config/loader");
|
|
14
14
|
const detect_language_1 = require("../../utils/detect-language");
|
|
15
15
|
const logger_1 = require("../../utils/logger");
|
|
16
|
+
const package_version_1 = require("../../utils/package-version");
|
|
17
|
+
Object.defineProperty(exports, "packageVersion", { enumerable: true, get: function () { return package_version_1.packageVersion; } });
|
|
16
18
|
const cache_1 = require("./cache");
|
|
17
19
|
const ensure_1 = require("./ensure");
|
|
18
20
|
const profiles_1 = require("./profiles");
|
|
19
|
-
/** Our own version, read from the package manifest that ships with us. */
|
|
20
|
-
function packageVersion() {
|
|
21
|
-
try {
|
|
22
|
-
const manifest = node_path_1.default.resolve(__dirname, '../../..', 'package.json');
|
|
23
|
-
return JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf-8')).version ?? '0.0.0';
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return '0.0.0';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
21
|
/**
|
|
30
22
|
* Everything the install layer knows about this repository, in one shot.
|
|
31
23
|
*
|
|
@@ -36,7 +28,7 @@ function packageVersion() {
|
|
|
36
28
|
async function planInstall(cwd = process.cwd(), usage) {
|
|
37
29
|
const config = (0, loader_1.loadProjectConfig)();
|
|
38
30
|
const detected = usage ?? (await (0, detect_language_1.detectLanguageUsage)(cwd, config.ignoreDirs, config.detect));
|
|
39
|
-
const location = (0, cache_1.resolveCacheDir)(packageVersion(), config.install?.cacheDir);
|
|
31
|
+
const location = (0, cache_1.resolveCacheDir)((0, package_version_1.packageVersion)(), config.install?.cacheDir);
|
|
40
32
|
return { decisions: (0, profiles_1.planProfiles)(detected, config), cacheDir: location?.dir ?? null };
|
|
41
33
|
}
|
|
42
34
|
/** Whether a profile's tooling is present in the cache right now. */
|
|
@@ -71,7 +63,7 @@ async function installProfiles(cwd = process.cwd(), all = false) {
|
|
|
71
63
|
// packages fight over npm's cache and gain nothing; the wall-clock cost lands
|
|
72
64
|
// once, on a cold cache, and clarity of the log matters more there.
|
|
73
65
|
for (const profile of wanted) {
|
|
74
|
-
results.push(await (0, ensure_1.ensureProfile)(profile, { cwd, version: packageVersion(), install }));
|
|
66
|
+
results.push(await (0, ensure_1.ensureProfile)(profile, { cwd, version: (0, package_version_1.packageVersion)(), install }));
|
|
75
67
|
}
|
|
76
68
|
return results;
|
|
77
69
|
}
|
|
@@ -90,7 +82,7 @@ async function runDoctor(cwd = process.cwd()) {
|
|
|
90
82
|
const lines = [];
|
|
91
83
|
lines.push('PR CheckMate — install doctor');
|
|
92
84
|
lines.push('');
|
|
93
|
-
lines.push(` version ${packageVersion()}`);
|
|
85
|
+
lines.push(` version ${(0, package_version_1.packageVersion)()}`);
|
|
94
86
|
lines.push(` repository ${cwd}`);
|
|
95
87
|
lines.push(` cache ${cacheDir ?? 'unavailable (no writable directory)'}`);
|
|
96
88
|
lines.push(` strategy ${config.install?.strategy ?? 'adaptive'}`);
|
|
@@ -116,7 +108,7 @@ async function runDoctor(cwd = process.cwd()) {
|
|
|
116
108
|
lines.push(` ${missing.length} profile(s) not installed yet. They are fetched on the next`);
|
|
117
109
|
lines.push(' run, or now with: npx pr-checkmate install');
|
|
118
110
|
}
|
|
119
|
-
const stale = (0, cache_1.stalePaths)(packageVersion(), config.install?.cacheDir);
|
|
111
|
+
const stale = (0, cache_1.stalePaths)((0, package_version_1.packageVersion)(), config.install?.cacheDir);
|
|
120
112
|
if (stale.length > 0) {
|
|
121
113
|
lines.push('');
|
|
122
114
|
lines.push(` ${stale.length} directory(ies) from other versions: npx pr-checkmate cache-prune`);
|
|
@@ -126,7 +118,7 @@ async function runDoctor(cwd = process.cwd()) {
|
|
|
126
118
|
/** Remove cached tooling belonging to other versions of pr-checkmate. */
|
|
127
119
|
function pruneCache() {
|
|
128
120
|
const config = (0, loader_1.loadProjectConfig)();
|
|
129
|
-
const stale = (0, cache_1.stalePaths)(packageVersion(), config.install?.cacheDir);
|
|
121
|
+
const stale = (0, cache_1.stalePaths)((0, package_version_1.packageVersion)(), config.install?.cacheDir);
|
|
130
122
|
if (stale.length === 0) {
|
|
131
123
|
logger_1.logger.print('Nothing to prune — no tooling from other versions is cached.');
|
|
132
124
|
return;
|
package/dist/core/reporters.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.report = report;
|
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const utils_1 = require("../utils");
|
|
10
10
|
const http_1 = require("../utils/http");
|
|
11
|
+
const package_version_1 = require("../utils/package-version");
|
|
11
12
|
const STATUS_ICON = {
|
|
12
13
|
pass: '✅',
|
|
13
14
|
warn: '⚠️',
|
|
@@ -74,7 +75,7 @@ function buildMarkdownReport(report, cfg = {}) {
|
|
|
74
75
|
// Say so rather than letting the table quietly disagree with the counts above.
|
|
75
76
|
...(hidden > 0 ? ['', `_${hidden} row(s) hidden by \`reporter.include\`._`] : []),
|
|
76
77
|
'',
|
|
77
|
-
|
|
78
|
+
`_Generated by [PR CheckMate](https://www.npmjs.com/package/pr-checkmate) v${(0, package_version_1.packageVersion)()}_`,
|
|
78
79
|
].join('\n');
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
@@ -89,7 +90,12 @@ function printStdout(report, cfg) {
|
|
|
89
90
|
const { passed, warned, failed, skipped } = report.summary;
|
|
90
91
|
utils_1.logger.print('');
|
|
91
92
|
utils_1.logger.print(SUMMARY_BORDER);
|
|
92
|
-
|
|
93
|
+
// The version belongs in the headline, not in a footnote. Which copy of
|
|
94
|
+
// pr-checkmate produced a report is the first thing to establish when the
|
|
95
|
+
// report disagrees with what someone expects, and it is not otherwise
|
|
96
|
+
// recoverable from the log: a stale global install answers `pr-checkmate` on
|
|
97
|
+
// `PATH` ahead of the project's own, and reads the same config while doing it.
|
|
98
|
+
utils_1.logger.print(` PR CheckMate Summary · v${(0, package_version_1.packageVersion)()}`);
|
|
93
99
|
utils_1.logger.print(SUMMARY_BORDER);
|
|
94
100
|
for (const r of report.results) {
|
|
95
101
|
const details = r.details ? `: ${r.details}` : '';
|