pr-checkmate 3.0.1 → 3.0.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.
@@ -1,4 +1,25 @@
1
1
  import { Check } from '../../types';
2
+ /** Why we could not read a published version. */
3
+ export type QueryFailure = 'absent' | 'unauthenticated' | 'unreadable';
4
+ /**
5
+ * Tell "never published" apart from "we were not let in".
6
+ *
7
+ * Lumped into one sentence they read the same and they are not: one is a fact
8
+ * about the package, the other is about this run's credentials. A skip whose
9
+ * reason is a guess between two causes is the defect of ticket 057, one layer up.
10
+ *
11
+ * A 401 is also evidence in itself, and it points one way: a name nobody ever
12
+ * published answers 404 on the public registry, while 401 comes from a host that
13
+ * has the package and will not show it to a stranger. So the message leads with
14
+ * the token, which is what that situation actually needs.
15
+ *
16
+ * It deliberately does NOT suggest `"private": true` as the way out. That field
17
+ * means "never publish this", and a package behind a private registry is
18
+ * published — advising it would break the publish step of the very repository
19
+ * asking. `packageSize.enabled: false` silences the row and touches nothing
20
+ * else, so that is the opt-out worth naming.
21
+ */
22
+ export declare function classifyQueryFailure(stderr: string): QueryFailure;
2
23
  /**
3
24
  * Reports how much the published package grew since the last release.
4
25
  *
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.packageSizeCheck = void 0;
7
+ exports.classifyQueryFailure = classifyQueryFailure;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const utils_1 = require("../../../utils");
@@ -31,19 +32,57 @@ const NAMED_FILES = 5;
31
32
  *
32
33
  * @param cwd Repository to run in.
33
34
  * @param spec `undefined` for the working tree, or `name@tag` for a release.
35
+ * @param registry Where to look for a published version; the manifest's own
36
+ * `publishConfig.registry` when it names one.
34
37
  */
