@jsenv/package-publish 1.11.52 → 1.11.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/package-publish",
3
- "version": "1.11.52",
3
+ "version": "1.11.53",
4
4
  "type": "module",
5
5
  "description": "Publish package to one or many registry.",
6
6
  "repository": {
@@ -23,8 +23,8 @@
23
23
  "/src/"
24
24
  ],
25
25
  "dependencies": {
26
- "@jsenv/filesystem": "4.15.21",
27
- "@jsenv/humanize": "1.8.0",
26
+ "@jsenv/filesystem": "4.15.22",
27
+ "@jsenv/humanize": "1.8.1",
28
28
  "semver": "7.8.5"
29
29
  },
30
30
  "devDependencies": {
@@ -4,6 +4,7 @@ import { exec } from "node:child_process";
4
4
  import { readFileSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { setNpmConfig } from "./set_npm_config.js";
7
+ import { waitForStagedVersionToLand } from "./staged_version.js";
7
8
 
8
9
  export const publish = async ({
9
10
  logger,
@@ -63,8 +64,9 @@ export const publish = async ({
63
64
  [computeRegistryKey(packageObject.name)]: registryUrl,
64
65
  }),
65
66
  );
67
+ let publishResult;
66
68
  try {
67
- const publishResult = await new Promise((resolve, reject) => {
69
+ publishResult = await new Promise((resolve, reject) => {
68
70
  const command = exec(
69
71
  "npm publish --no-workspaces",
70
72
  {
@@ -106,23 +108,10 @@ export const publish = async ({
106
108
  reason: "already-published",
107
109
  });
108
110
  } else if (error.message.includes("previously staged version")) {
109
- // The registry accepted a tarball for that version (from a run
110
- // interrupted before npm confirmed, or one whose PUT it took
111
- // without exposing the version yet) and refuses any further
112
- // publish of it until it becomes visible. The publish did go
113
- // through, so wait for the version to land.
114
- resolve(
115
- waitForStagedVersionToLand({
116
- logger,
117
- registryUrl,
118
- packageName: packageObject.name,
119
- packageVersion: packageObject.version,
120
- token,
121
- }).then(() => ({
122
- success: true,
123
- reason: "already-published",
124
- })),
125
- );
111
+ resolve({
112
+ success: true,
113
+ reason: "staged",
114
+ });
126
115
  }
127
116
  // github publish conflict
128
117
  else if (
@@ -158,14 +147,28 @@ export const publish = async ({
158
147
  });
159
148
  if (publishResult.reason === "already-published") {
160
149
  publishTask.setRightText(`(already published)`);
150
+ } else if (publishResult.reason === "staged") {
151
+ publishTask.setRightText(`(staged)`);
161
152
  }
162
153
  publishTask.done();
163
- return publishResult;
164
154
  } finally {
165
155
  restoreProcessEnv();
166
156
  restorePackageFile();
167
157
  restoreNpmConfigFile();
168
158
  }
159
+ if (publishResult.reason === "staged") {
160
+ await waitForStagedVersionToLand({
161
+ registryUrl,
162
+ packageName: packageObject.name,
163
+ packageVersion: packageObject.version,
164
+ token,
165
+ });
166
+ return {
167
+ success: true,
168
+ reason: "already-published",
169
+ };
170
+ }
171
+ return publishResult;
169
172
  } catch (e) {
170
173
  publishTask.fail();
171
174
  console.error(e.stack);
@@ -198,75 +201,3 @@ const computeRegistryKey = (packageName) => {
198
201
  }
199
202
  return `registry`;
200
203
  };
201
-
202
- // A version becomes available in two steps: npm stages it, then the registry
203
- // makes it visible. While it is staged the registry answers 409 to a publish of
204
- // that same version, and once staged that version can never be published again,
205
- // so waiting for it is the only way through.
206
- const STAGED_VERSION_TIMEOUT_MS = 120_000;
207
- const STAGED_VERSION_POLL_INTERVAL_MS = 5_000;
208
-
209
- const waitForStagedVersionToLand = async ({
210
- logger,
211
- registryUrl,
212
- packageName,
213
- packageVersion,
214
- token,
215
- }) => {
216
- logger.info(
217
- `${packageName}@${packageVersion} is staged on ${registryUrl}, waiting for the registry to publish it`,
218
- );
219
- const msBeforeTimeout = Date.now() + STAGED_VERSION_TIMEOUT_MS;
220
- while (true) {
221
- const versionIsInRegistry = await checkVersionInRegistry({
222
- registryUrl,
223
- packageName,
224
- packageVersion,
225
- token,
226
- });
227
- if (versionIsInRegistry) {
228
- return;
229
- }
230
- if (Date.now() > msBeforeTimeout) {
231
- throw new Error(
232
- `${packageName}@${packageVersion} is staged on ${registryUrl} but did not get published. Bump the version, a staged version cannot be published again.`,
233
- );
234
- }
235
- await new Promise((resolve) => {
236
- setTimeout(resolve, STAGED_VERSION_POLL_INTERVAL_MS);
237
- });
238
- }
239
- };
240
-
241
- const checkVersionInRegistry = async ({
242
- registryUrl,
243
- packageName,
244
- packageVersion,
245
- token,
246
- }) => {
247
- let response;
248
- try {
249
- response = await fetch(`${registryUrl}/${packageName}`, {
250
- headers: {
251
- "accept":
252
- "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
253
- // the registry is served by a cache; without this the version can stay
254
- // invisible long after it landed
255
- "cache-control": "no-cache",
256
- ...(token
257
- ? {
258
- authorization: `token ${token}`,
259
- }
260
- : {}),
261
- },
262
- });
263
- } catch {
264
- // a network hiccup is one more reason for the version not to be there yet
265
- return false;
266
- }
267
- if (response.status !== 200) {
268
- return false;
269
- }
270
- const packageObject = await response.json();
271
- return Boolean(packageObject.versions[packageVersion]);
272
- };
@@ -0,0 +1,142 @@
1
+ /*
2
+ * A version lands on the registry in two steps: the registry takes the tarball
3
+ * ("staged"), then it exposes the version. While a version is staged the
4
+ * registry answers 409 'Cannot publish over previously staged version "x.y.z"'
5
+ * to any publish of it, so waiting for it to be exposed is the only way through.
6
+ *
7
+ * The packument says nothing about a staged version, but its tarball is already
8
+ * served: that is what tells a staged version apart from one that was never
9
+ * published.
10
+ */
11
+
12
+ import { createTaskLog } from "@jsenv/humanize";
13
+
14
+ export const VERSION_STATUS = {
15
+ ABSENT: "absent",
16
+ STAGED: "staged",
17
+ PUBLISHED: "published",
18
+ };
19
+
20
+ const STAGED_VERSION_TIMEOUT_MS = 300_000;
21
+ const STAGED_VERSION_POLL_INTERVAL_MS = 5_000;
22
+
23
+ export const checkVersionStatusInRegistry = async ({
24
+ registryUrl,
25
+ packageName,
26
+ packageVersion,
27
+ token,
28
+ }) => {
29
+ const versionIsInRegistry = await checkVersionIsInRegistry({
30
+ registryUrl,
31
+ packageName,
32
+ packageVersion,
33
+ token,
34
+ });
35
+ if (versionIsInRegistry) {
36
+ return VERSION_STATUS.PUBLISHED;
37
+ }
38
+ const tarballIsInRegistry = await checkTarballIsInRegistry({
39
+ registryUrl,
40
+ packageName,
41
+ packageVersion,
42
+ token,
43
+ });
44
+ if (tarballIsInRegistry) {
45
+ return VERSION_STATUS.STAGED;
46
+ }
47
+ return VERSION_STATUS.ABSENT;
48
+ };
49
+
50
+ export const waitForStagedVersionToLand = async ({
51
+ registryUrl,
52
+ packageName,
53
+ packageVersion,
54
+ token,
55
+ }) => {
56
+ const waitTask = createTaskLog(
57
+ `wait for ${packageName}@${packageVersion} to be published by ${registryUrl}`,
58
+ );
59
+ const msBeforeTimeout = Date.now() + STAGED_VERSION_TIMEOUT_MS;
60
+ try {
61
+ while (true) {
62
+ const versionIsInRegistry = await checkVersionIsInRegistry({
63
+ registryUrl,
64
+ packageName,
65
+ packageVersion,
66
+ token,
67
+ });
68
+ if (versionIsInRegistry) {
69
+ waitTask.done();
70
+ return;
71
+ }
72
+ if (Date.now() > msBeforeTimeout) {
73
+ throw new Error(
74
+ `${packageName}@${packageVersion} is staged on ${registryUrl} but did not get published. Run the publish again to keep waiting; a staged version cannot be published again.`,
75
+ );
76
+ }
77
+ await new Promise((resolve) => {
78
+ setTimeout(resolve, STAGED_VERSION_POLL_INTERVAL_MS);
79
+ });
80
+ }
81
+ } catch (e) {
82
+ waitTask.fail();
83
+ throw e;
84
+ }
85
+ };
86
+
87
+ const checkVersionIsInRegistry = async ({
88
+ registryUrl,
89
+ packageName,
90
+ packageVersion,
91
+ token,
92
+ }) => {
93
+ const response = await fetchRegistry(`${registryUrl}/${packageName}`, {
94
+ method: "GET",
95
+ token,
96
+ });
97
+ if (!response || response.status !== 200) {
98
+ return false;
99
+ }
100
+ const packageObject = await response.json();
101
+ return Boolean(packageObject.versions[packageVersion]);
102
+ };
103
+
104
+ const checkTarballIsInRegistry = async ({
105
+ registryUrl,
106
+ packageName,
107
+ packageVersion,
108
+ token,
109
+ }) => {
110
+ const response = await fetchRegistry(
111
+ computeTarballUrl({ registryUrl, packageName, packageVersion }),
112
+ { method: "HEAD", token },
113
+ );
114
+ return Boolean(response) && response.status === 200;
115
+ };
116
+
117
+ const computeTarballUrl = ({ registryUrl, packageName, packageVersion }) => {
118
+ const packageBasename =
119
+ packageName[0] === "@"
120
+ ? packageName.slice(packageName.indexOf("/") + 1)
121
+ : packageName;
122
+ return `${registryUrl}/${packageName}/-/${packageBasename}-${packageVersion}.tgz`;
123
+ };
124
+
125
+ const fetchRegistry = async (url, { method, token }) => {
126
+ try {
127
+ return await fetch(url, {
128
+ method,
129
+ headers: {
130
+ "accept":
131
+ "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
132
+ // the registry is served by a cache; without this a version can stay
133
+ // invisible long after it landed
134
+ "cache-control": "no-cache",
135
+ ...(token ? { authorization: `token ${token}` } : {}),
136
+ },
137
+ });
138
+ } catch {
139
+ // a network hiccup is one more reason for the version not to be there yet
140
+ return null;
141
+ }
142
+ };