@jsenv/package-publish 1.11.49 → 1.11.51
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.
|
|
3
|
+
"version": "1.11.51",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Publish package to one or many registry.",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"/src/"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@jsenv/filesystem": "4.15.
|
|
26
|
+
"@jsenv/filesystem": "4.15.20",
|
|
27
27
|
"@jsenv/humanize": "1.7.8",
|
|
28
28
|
"semver": "7.8.5"
|
|
29
29
|
},
|
|
@@ -8,7 +8,7 @@ export const fetchLatestInRegistry = async ({
|
|
|
8
8
|
token,
|
|
9
9
|
}) => {
|
|
10
10
|
const requestUrl = `${registryUrl}/${packageName}`;
|
|
11
|
-
const response = await
|
|
11
|
+
const response = await fetchWithRetryOnTransientError(requestUrl, {
|
|
12
12
|
method: "GET",
|
|
13
13
|
headers: {
|
|
14
14
|
// "user-agent": "jsenv",
|
|
@@ -38,6 +38,40 @@ export const fetchLatestInRegistry = async ({
|
|
|
38
38
|
return packageObject.versions[packageObject["dist-tags"].latest];
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
// The registry (or a middlebox) sometimes closes a keep-alive socket while a
|
|
42
|
+
// request is in flight, surfacing as "fetch failed" with ECONNRESET. Fetching a
|
|
43
|
+
// whole workspace fires many requests at once so one reset per run is common.
|
|
44
|
+
// These failures are transient by nature and cannot be prevented client-side,
|
|
45
|
+
// so retry a couple of times before giving up.
|
|
46
|
+
const TRANSIENT_NETWORK_ERROR_CODES = [
|
|
47
|
+
"ECONNRESET",
|
|
48
|
+
"ETIMEDOUT",
|
|
49
|
+
"EPIPE",
|
|
50
|
+
"EAI_AGAIN",
|
|
51
|
+
"UND_ERR_SOCKET",
|
|
52
|
+
];
|
|
53
|
+
const fetchWithRetryOnTransientError = async (url, options) => {
|
|
54
|
+
let attemptCount = 0;
|
|
55
|
+
while (true) {
|
|
56
|
+
attemptCount++;
|
|
57
|
+
try {
|
|
58
|
+
return await fetch(url, options);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
const errorCode = e.cause?.code;
|
|
61
|
+
if (
|
|
62
|
+
attemptCount >= 3 ||
|
|
63
|
+
!TRANSIENT_NETWORK_ERROR_CODES.includes(errorCode)
|
|
64
|
+
) {
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
const delay = attemptCount * 500;
|
|
68
|
+
await new Promise((resolve) => {
|
|
69
|
+
setTimeout(resolve, delay);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
41
75
|
const writeUnexpectedResponseStatus = ({
|
|
42
76
|
requestUrl,
|
|
43
77
|
responseStatus,
|
package/src/internal/publish.js
CHANGED
|
@@ -64,7 +64,7 @@ export const publish = async ({
|
|
|
64
64
|
}),
|
|
65
65
|
);
|
|
66
66
|
try {
|
|
67
|
-
const
|
|
67
|
+
const publishResult = await new Promise((resolve, reject) => {
|
|
68
68
|
const command = exec(
|
|
69
69
|
"npm publish --no-workspaces",
|
|
70
70
|
{
|
|
@@ -105,6 +105,24 @@ export const publish = async ({
|
|
|
105
105
|
success: true,
|
|
106
106
|
reason: "already-published",
|
|
107
107
|
});
|
|
108
|
+
} 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
|
+
);
|
|
108
126
|
}
|
|
109
127
|
// github publish conflict
|
|
110
128
|
else if (
|
|
@@ -138,14 +156,11 @@ export const publish = async ({
|
|
|
138
156
|
});
|
|
139
157
|
}
|
|
140
158
|
});
|
|
141
|
-
if (reason === "already-published") {
|
|
159
|
+
if (publishResult.reason === "already-published") {
|
|
142
160
|
publishTask.setRightText(`(already published)`);
|
|
143
161
|
}
|
|
144
162
|
publishTask.done();
|
|
145
|
-
return
|
|
146
|
-
success: true,
|
|
147
|
-
reason,
|
|
148
|
-
};
|
|
163
|
+
return publishResult;
|
|
149
164
|
} finally {
|
|
150
165
|
restoreProcessEnv();
|
|
151
166
|
restorePackageFile();
|
|
@@ -183,3 +198,75 @@ const computeRegistryKey = (packageName) => {
|
|
|
183
198
|
}
|
|
184
199
|
return `registry`;
|
|
185
200
|
};
|
|
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
|
+
};
|