35
- async function packManifest(cwd, spec) {
36
- const args = ['pack', ...(spec ? [spec] : []), '--dry-run', '--json', '--ignore-scripts'];
38
+ async function packManifest(cwd, spec, registry) {
39
+ const args = [
40
+ 'pack',
41
+ ...(spec ? [spec] : []),
42
+ '--dry-run',
43
+ '--json',
44
+ '--ignore-scripts',
45
+ ...(registry ? ['--registry', registry] : []),
46
+ ];
37
47
  const result = await (0, execa_1.execa)('npm', args, { cwd, reject: false });
38
48
  if (result.exitCode !== 0)
39
- return null;
49
+ return { failure: classifyQueryFailure(result.stderr ?? '') };
40
50
  try {
41
51
  const parsed = JSON.parse(result.stdout);
42
- return Array.isArray(parsed) && parsed.length > 0 ? parsed[0] : null;
52
+ if (Array.isArray(parsed) && parsed.length > 0) {
53
+ return { manifest: parsed[0] };
54
+ }
43
55
  }
44
56
  catch {
45
- return null;
57
+ // Fall through: npm answered with something that is not a manifest.
58
+ }
59
+ return { failure: 'unreadable' };
60
+ }
61
+ /**
62
+ * Tell "never published" apart from "we were not let in".
63
+ *
64
+ * Lumped into one sentence they read the same and they are not: one is a fact
65
+ * about the package, the other is about this run's credentials. A skip whose
66
+ * reason is a guess between two causes is the defect of ticket 057, one layer up.
67
+ *
68
+ * A 401 is also evidence in itself, and it points one way: a name nobody ever
69
+ * published answers 404 on the public registry, while 401 comes from a host that
70
+ * has the package and will not show it to a stranger. So the message leads with
71
+ * the token, which is what that situation actually needs.
72
+ *
73
+ * It deliberately does NOT suggest `"private": true` as the way out. That field
74
+ * means "never publish this", and a package behind a private registry is
75
+ * published — advising it would break the publish step of the very repository
76
+ * asking. `packageSize.enabled: false` silences the row and touches nothing
77
+ * else, so that is the opt-out worth naming.
78
+ */
79
+ function classifyQueryFailure(stderr) {
80
+ if (/\bE?40[13]\b|Unauthorized|Forbidden|authentication token/i.test(stderr)) {
81
+ return 'unauthenticated';
46
82
  }
83
+ if (/\bE?404\b|Not found/i.test(stderr))
84
+ return 'absent';
85
+ return 'unreadable';
47
86
  }
48
87
  /** `1.15 MB`, at the precision a human compares two of these at. */
49
88
  function mb(bytes) {
@@ -66,11 +105,21 @@ function growthByFile(before, after) {
66
105
  .slice(0, NAMED_FILES)
67
106
  .map(f => `${f.path} +${mb(f.delta)}${f.isNew ? ' (new)' : ''}`);
68
107
  }
69
- /** Whether this repository publishes anything to compare. */
70
- function publishedName(cwd) {
108
+ /** What this repository publishes, and where — or `null` if it publishes nothing. */
109
+ function published(cwd) {
71
110
  try {
72
111
  const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cwd, 'package.json'), 'utf-8'));
73
- return pkg.private || !pkg.name ? null : pkg.name;
112
+ if (pkg.private || !pkg.name)
113
+ return null;
114
+ // `publishConfig.registry` is the manifest's own statement of where this
115
+ // package lives, and npm uses it for `publish` but not for a fetch. Asking
116
+ // the default registry instead reports "no published version" for every
117
+ // package on GitHub Packages or a company mirror — which is not a fact about
118
+ // the package, it is us having asked the wrong host. Measured on a real
119
+ // client: the default registry answered 404 for a package published at
120
+ // version 1.2.15.
121
+ const registry = pkg.publishConfig?.registry;
122
+ return registry ? { name: pkg.name, registry } : { name: pkg.name };
74
123
  }
75
124
  catch {
76
125
  return null;
@@ -98,33 +147,57 @@ exports.packageSizeCheck = {
98
147
  return false;
99
148
  // A private package is never published, so there is nothing to compare with
100
149
  // and no consumer to pay for the growth.
101
- return publishedName(ctx.cwd) !== null;
150
+ return published(ctx.cwd) !== null;
102
151
  },
103
152
  async run(ctx) {
104
- const name = publishedName(ctx.cwd);
105
- if (!name)
153
+ const target = published(ctx.cwd);
154
+ if (!target)
106
155
  return (0, types_1.skip)('no publishable package.json');
107
156
  const tag = ctx.config.packageSize?.compareWith ?? 'latest';
108
157
  const maxGrowth = ctx.config.packageSize?.maxGrowthPercent ?? DEFAULT_MAX_GROWTH;
109
- const current = await packManifest(ctx.cwd);
110
- if (!current)
158
+ const spec = `${target.name}@${tag}`;
159
+ const local = await packManifest(ctx.cwd);
160
+ if (!('manifest' in local))
111
161
  return (0, types_1.skip)('npm could not describe this package');
112
- // Registry unreachable, or nothing published yet. Both are "we could not
113
- // look", never "nothing changed" — a pass here would be a green row over a
114
- // comparison that never happened (ticket 072).
115
- const published = await packManifest(ctx.cwd, `${name}@${tag}`);
116
- if (!published) {
117
- return (0, types_1.skip)(`no published ${name}@${tag} to compare with (or registry unreachable)`);
162
+ const current = local.manifest;
163
+ // Never a pass: a green row over a comparison that did not happen is the
164
+ // mistake of ticket 072. And never one reason standing in for three — the
165
+ // three are different situations for the reader, and only one of them is a
166
+ // fact about the package rather than about the run.
167
+ const remote = await packManifest(ctx.cwd, spec, target.registry);
168
+ if (!('manifest' in remote)) {
169
+ // The fact, and nothing past it. The remedy for a private registry is three
170
+ // lines in a workflow — `setup-node` with `registry-url`, a scope, and
171
+ // `NODE_AUTH_TOKEN`, because npm reads the token through a reference in
172
+ // `.npmrc` — and instructions that do not fit cannot be followed. They
173
+ // belong on the check's page, which the report already links to.
174
+ //
175
+ // The package name is left out on purpose: the reader owns it. The host is
176
+ // not, because it is the one part they cannot infer — and the difference
177
+ // between "we asked the wrong registry" and "we were refused" is exactly
178
+ // what this row got wrong before.
179
+ // Named only when we know it. Without `publishConfig.registry` npm used
180
+ // whatever the client's own config points at, and we do not know what that
181
+ // was — "at the registry" would be filler dressed up as information.
182
+ const host = target.registry ? new URL(target.registry).host : null;
183
+ const at = host ? ` at ${host}` : '';
184
+ const reason = {
185
+ absent: `nothing published under this name${at} yet`,
186
+ unauthenticated: `no credentials for ${host ?? 'the registry'}`,
187
+ unreadable: `could not read the published version${at}`,
188
+ }[remote.failure];
189
+ return (0, types_1.skip)(reason);
118
190
  }
119
- const delta = current.unpackedSize - published.unpackedSize;
120
- const growth = published.unpackedSize > 0 ? (delta / published.unpackedSize) * 100 : 0;
121
- const summary = `${mb(published.unpackedSize)} → ${mb(current.unpackedSize)} ` +
122
- `(${growth >= 0 ? '+' : ''}${growth.toFixed(1)}%) vs ${published.version}`;
191
+ const previous = remote.manifest;
192
+ const delta = current.unpackedSize - previous.unpackedSize;
193
+ const growth = previous.unpackedSize > 0 ? (delta / previous.unpackedSize) * 100 : 0;
194
+ const summary = `${mb(previous.unpackedSize)} → ${mb(current.unpackedSize)} ` +
195
+ `(${growth >= 0 ? '+' : ''}${growth.toFixed(1)}%) vs ${previous.version}`;
123
196
  if (growth <= maxGrowth) {
124
197
  utils_1.logger.info(`[Package Size]: ✅ ${summary}`);
125
198
  return (0, types_1.pass)(summary);
126
199
  }
127
- (0, findings_1.logSample)('Package Size', `${summary}:`, growthByFile(published.files, current.files));
200
+ (0, findings_1.logSample)('Package Size', `${summary}:`, growthByFile(previous.files, current.files));
128
201
  return (0, types_1.warn)(summary);
129
202
  },
130
203
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-checkmate",
3
- "version": "3.0.1",
3
+ "version": "3.0.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",