pr-checkmate 4.1.0 → 4.1.1

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.
@@ -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
- exports.packageSizeCheck = {
158
- name: 'Package Size',
159
- phase: 'informational',
160
- weight: 'heavy',
161
- applies(ctx) {
162
- if (ctx.config.packageSize?.enabled === false)
163
- return false;
164
- // A private package is never published, so there is nothing to compare with
165
- // and no consumer to pay for the growth.
166
- return published(ctx.cwd) !== null;
167
- },
168
- async run(ctx) {
169
- const target = published(ctx.cwd);
170
- if (!target)
171
- return (0, types_js_1.skip)('no publishable package.json');
172
- const tag = ctx.config.packageSize?.compareWith ?? 'latest';
173
- const maxGrowth = ctx.config.packageSize?.maxGrowthPercent ?? DEFAULT_MAX_GROWTH;
174
- const spec = `${target.name}@${tag}`;
175
- const local = await packManifest(ctx.cwd);
176
- if (!('manifest' in local))
177
- return (0, types_js_1.skip)('npm could not describe this package');
178
- const current = local.manifest;
179
- // Never a pass: a green row over a comparison that did not happen is the
180
- // mistake of ticket 072. And never one reason standing in for three — the
181
- // three are different situations for the reader, and only one of them is a
182
- // fact about the package rather than about the run.
183
- const remote = await packManifest(ctx.cwd, spec, target.registry);
184
- if (!('manifest' in remote)) {
185
- // The fact, and nothing past it. The remedy for a private registry is three
186
- // lines in a workflow — `setup-node` with `registry-url`, a scope, and
187
- // `NODE_AUTH_TOKEN`, because npm reads the token through a reference in
188
- // `.npmrc` — and instructions that do not fit cannot be followed. They
189
- // belong on the check's page, which the report already links to.
190
- //
191
- // The package name is left out on purpose: the reader owns it. The host is
192
- // not, because it is the one part they cannot infer — and the difference
193
- // between "we asked the wrong registry" and "we were refused" is exactly
194
- // what this row got wrong before.
195
- // Named only when we know it. Without `publishConfig.registry` npm used
196
- // whatever the client's own config points at, and we do not know what that
197
- // was — "at the registry" would be filler dressed up as information.
198
- const host = target.registry ? new URL(target.registry).host : null;
199
- const at = host ? ` at ${host}` : '';
200
- const reason = {
201
- absent: `nothing published under this name${at} yet`,
202
- unauthenticated: `no credentials for ${host ?? 'the registry'}`,
203
- unreadable: `could not read the published version${at}`,
204
- }[remote.failure];
205
- return (0, types_js_1.skip)(reason);
206
- }
207
- const previous = remote.manifest;
208
- const delta = current.unpackedSize - previous.unpackedSize;
209
- const growth = previous.unpackedSize > 0 ? (delta / previous.unpackedSize) * 100 : 0;
210
- const summary = `${mb(previous.unpackedSize)} → ${mb(current.unpackedSize)} ` +
211
- `(${growth >= 0 ? '+' : ''}${growth.toFixed(1)}%) vs ${previous.version}`;
212
- if (growth <= maxGrowth) {
213
- index_js_1.logger.info(`[Package Size]: ✅ ${summary}`);
214
- return (0, types_js_1.pass)(summary);
215
- }
216
- (0, findings_js_1.logSample)('Package Size', `${summary}:`, growthByFile(previous.files, current.files));
217
- return (0, types_js_1.warn)(summary);
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 const packageSizeCheck = {
150
- name: 'Package Size',
151
- phase: 'informational',
152
- weight: 'heavy',
153
- applies(ctx) {
154
- if (ctx.config.packageSize?.enabled === false)
155
- return false;
156
- // A private package is never published, so there is nothing to compare with
157
- // and no consumer to pay for the growth.
158
- return published(ctx.cwd) !== null;
159
- },
160
- async run(ctx) {
161
- const target = published(ctx.cwd);
162
- if (!target)
163
- return skip('no publishable package.json');
164
- const tag = ctx.config.packageSize?.compareWith ?? 'latest';
165
- const maxGrowth = ctx.config.packageSize?.maxGrowthPercent ?? DEFAULT_MAX_GROWTH;
166
- const spec = `${target.name}@${tag}`;
167
- const local = await packManifest(ctx.cwd);
168
- if (!('manifest' in local))
169
- return skip('npm could not describe this package');
170
- const current = local.manifest;
171
- // Never a pass: a green row over a comparison that did not happen is the
172
- // mistake of ticket 072. And never one reason standing in for three — the
173
- // three are different situations for the reader, and only one of them is a
174
- // fact about the package rather than about the run.
175
- const remote = await packManifest(ctx.cwd, spec, target.registry);
176
- if (!('manifest' in remote)) {
177
- // The fact, and nothing past it. The remedy for a private registry is three
178
- // lines in a workflow — `setup-node` with `registry-url`, a scope, and
179
- // `NODE_AUTH_TOKEN`, because npm reads the token through a reference in
180
- // `.npmrc` — and instructions that do not fit cannot be followed. They
181
- // belong on the check's page, which the report already links to.
182
- //
183
- // The package name is left out on purpose: the reader owns it. The host is
184
- // not, because it is the one part they cannot infer — and the difference
185
- // between "we asked the wrong registry" and "we were refused" is exactly
186
- // what this row got wrong before.
187
- // Named only when we know it. Without `publishConfig.registry` npm used
188
- // whatever the client's own config points at, and we do not know what that
189
- // was — "at the registry" would be filler dressed up as information.
190
- const host = target.registry ? new URL(target.registry).host : null;
191
- const at = host ? ` at ${host}` : '';
192
- const reason = {
193
- absent: `nothing published under this name${at} yet`,
194
- unauthenticated: `no credentials for ${host ?? 'the registry'}`,
195
- unreadable: `could not read the published version${at}`,
196
- }[remote.failure];
197
- return skip(reason);
198
- }
199
- const previous = remote.manifest;
200
- const delta = current.unpackedSize - previous.unpackedSize;
201
- const growth = previous.unpackedSize > 0 ? (delta / previous.unpackedSize) * 100 : 0;
202
- const summary = `${mb(previous.unpackedSize)} → ${mb(current.unpackedSize)} ` +
203
- `(${growth >= 0 ? '+' : ''}${growth.toFixed(1)}%) vs ${previous.version}`;
204
- if (growth <= maxGrowth) {
205
- logger.info(`[Package Size]: ✅ ${summary}`);
206
- return pass(summary);
207
- }
208
- logSample('Package Size', `${summary}:`, growthByFile(previous.files, current.files));
209
- return warn(summary);
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.4"
14
+ "gitleaks-secret-scanner-fixed": "3.0.6"
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.4",
577
- "resolved": "https://registry.npmjs.org/gitleaks-secret-scanner-fixed/-/gitleaks-secret-scanner-fixed-3.0.4.tgz",
578
- "integrity": "sha512-UxNQSWIcSIux7M6Ue/pcEDz+E/UQVicvpoa/a5TCq6PD32B3sSNe3WY7wIpiyddpJIUImF2gFkQbWM+wZQrKFQ==",
576
+ "version": "3.0.6",
577
+ "resolved": "https://registry.npmjs.org/gitleaks-secret-scanner-fixed/-/gitleaks-secret-scanner-fixed-3.0.6.tgz",
578
+ "integrity": "sha512-qW7JSjmuuPGZxn3b6QUu2etB7i5b1xl6Mx6KFodqzw+C4NYifP4YKW0dtKuwc1YYJAI/h3fEM0EftSwFjxy57A==",
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.0",
584
+ "toml": "^5.0.1",
585
585
  "unzipper": "^0.12.3"
586
586
  },
587
587
  "bin": {
@@ -5,7 +5,7 @@
5
5
  "execa": "9.6.1",
6
6
  "yaml": "2.9.1",
7
7
  "inquirer": "14.2.2",
8
- "gitleaks-secret-scanner-fixed": "3.0.4"
8
+ "gitleaks-secret-scanner-fixed": "3.0.6"
9
9
  },
10
10
  "private": true
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-checkmate",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
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.4",
86
+ "gitleaks-secret-scanner-fixed": "3.0.6",
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",