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
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RunScope } from './types';
|
|
2
|
+
import { CheckContext, CheckReport, CheckStatus, RunOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Apply the universal `config.severity` override for a check. Only remaps
|
|
5
|
+
* between warn↔fail (pass/skip are never changed); `off` is handled earlier by
|
|
6
|
+
* excluding the check from the run entirely. This is what lets a repo turn any
|
|
7
|
+
* advisory check into a gate, or downgrade a blocking one, by name.
|
|
8
|
+
*/
|
|
9
|
+
export declare function applySeverity(name: string, status: CheckStatus, ctx: CheckContext): CheckStatus;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a diff-only check's skip is worth a row in the report.
|
|
12
|
+
*
|
|
13
|
+
* The two cases look identical from inside the check and are opposites to the
|
|
14
|
+
* person reading the result:
|
|
15
|
+
*
|
|
16
|
+
* - The range is missing because the run FELL BACK to a full scan — a shallow
|
|
17
|
+
* clone, an unreachable PR base. Coverage was lost without anyone choosing
|
|
18
|
+
* that, and saying nothing is how it stays lost (ticket 057).
|
|
19
|
+
* - The full scan was ASKED FOR. `--full` means "look at everything now", so
|
|
20
|
+
* three rows explaining that three checks review a diff repeat what the reader
|
|
21
|
+
* just said and push the real findings further down.
|
|
22
|
+
*
|
|
23
|
+
* `always` and `never` remain absolute; only `auto` distinguishes them.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shouldReportDiffSkip(scope: RunScope | undefined, reportSkipped?: 'auto' | 'always' | 'never'): boolean;
|
|
2
26
|
/**
|
|
3
27
|
* Run all applicable checks and return a structured report.
|
|
4
28
|
*
|
package/dist/core/run-checks.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applySeverity = applySeverity;
|
|
4
|
+
exports.shouldReportDiffSkip = shouldReportDiffSkip;
|
|
3
5
|
exports.runChecks = runChecks;
|
|
4
6
|
const utils_1 = require("../utils");
|
|
5
7
|
const pool_1 = require("../utils/pool");
|
|
@@ -71,6 +73,29 @@ function applySeverity(name, status, ctx) {
|
|
|
71
73
|
return 'warn';
|
|
72
74
|
return status;
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether a diff-only check's skip is worth a row in the report.
|
|
78
|
+
*
|
|
79
|
+
* The two cases look identical from inside the check and are opposites to the
|
|
80
|
+
* person reading the result:
|
|
81
|
+
*
|
|
82
|
+
* - The range is missing because the run FELL BACK to a full scan — a shallow
|
|
83
|
+
* clone, an unreachable PR base. Coverage was lost without anyone choosing
|
|
84
|
+
* that, and saying nothing is how it stays lost (ticket 057).
|
|
85
|
+
* - The full scan was ASKED FOR. `--full` means "look at everything now", so
|
|
86
|
+
* three rows explaining that three checks review a diff repeat what the reader
|
|
87
|
+
* just said and push the real findings further down.
|
|
88
|
+
*
|
|
89
|
+
* `always` and `never` remain absolute; only `auto` distinguishes them.
|
|
90
|
+
*/
|
|
91
|
+
function shouldReportDiffSkip(scope, reportSkipped = 'auto') {
|
|
92
|
+
if (reportSkipped === 'never')
|
|
93
|
+
return false;
|
|
94
|
+
if (reportSkipped === 'always')
|
|
95
|
+
return true;
|
|
96
|
+
const askedForFullScan = scope?.kind === 'full' && !scope.degraded;
|
|
97
|
+
return !askedForFullScan;
|
|
98
|
+
}
|
|
74
99
|
/**
|
|
75
100
|
* Why a `requiresDiff` check could not run, phrased for the person reading the
|
|
76
101
|
* report. A full scan the client asked for is expected; one we fell back to is
|
|
@@ -90,11 +115,19 @@ function noDiffReason(ctx) {
|
|
|
90
115
|
*/
|
|
91
116
|
async function runOne(check, ctx, collector, heavy) {
|
|
92
117
|
try {
|
|
93
|
-
// A diff-only check without a range is a hole in coverage
|
|
94
|
-
// irrelevance
|
|
118
|
+
// A diff-only check without a range is usually a hole in coverage rather
|
|
119
|
+
// than an irrelevance, so it is reported as a skip and the gap stays visible
|
|
120
|
+
// (ticket 057).
|
|
121
|
+
//
|
|
122
|
+
// Except when the whole-repository scan was ASKED FOR. `--full` means "look
|
|
123
|
+
// at everything now", and answering with three rows explaining that three
|
|
124
|
+
// checks review a diff tells the reader something they just said themselves.
|
|
125
|
+
// A degraded scope still reports: there the missing range is a symptom —
|
|
126
|
+
// a shallow clone, an unreachable base — and staying quiet about it is how
|
|
127
|
+
// coverage disappears unnoticed.
|
|
95
128
|
if (check.requiresDiff && !ctx.baseSha) {
|
|
96
|
-
|
|
97
|
-
|
|
129
|
+
if (shouldReportDiffSkip(ctx.scope, ctx.config.run?.reportSkipped) &&
|
|
130
|
+
(await check.applies(ctx))) {
|
|
98
131
|
collector.register(check.name, 'skip', noDiffReason(ctx));
|
|
99
132
|
}
|
|
100
133
|
return;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -164,6 +164,24 @@ export declare const pass: (details?: string) => CheckOutcome;
|
|
|
164
164
|
export declare const warn: (details: string) => CheckOutcome;
|
|
165
165
|
export declare const fail: (details: string) => CheckOutcome;
|
|
166
166
|
export declare const skip: (details: string) => CheckOutcome;
|
|
167
|
+
/**
|
|
168
|
+
* Whether this repository is an npm project — the gate for every check that reads
|
|
169
|
+
* the npm dependency tree (NPM Audit, Dependencies, License Check, Outdated Deps).
|
|
170
|
+
*
|
|
171
|
+
* A `package.json` on its own is not the answer, and this is the one predicate
|
|
172
|
+
* where that matters most: installing pr-checkmate as a devDependency *creates*
|
|
173
|
+
* that file. Gate on it alone and a Python service gets four rows auditing the
|
|
174
|
+
* dependency tree of the tool auditing it — `npm audit` over our own transitive
|
|
175
|
+
* packages, licences of our own `node_modules`, "outdated" versions of tooling
|
|
176
|
+
* the team never chose. All four are unactionable by construction: nothing in the
|
|
177
|
+
* repository's own code can change any of them.
|
|
178
|
+
*
|
|
179
|
+
* So both halves must hold — a manifest to read, and JavaScript or TypeScript
|
|
180
|
+
* actually written here. A polyglot repository (a Django backend with a JS
|
|
181
|
+
* frontend) satisfies both and keeps every check; a Python repository whose only
|
|
182
|
+
* `package.json` is the one that installed us satisfies neither.
|
|
183
|
+
*/
|
|
184
|
+
export declare function isNpmProject(ctx: CheckContext): boolean;
|
|
167
185
|
/**
|
|
168
186
|
* Identity helper for authoring a `Check` with full type inference and checking.
|
|
169
187
|
* Purely ergonomic — `defineCheck({ ... })` is equivalent to `{ ... } satisfies Check`.
|
package/dist/core/types.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.defineCheck = exports.skip = exports.fail = exports.warn = exports.pass = exports.PHASES = void 0;
|
|
7
|
+
exports.isNpmProject = isNpmProject;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
4
10
|
exports.PHASES = ['blocking', 'informational', 'format'];
|
|
5
11
|
/** Outcome constructors — keep check bodies terse. */
|
|
6
12
|
const pass = (details) => ({
|
|
@@ -14,6 +20,28 @@ const fail = (details) => ({ status: 'fail', details });
|
|
|
14
20
|
exports.fail = fail;
|
|
15
21
|
const skip = (details) => ({ status: 'skip', details });
|
|
16
22
|
exports.skip = skip;
|
|
23
|
+
/**
|
|
24
|
+
* Whether this repository is an npm project — the gate for every check that reads
|
|
25
|
+
* the npm dependency tree (NPM Audit, Dependencies, License Check, Outdated Deps).
|
|
26
|
+
*
|
|
27
|
+
* A `package.json` on its own is not the answer, and this is the one predicate
|
|
28
|
+
* where that matters most: installing pr-checkmate as a devDependency *creates*
|
|
29
|
+
* that file. Gate on it alone and a Python service gets four rows auditing the
|
|
30
|
+
* dependency tree of the tool auditing it — `npm audit` over our own transitive
|
|
31
|
+
* packages, licences of our own `node_modules`, "outdated" versions of tooling
|
|
32
|
+
* the team never chose. All four are unactionable by construction: nothing in the
|
|
33
|
+
* repository's own code can change any of them.
|
|
34
|
+
*
|
|
35
|
+
* So both halves must hold — a manifest to read, and JavaScript or TypeScript
|
|
36
|
+
* actually written here. A polyglot repository (a Django backend with a JS
|
|
37
|
+
* frontend) satisfies both and keeps every check; a Python repository whose only
|
|
38
|
+
* `package.json` is the one that installed us satisfies neither.
|
|
39
|
+
*/
|
|
40
|
+
function isNpmProject(ctx) {
|
|
41
|
+
if (!ctx.languages.includes('javascript') && !ctx.languages.includes('typescript'))
|
|
42
|
+
return false;
|
|
43
|
+
return node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, 'package.json'));
|
|
44
|
+
}
|
|
17
45
|
/**
|
|
18
46
|
* Identity helper for authoring a `Check` with full type inference and checking.
|
|
19
47
|
* Purely ergonomic — `defineCheck({ ... })` is equivalent to `{ ... } satisfies Check`.
|
package/dist/init.js
CHANGED
|
@@ -91,7 +91,21 @@ const ADVISORY_FLAG = ' continue-on-error: true';
|
|
|
91
91
|
* were just asked about.
|
|
92
92
|
*/
|
|
93
93
|
function setupWorkflow(root, mode) {
|
|
94
|
-
|
|
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);
|
|
95
109
|
const workflowDir = path_1.default.join(root, '.github', 'workflows');
|
|
96
110
|
const workflowDest = path_1.default.join(workflowDir, 'pr-checkmate.yml');
|
|
97
111
|
if (!fs_1.default.existsSync(workflowSource)) {
|
|
@@ -105,13 +119,46 @@ function setupWorkflow(root, mode) {
|
|
|
105
119
|
if (!fs_1.default.existsSync(workflowDir)) {
|
|
106
120
|
fs_1.default.mkdirSync(workflowDir, { recursive: true });
|
|
107
121
|
}
|
|
108
|
-
const template = fs_1.default.readFileSync(workflowSource, 'utf-8');
|
|
122
|
+
const template = fs_1.default.readFileSync(workflowSource, 'utf-8').replace(/__IMAGE_TAG__/g, imageTag());
|
|
109
123
|
const workflow = mode === 'advisory'
|
|
110
124
|
? template.replace(RUN_STEP_ANCHOR, `${ADVISORY_NOTICE}\n${RUN_STEP_ANCHOR}\n${ADVISORY_FLAG}`)
|
|
111
125
|
: template;
|
|
112
126
|
fs_1.default.writeFileSync(workflowDest, workflow);
|
|
113
127
|
utils_1.logger.info(`[init]: ✅ Created .github/workflows/pr-checkmate.yml (${mode === 'strict' ? 'blocking' : 'advisory'})`);
|
|
114
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
|
+
}
|
|
161
|
+
}
|
|
115
162
|
/** Where the advisory banner is spliced in — the step that runs the checks. */
|
|
116
163
|
const RUN_STEP_ANCHOR = ' - name: Run PR CheckMate';
|
|
117
164
|
/** Create a tsconfig.json scaffold if the consumer repo lacks one. */
|
|
@@ -217,30 +264,62 @@ function buildInitialConfig(sourcePath, languages) {
|
|
|
217
264
|
commands: config_1.DEFAULT_COMMANDS,
|
|
218
265
|
'// reporting': REPORTING_HINT,
|
|
219
266
|
sourcePath,
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
//
|
|
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.
|
|
224
284
|
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
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.
|
|
230
292
|
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// deadCode.ignoreFiles → test helpers start reporting as unused exports
|
|
236
|
-
// Anyone who needs those sets them deliberately, with the defaults in view.
|
|
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.
|
|
237
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.`,
|
|
238
305
|
cspell: { words: [], ignorePaths: [] },
|
|
239
|
-
|
|
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: [] },
|
|
240
311
|
markdownlint: { ignores: [] },
|
|
241
312
|
dependency: { ignore: [] },
|
|
242
313
|
circularDeps: { ignore: [] },
|
|
243
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] },
|
|
244
323
|
documentation: config_1.documentation,
|
|
245
324
|
languages,
|
|
246
325
|
};
|
|
@@ -17,6 +17,10 @@ const configFiles = [
|
|
|
17
17
|
{ source: 'src/config/cspell.json', dest: 'cspell.json' },
|
|
18
18
|
{ source: 'tsconfig.json', dest: 'tsconfig.json' },
|
|
19
19
|
{ source: 'src/config/pr-checkmate-workflow.yml', dest: 'dist/config/pr-checkmate-workflow.yml' },
|
|
20
|
+
{
|
|
21
|
+
source: 'src/config/pr-checkmate-workflow-docker.yml',
|
|
22
|
+
dest: 'dist/config/pr-checkmate-workflow-docker.yml',
|
|
23
|
+
},
|
|
20
24
|
{ source: 'src/config/pr-checkmate.schema.json', dest: 'pr-checkmate.schema.json' },
|
|
21
25
|
];
|
|
22
26
|
/**
|
|
@@ -18,7 +18,17 @@ const logger_1 = require("./logger");
|
|
|
18
18
|
*/
|
|
19
19
|
const MARKER_FILES = {
|
|
20
20
|
typescript: ['tsconfig.json'],
|
|
21
|
-
|
|
21
|
+
// Deliberately empty. `package.json` used to sit here, and it is the one marker
|
|
22
|
+
// this package creates itself: installing pr-checkmate through npm puts a
|
|
23
|
+
// `package.json` in a repository that may not hold a single line of JavaScript.
|
|
24
|
+
// A Python service then detected as JavaScript and got the whole JS half of the
|
|
25
|
+
// report — ESLint, Prettier, NPM Audit, License Check — every row of it about
|
|
26
|
+
// the file that installed us. `install/profiles.ts` already refused to buy
|
|
27
|
+
// 152 MB of toolchain off this marker (ticket 064); the checks now refuse to
|
|
28
|
+
// run off it for the same reason. Real JavaScript is detected by its `.js`,
|
|
29
|
+
// `.jsx`, `.mjs` and `.cjs` files, and a repository that has none loses
|
|
30
|
+
// nothing: every JS check it lost would have reported "no files" anyway.
|
|
31
|
+
javascript: [],
|
|
22
32
|
python: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'],
|
|
23
33
|
cpp: ['CMakeLists.txt', 'compile_commands.json', 'conanfile.txt', 'conanfile.py'],
|
|
24
34
|
swift: ['Package.swift', 'Package.resolved'],
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -28,6 +28,7 @@ __exportStar(require("./direct-deps"), exports);
|
|
|
28
28
|
__exportStar(require("./detect-language"), exports);
|
|
29
29
|
__exportStar(require("./glob"), exports);
|
|
30
30
|
__exportStar(require("./update-notifier"), exports);
|
|
31
|
+
__exportStar(require("./package-version"), exports);
|
|
31
32
|
__exportStar(require("./ci"), exports);
|
|
32
33
|
__exportStar(require("./git"), exports);
|
|
33
34
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.packageVersion = packageVersion;
|
|
7
|
+
exports.packageName = packageName;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
/**
|
|
11
|
+
* Our own name and version, read from the manifest that ships inside the package.
|
|
12
|
+
*
|
|
13
|
+
* Read from disk rather than baked in at build time so it is the *running* copy
|
|
14
|
+
* that answers. That distinction is the whole point: a stale global install
|
|
15
|
+
* (`npm i -g pr-checkmate`) shadows the project's own copy on `PATH`, and a repo
|
|
16
|
+
* pinned to the latest version can still be checked by a year-old binary without
|
|
17
|
+
* anything in the log saying so. One client spent a while reading an upgrade
|
|
18
|
+
* notice for a version they had already installed, because nothing printed which
|
|
19
|
+
* version was speaking.
|
|
20
|
+
*
|
|
21
|
+
* Falls back to placeholders rather than throwing: no caller here is important
|
|
22
|
+
* enough to fail a run over a manifest it could not read.
|
|
23
|
+
*/
|
|
24
|
+
function readManifest() {
|
|
25
|
+
try {
|
|
26
|
+
const manifest = node_path_1.default.resolve(__dirname, '../..', 'package.json');
|
|
27
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf-8'));
|
|
28
|
+
return { name: parsed.name ?? 'pr-checkmate', version: parsed.version ?? '0.0.0' };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { name: 'pr-checkmate', version: '0.0.0' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** The version of pr-checkmate that is executing right now. */
|
|
35
|
+
function packageVersion() {
|
|
36
|
+
return readManifest().version;
|
|
37
|
+
}
|
|
38
|
+
/** The package name that is executing right now — the one to query npm for. */
|
|
39
|
+
function packageName() {
|
|
40
|
+
return readManifest().name;
|
|
41
|
+
}
|
|
@@ -1,16 +1,12 @@
|
|
|
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.isMajorUpgrade = isMajorUpgrade;
|
|
7
4
|
exports.getUpdateNotice = getUpdateNotice;
|
|
8
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
10
5
|
// Imported from the module rather than the `../config` barrel on purpose:
|
|
11
6
|
// `config/index.ts` imports `../utils`, so going through the barrel would make
|
|
12
7
|
// utils ⇄ config circular. `constants.ts` itself pulls in nothing from utils.
|
|
13
8
|
const constants_1 = require("../config/constants");
|
|
9
|
+
const package_version_1 = require("./package-version");
|
|
14
10
|
// Same widths as the run summary in `reporters.ts`, so the two boxes line up
|
|
15
11
|
// when both are printed at the end of a run.
|
|
16
12
|
const NOTICE_BORDER = '═'.repeat(63);
|
|
@@ -72,8 +68,10 @@ async function getUpdateNotice(timeoutMs = 2500) {
|
|
|
72
68
|
if (process.env.PR_CHECKMATE_NO_UPDATE_CHECK)
|
|
73
69
|
return null;
|
|
74
70
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// The *running* copy's identity, not the project's declared dependency. A
|
|
72
|
+
// stale global install shadows the local one on `PATH`, and it is the stale
|
|
73
|
+
// copy that must own the notice it prints.
|
|
74
|
+
const pkg = { name: (0, package_version_1.packageName)(), version: (0, package_version_1.packageVersion)() };
|
|
77
75
|
const controller = new AbortController();
|
|
78
76
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
77
|
let latest;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@typescript-eslint/no-floating-promises": ["error"],
|
|
13
13
|
"space-before-function-paren": "off",
|
|
14
14
|
"operator-linebreak": "off",
|
|
15
|
-
"max-len": ["warn", { "code":
|
|
15
|
+
"max-len": ["warn", { "code": 300 }],
|
|
16
16
|
"semi": ["warn", "always"],
|
|
17
17
|
"quotes": ["warn", "single", { "avoidEscape": true }],
|
|
18
18
|
"object-curly-spacing": ["warn", "always"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-checkmate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Security-first PR gate for CI: secret scanning, grype dependency scanning, SBOM — plus linting and formatting across 11 languages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
".prettierrc",
|
|
35
35
|
"cspell.json",
|
|
36
36
|
"src/config/pr-checkmate-workflow.yml",
|
|
37
|
+
"src/config/pr-checkmate-workflow-docker.yml",
|
|
37
38
|
"pr-checkmate.schema.json",
|
|
38
39
|
".markdownlint.json",
|
|
39
40
|
"gitleaks.toml",
|
package/src/config/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"]
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"@typescript-eslint/no-floating-promises": ["error"],
|
|
13
13
|
"space-before-function-paren": "off",
|
|
14
14
|
"operator-linebreak": "off",
|
|
15
|
-
"max-len": ["warn", { "code":
|
|
15
|
+
"max-len": ["warn", { "code": 300 }],
|
|
16
16
|
"semi": ["warn", "always"],
|
|
17
17
|
"quotes": ["warn", "single", { "avoidEscape": true }],
|
|
18
18
|
"object-curly-spacing": ["warn", "always"],
|
|
@@ -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,
|