pr-checkmate 4.1.0 → 4.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/dist/cjs/core/checks/quality/package-size.d.ts +8 -0
- package/dist/cjs/core/checks/quality/package-size.js +77 -64
- package/dist/esm/core/checks/quality/package-size.d.ts +8 -0
- package/dist/esm/core/checks/quality/package-size.js +76 -64
- package/install-manifests/core/package-lock.json +5 -5
- package/install-manifests/core/package.json +1 -1
- package/package.json +4 -3
|
@@ -20,6 +20,13 @@ interface PackManifest {
|
|
|
20
20
|
* package" and the check into a permanent skip.
|
|
21
21
|
*/
|
|
22
22
|
export declare function pickManifest(parsed: unknown): PackManifest | null;
|
|
23
|
+
type PackResult = {
|
|
24
|
+
manifest: PackManifest;
|
|
25
|
+
} | {
|
|
26
|
+
failure: QueryFailure;
|
|
27
|
+
};
|
|
28
|
+
/** Describes a package as `npm pack` would; swapped out by tests that must not reach a registry. */
|
|
29
|
+
export type PackFn = (cwd: string, spec?: string, registry?: string) => Promise<PackResult>;
|
|
23
30
|
/** Why we could not read a published version. */
|
|
24
31
|
type QueryFailure = 'absent' | 'unauthenticated' | 'unreadable';
|
|
25
32
|
/**
|
|
@@ -54,5 +61,6 @@ export declare function classifyQueryFailure(stderr: string): QueryFailure;
|
|
|
54
61
|
* WASM binary landed — and a red build for it would be wrong. `severity:
|
|
55
62
|
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
56
63
|
*/
|
|
64
|
+
export declare function createPackageSizeCheck(pack?: PackFn): Check;
|
|
57
65
|
export declare const packageSizeCheck: Check;
|
|
58
66
|
export {};
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.packageSizeCheck = void 0;
|
|
7
7
|
exports.pickManifest = pickManifest;
|
|
8
8
|
exports.classifyQueryFailure = classifyQueryFailure;
|
|
9
|
+
exports.createPackageSizeCheck = createPackageSizeCheck;
|
|
9
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
12
|
const execa_js_1 = require("../../../utils/execa.js");
|
|
@@ -16,6 +17,13 @@ const findings_js_1 = require("../findings.js");
|
|
|
16
17
|
const DEFAULT_MAX_GROWTH = 20;
|
|
17
18
|
/** How many of the files behind the growth are named. */
|
|
18
19
|
const NAMED_FILES = 5;
|
|
20
|
+
/**
|
|
21
|
+
* How long one registry request may take, and how long the whole `npm pack` may
|
|
22
|
+
* run. A registry that does not answer must end as "could not read", not as a
|
|
23
|
+
* check that holds the report for npm's default retry schedule.
|
|
24
|
+
*/
|
|
25
|
+
const NPM_FETCH_TIMEOUT_MS = 10000;
|
|
26
|
+
const NPM_TIMEOUT_MS = 30000;
|
|
19
27
|
/**
|
|
20
28
|
* The manifest out of whatever `npm pack --json` printed.
|
|
21
29
|
*
|
|
@@ -59,9 +67,11 @@ async function packManifest(cwd, spec, registry) {
|
|
|
59
67
|
'--dry-run',
|
|
60
68
|
'--json',
|
|
61
69
|
'--ignore-scripts',
|
|
70
|
+
'--fetch-retries=0',
|
|
71
|
+
`--fetch-timeout=${NPM_FETCH_TIMEOUT_MS}`,
|
|
62
72
|
...(registry ? ['--registry', registry] : []),
|
|
63
73
|
];
|
|
64
|
-
const result = await (0, execa_js_1.execa)('npm', args, { cwd, reject: false });
|
|
74
|
+
const result = await (0, execa_js_1.execa)('npm', args, { cwd, reject: false, timeout: NPM_TIMEOUT_MS });
|
|
65
75
|
if (result.exitCode !== 0)
|
|
66
76
|
return { failure: classifyQueryFailure(result.stderr ?? '') };
|
|
67
77
|
try {
|
|
@@ -154,66 +164,69 @@ function published(cwd) {
|
|
|
154
164
|
* WASM binary landed — and a red build for it would be wrong. `severity:
|
|
155
165
|
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
156
166
|
*/
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
167
|
+
function createPackageSizeCheck(pack = packManifest) {
|
|
168
|
+
return {
|
|
169
|
+
name: 'Package Size',
|
|
170
|
+
phase: 'informational',
|
|
171
|
+
weight: 'heavy',
|
|
172
|
+
applies(ctx) {
|
|
173
|
+
if (ctx.config.packageSize?.enabled === false)
|
|
174
|
+
return false;
|
|
175
|
+
// A private package is never published, so there is nothing to compare with
|
|
176
|
+
// and no consumer to pay for the growth.
|
|
177
|
+
return published(ctx.cwd) !== null;
|
|
178
|
+
},
|
|
179
|
+
async run(ctx) {
|
|
180
|
+
const target = published(ctx.cwd);
|
|
181
|
+
if (!target)
|
|
182
|
+
return (0, types_js_1.skip)('no publishable package.json');
|
|
183
|
+
const tag = ctx.config.packageSize?.compareWith ?? 'latest';
|
|
184
|
+
const maxGrowth = ctx.config.packageSize?.maxGrowthPercent ?? DEFAULT_MAX_GROWTH;
|
|
185
|
+
const spec = `${target.name}@${tag}`;
|
|
186
|
+
const local = await pack(ctx.cwd);
|
|
187
|
+
if (!('manifest' in local))
|
|
188
|
+
return (0, types_js_1.skip)('npm could not describe this package');
|
|
189
|
+
const current = local.manifest;
|
|
190
|
+
// Never a pass: a green row over a comparison that did not happen is the
|
|
191
|
+
// mistake of ticket 072. And never one reason standing in for three — the
|
|
192
|
+
// three are different situations for the reader, and only one of them is a
|
|
193
|
+
// fact about the package rather than about the run.
|
|
194
|
+
const remote = await pack(ctx.cwd, spec, target.registry);
|
|
195
|
+
if (!('manifest' in remote)) {
|
|
196
|
+
// The fact, and nothing past it. The remedy for a private registry is three
|
|
197
|
+
// lines in a workflow — `setup-node` with `registry-url`, a scope, and
|
|
198
|
+
// `NODE_AUTH_TOKEN`, because npm reads the token through a reference in
|
|
199
|
+
// `.npmrc` — and instructions that do not fit cannot be followed. They
|
|
200
|
+
// belong on the check's page, which the report already links to.
|
|
201
|
+
//
|
|
202
|
+
// The package name is left out on purpose: the reader owns it. The host is
|
|
203
|
+
// not, because it is the one part they cannot infer — and the difference
|
|
204
|
+
// between "we asked the wrong registry" and "we were refused" is exactly
|
|
205
|
+
// what this row got wrong before.
|
|
206
|
+
// Named only when we know it. Without `publishConfig.registry` npm used
|
|
207
|
+
// whatever the client's own config points at, and we do not know what that
|
|
208
|
+
// was — "at the registry" would be filler dressed up as information.
|
|
209
|
+
const host = target.registry ? new URL(target.registry).host : null;
|
|
210
|
+
const at = host ? ` at ${host}` : '';
|
|
211
|
+
const reason = {
|
|
212
|
+
absent: `nothing published under this name${at} yet`,
|
|
213
|
+
unauthenticated: `no credentials for ${host ?? 'the registry'}`,
|
|
214
|
+
unreadable: `could not read the published version${at}`,
|
|
215
|
+
}[remote.failure];
|
|
216
|
+
return (0, types_js_1.skip)(reason);
|
|
217
|
+
}
|
|
218
|
+
const previous = remote.manifest;
|
|
219
|
+
const delta = current.unpackedSize - previous.unpackedSize;
|
|
220
|
+
const growth = previous.unpackedSize > 0 ? (delta / previous.unpackedSize) * 100 : 0;
|
|
221
|
+
const summary = `${mb(previous.unpackedSize)} → ${mb(current.unpackedSize)} ` +
|
|
222
|
+
`(${growth >= 0 ? '+' : ''}${growth.toFixed(1)}%) vs ${previous.version}`;
|
|
223
|
+
if (growth <= maxGrowth) {
|
|
224
|
+
index_js_1.logger.info(`[Package Size]: ✅ ${summary}`);
|
|
225
|
+
return (0, types_js_1.pass)(summary);
|
|
226
|
+
}
|
|
227
|
+
(0, findings_js_1.logSample)('Package Size', `${summary}:`, growthByFile(previous.files, current.files));
|
|
228
|
+
return (0, types_js_1.warn)(summary);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
exports.packageSizeCheck = createPackageSizeCheck();
|
|
@@ -20,6 +20,13 @@ interface PackManifest {
|
|
|
20
20
|
* package" and the check into a permanent skip.
|
|
21
21
|
*/
|
|
22
22
|
export declare function pickManifest(parsed: unknown): PackManifest | null;
|
|
23
|
+
type PackResult = {
|
|
24
|
+
manifest: PackManifest;
|
|
25
|
+
} | {
|
|
26
|
+
failure: QueryFailure;
|
|
27
|
+
};
|
|
28
|
+
/** Describes a package as `npm pack` would; swapped out by tests that must not reach a registry. */
|
|
29
|
+
export type PackFn = (cwd: string, spec?: string, registry?: string) => Promise<PackResult>;
|
|
23
30
|
/** Why we could not read a published version. */
|
|
24
31
|
type QueryFailure = 'absent' | 'unauthenticated' | 'unreadable';
|
|
25
32
|
/**
|
|
@@ -54,5 +61,6 @@ export declare function classifyQueryFailure(stderr: string): QueryFailure;
|
|
|
54
61
|
* WASM binary landed — and a red build for it would be wrong. `severity:
|
|
55
62
|
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
56
63
|
*/
|
|
64
|
+
export declare function createPackageSizeCheck(pack?: PackFn): Check;
|
|
57
65
|
export declare const packageSizeCheck: Check;
|
|
58
66
|
export {};
|
|
@@ -8,6 +8,13 @@ import { logSample } from '../findings.js';
|
|
|
8
8
|
const DEFAULT_MAX_GROWTH = 20;
|
|
9
9
|
/** How many of the files behind the growth are named. */
|
|
10
10
|
const NAMED_FILES = 5;
|
|
11
|
+
/**
|
|
12
|
+
* How long one registry request may take, and how long the whole `npm pack` may
|
|
13
|
+
* run. A registry that does not answer must end as "could not read", not as a
|
|
14
|
+
* check that holds the report for npm's default retry schedule.
|
|
15
|
+
*/
|
|
16
|
+
const NPM_FETCH_TIMEOUT_MS = 10000;
|
|
17
|
+
const NPM_TIMEOUT_MS = 30000;
|
|
11
18
|
/**
|
|
12
19
|
* The manifest out of whatever `npm pack --json` printed.
|
|
13
20
|
*
|
|
@@ -51,9 +58,11 @@ async function packManifest(cwd, spec, registry) {
|
|
|
51
58
|
'--dry-run',
|
|
52
59
|
'--json',
|
|
53
60
|
'--ignore-scripts',
|
|
61
|
+
'--fetch-retries=0',
|
|
62
|
+
`--fetch-timeout=${NPM_FETCH_TIMEOUT_MS}`,
|
|
54
63
|
...(registry ? ['--registry', registry] : []),
|
|
55
64
|
];
|
|
56
|
-
const result = await execa('npm', args, { cwd, reject: false });
|
|
65
|
+
const result = await execa('npm', args, { cwd, reject: false, timeout: NPM_TIMEOUT_MS });
|
|
57
66
|
if (result.exitCode !== 0)
|
|
58
67
|
return { failure: classifyQueryFailure(result.stderr ?? '') };
|
|
59
68
|
try {
|
|
@@ -146,66 +155,69 @@ function published(cwd) {
|
|
|
146
155
|
* WASM binary landed — and a red build for it would be wrong. `severity:
|
|
147
156
|
* { "Package Size": "error" }` makes it a gate for a repository that wants one.
|
|
148
157
|
*/
|
|
149
|
-
export
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
158
|
+
export function createPackageSizeCheck(pack = packManifest) {
|
|
159
|
+
return {
|
|
160
|
+
name: 'Package Size',
|
|
161
|
+
phase: 'informational',
|
|
162
|
+
weight: 'heavy',
|
|
163
|
+
applies(ctx) {
|
|
164
|
+
if (ctx.config.packageSize?.enabled === false)
|
|
165
|
+
return false;
|
|
166
|
+
// A private package is never published, so there is nothing to compare with
|
|
167
|
+
// and no consumer to pay for the growth.
|
|
168
|
+
return published(ctx.cwd) !== null;
|
|
169
|
+
},
|
|
170
|
+
async run(ctx) {
|
|
171
|
+
const target = published(ctx.cwd);
|
|
172
|
+
if (!target)
|
|
173
|
+
return skip('no publishable package.json');
|
|
174
|
+
const tag = ctx.config.packageSize?.compareWith ?? 'latest';
|
|
175
|
+
const maxGrowth = ctx.config.packageSize?.maxGrowthPercent ?? DEFAULT_MAX_GROWTH;
|
|
176
|
+
const spec = `${target.name}@${tag}`;
|
|
177
|
+
const local = await pack(ctx.cwd);
|
|
178
|
+
if (!('manifest' in local))
|
|
179
|
+
return skip('npm could not describe this package');
|
|
180
|
+
const current = local.manifest;
|
|
181
|
+
// Never a pass: a green row over a comparison that did not happen is the
|
|
182
|
+
// mistake of ticket 072. And never one reason standing in for three — the
|
|
183
|
+
// three are different situations for the reader, and only one of them is a
|
|
184
|
+
// fact about the package rather than about the run.
|
|
185
|
+
const remote = await pack(ctx.cwd, spec, target.registry);
|
|
186
|
+
if (!('manifest' in remote)) {
|
|
187
|
+
// The fact, and nothing past it. The remedy for a private registry is three
|
|
188
|
+
// lines in a workflow — `setup-node` with `registry-url`, a scope, and
|
|
189
|
+
// `NODE_AUTH_TOKEN`, because npm reads the token through a reference in
|
|
190
|
+
// `.npmrc` — and instructions that do not fit cannot be followed. They
|
|
191
|
+
// belong on the check's page, which the report already links to.
|
|
192
|
+
//
|
|
193
|
+
// The package name is left out on purpose: the reader owns it. The host is
|
|
194
|
+
// not, because it is the one part they cannot infer — and the difference
|
|
195
|
+
// between "we asked the wrong registry" and "we were refused" is exactly
|
|
196
|
+
// what this row got wrong before.
|
|
197
|
+
// Named only when we know it. Without `publishConfig.registry` npm used
|
|
198
|
+
// whatever the client's own config points at, and we do not know what that
|
|
199
|
+
// was — "at the registry" would be filler dressed up as information.
|
|
200
|
+
const host = target.registry ? new URL(target.registry).host : null;
|
|
201
|
+
const at = host ? ` at ${host}` : '';
|
|
202
|
+
const reason = {
|
|
203
|
+
absent: `nothing published under this name${at} yet`,
|
|
204
|
+
unauthenticated: `no credentials for ${host ?? 'the registry'}`,
|
|
205
|
+
unreadable: `could not read the published version${at}`,
|
|
206
|
+
}[remote.failure];
|
|
207
|
+
return skip(reason);
|
|
208
|
+
}
|
|
209
|
+
const previous = remote.manifest;
|
|
210
|
+
const delta = current.unpackedSize - previous.unpackedSize;
|
|
211
|
+
const growth = previous.unpackedSize > 0 ? (delta / previous.unpackedSize) * 100 : 0;
|
|
212
|
+
const summary = `${mb(previous.unpackedSize)} → ${mb(current.unpackedSize)} ` +
|
|
213
|
+
`(${growth >= 0 ? '+' : ''}${growth.toFixed(1)}%) vs ${previous.version}`;
|
|
214
|
+
if (growth <= maxGrowth) {
|
|
215
|
+
logger.info(`[Package Size]: ✅ ${summary}`);
|
|
216
|
+
return pass(summary);
|
|
217
|
+
}
|
|
218
|
+
logSample('Package Size', `${summary}:`, growthByFile(previous.files, current.files));
|
|
219
|
+
return warn(summary);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export const packageSizeCheck = createPackageSizeCheck();
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"execa": "9.6.1",
|
|
12
12
|
"yaml": "2.9.1",
|
|
13
13
|
"inquirer": "14.2.2",
|
|
14
|
-
"gitleaks-secret-scanner-fixed": "3.0
|
|
14
|
+
"gitleaks-secret-scanner-fixed": "3.1.0"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"node_modules/@inquirer/ansi": {
|
|
@@ -573,15 +573,15 @@
|
|
|
573
573
|
}
|
|
574
574
|
},
|
|
575
575
|
"node_modules/gitleaks-secret-scanner-fixed": {
|
|
576
|
-
"version": "3.0
|
|
577
|
-
"resolved": "https://registry.npmjs.org/gitleaks-secret-scanner-fixed/-/gitleaks-secret-scanner-fixed-3.0.
|
|
578
|
-
"integrity": "sha512-
|
|
576
|
+
"version": "3.1.0",
|
|
577
|
+
"resolved": "https://registry.npmjs.org/gitleaks-secret-scanner-fixed/-/gitleaks-secret-scanner-fixed-3.1.0.tgz",
|
|
578
|
+
"integrity": "sha512-QDsoAkzJEdV+iOK4iNneUW/2tHCum8TCIISzGIXG/dGNR26+wy9zoYqpWh+20pH1iK/WUBV3WkcESeO91JC0zg==",
|
|
579
579
|
"hasInstallScript": true,
|
|
580
580
|
"license": "MIT",
|
|
581
581
|
"dependencies": {
|
|
582
582
|
"fs-extra": "^11.2.0",
|
|
583
583
|
"tar": "^7.5.22",
|
|
584
|
-
"toml": "^5.0.
|
|
584
|
+
"toml": "^5.0.1",
|
|
585
585
|
"unzipper": "^0.12.3"
|
|
586
586
|
},
|
|
587
587
|
"bin": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-checkmate",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.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",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"lint": "eslint \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\" --fix",
|
|
58
58
|
"prettier": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\" \"tests/**/*.{ts,tsx}\"",
|
|
59
59
|
"spellcheck": "cspell '**/*.{ts,tsx,js,jsx,md,json}' --no-progress",
|
|
60
|
-
"docs": "typedoc",
|
|
60
|
+
"docs": "patch-package && typedoc",
|
|
61
61
|
"scan:secrets": "gitleaks-secret-scanner --diff-mode ci --html-report gitleaks-report.html",
|
|
62
62
|
"test": "node --require ./scripts/ts-node-nodenext-resolve.cjs --require ts-node/register --test --test-timeout=60000 \"tests/**/*.test.ts\"",
|
|
63
63
|
"test:ci": "bash scripts/test-per-file.sh",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"license": "LicenseRef-PR-Checkmate-Proprietary",
|
|
84
84
|
"dependencies": {
|
|
85
85
|
"execa": "^9.6.0",
|
|
86
|
-
"gitleaks-secret-scanner-fixed": "3.0
|
|
86
|
+
"gitleaks-secret-scanner-fixed": "3.1.0",
|
|
87
87
|
"inquirer": "^14.1.0",
|
|
88
88
|
"yaml": "^2.9.0"
|
|
89
89
|
},
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
"knip": "^6.32.2",
|
|
111
111
|
"license-checker-rseidelsohn": "^5.0.1",
|
|
112
112
|
"markdownlint": "^0.41.1",
|
|
113
|
+
"patch-package": "^8.0.1",
|
|
113
114
|
"prettier": "^3.9.6",
|
|
114
115
|
"pyright": "^1.1.413",
|
|
115
116
|
"standard-version": "^9.5.0",
|