@jsenv/package-publish 1.11.58 → 1.12.0

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.58",
3
+ "version": "1.12.0",
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.27",
27
- "@jsenv/humanize": "1.8.5",
26
+ "@jsenv/filesystem": "4.15.28",
27
+ "@jsenv/humanize": "1.8.6",
28
28
  "semver": "7.8.5"
29
29
  },
30
30
  "devDependencies": {
@@ -4,7 +4,6 @@ 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";
8
7
 
9
8
  export const publish = async ({
10
9
  logger,
@@ -16,12 +15,16 @@ export const publish = async ({
16
15
  }) => {
17
16
  const publishTask = createTaskLog(`publish ${packageSlug} on ${registryUrl}`);
18
17
  try {
19
- // process.env.NODE_AUTH_TOKEN
20
- const previousValue = process.env.NODE_AUTH_TOKEN;
21
- const restoreProcessEnv = () => {
22
- process.env.NODE_AUTH_TOKEN = previousValue;
23
- };
24
- process.env.NODE_AUTH_TOKEN = token;
18
+ // without a token npm authenticates by itself, for instance through
19
+ // trusted publishing (OIDC) in a GitHub workflow
20
+ let restoreProcessEnv = () => {};
21
+ if (token) {
22
+ const previousValue = process.env.NODE_AUTH_TOKEN;
23
+ restoreProcessEnv = () => {
24
+ process.env.NODE_AUTH_TOKEN = previousValue;
25
+ };
26
+ process.env.NODE_AUTH_TOKEN = token;
27
+ }
25
28
  // updating package.json to publish on the correct registry
26
29
  let restorePackageFile = () => {};
27
30
  const rootPackageFileUrl = new URL("./package.json", rootDirectoryUrl);
@@ -41,7 +44,7 @@ export const publish = async ({
41
44
  JSON.stringify(packageObject, null, " "),
42
45
  );
43
46
  }
44
- // updating .npmrc to add the token
47
+ // updating .npmrc to add the registry (and the token when there is one)
45
48
  const npmConfigFileUrl = new URL("./.npmrc", rootDirectoryUrl);
46
49
  let restoreNpmConfigFile;
47
50
  let npmConfigFileContent;
@@ -60,7 +63,7 @@ export const publish = async ({
60
63
  writeFileSync(
61
64
  npmConfigFileUrl,
62
65
  setNpmConfig(npmConfigFileContent, {
63
- [computeRegistryTokenKey(registryUrl)]: token,
66
+ ...(token ? { [computeRegistryTokenKey(registryUrl)]: token } : {}),
64
67
  [computeRegistryKey(packageObject.name)]: registryUrl,
65
68
  }),
66
69
  );
@@ -108,6 +111,9 @@ export const publish = async ({
108
111
  reason: "already-published",
109
112
  });
110
113
  } else if (error.message.includes("previously staged version")) {
114
+ // an earlier publish of this version was accepted and the
115
+ // registry holds it (see version_in_registry.js): it exposes
116
+ // it later by itself, nothing more can be done from here
111
117
  resolve({
112
118
  success: true,
113
119
  reason: "staged",
@@ -145,29 +151,21 @@ export const publish = async ({
145
151
  });
146
152
  }
147
153
  });
148
- if (publishResult.reason === "already-published") {
149
- publishTask.setRightText(`(already published)`);
150
- } else if (publishResult.reason === "staged") {
151
- publishTask.setRightText(`(staged)`);
154
+ if (publishResult.reason === "staged") {
155
+ publishTask.happen(
156
+ `${packageSlug} is staged on ${registryUrl}, an earlier publish handed it over and the registry exposes it later`,
157
+ );
158
+ } else {
159
+ if (publishResult.reason === "already-published") {
160
+ publishTask.setRightText(`(already published)`);
161
+ }
162
+ publishTask.done();
152
163
  }
153
- publishTask.done();
154
164
  } finally {
155
165
  restoreProcessEnv();
156
166
  restorePackageFile();
157
167
  restoreNpmConfigFile();
158
168
  }
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
169
  return publishResult;
172
170
  } catch (e) {
173
171
  publishTask.fail();
@@ -0,0 +1,78 @@
1
+ /*
2
+ * Whether the registry exposes a version, and waiting until it does.
3
+ *
4
+ * A version the registry accepted is not exposed right away: npm holds it
5
+ * ("staged") for minutes, sometimes hours. Nothing read from the registry tells
6
+ * a staged version apart from one that was never published, the packument
7
+ * ignores it and its tarball is a 404.
8
+ */
9
+
10
+ import { createTaskLog } from "@jsenv/humanize";
11
+
12
+ const POLL_INTERVAL_MS = 5_000;
13
+
14
+ export const waitForVersionInRegistry = async ({
15
+ registryUrl,
16
+ packageName,
17
+ packageVersion,
18
+ token,
19
+ timeout,
20
+ timeoutErrorMessage,
21
+ }) => {
22
+ const waitTask = createTaskLog(
23
+ `wait for ${packageName}@${packageVersion} to be published by ${registryUrl}`,
24
+ );
25
+ const msBeforeTimeout = Date.now() + timeout;
26
+ try {
27
+ while (true) {
28
+ const versionIsInRegistry = await checkVersionIsInRegistry({
29
+ registryUrl,
30
+ packageName,
31
+ packageVersion,
32
+ token,
33
+ });
34
+ if (versionIsInRegistry) {
35
+ waitTask.done();
36
+ return;
37
+ }
38
+ if (Date.now() > msBeforeTimeout) {
39
+ throw new Error(timeoutErrorMessage);
40
+ }
41
+ await new Promise((resolve) => {
42
+ setTimeout(resolve, POLL_INTERVAL_MS);
43
+ });
44
+ }
45
+ } catch (e) {
46
+ waitTask.fail();
47
+ throw e;
48
+ }
49
+ };
50
+
51
+ export const checkVersionIsInRegistry = async ({
52
+ registryUrl,
53
+ packageName,
54
+ packageVersion,
55
+ token,
56
+ }) => {
57
+ let response;
58
+ try {
59
+ response = await fetch(`${registryUrl}/${packageName}`, {
60
+ headers: {
61
+ "accept":
62
+ "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
63
+ // the registry is served by a cache; without this a version can stay
64
+ // invisible long after it landed
65
+ "cache-control": "no-cache",
66
+ ...(token ? { authorization: `token ${token}` } : {}),
67
+ },
68
+ });
69
+ } catch {
70
+ // a network hiccup is one more reason for the version not to be there yet
71
+ return false;
72
+ }
73
+ if (response.status !== 200) {
74
+ return false;
75
+ }
76
+ const packageObject = await response.json();
77
+ return Boolean(packageObject.versions[packageVersion]);
78
+ };
@@ -1,142 +0,0 @@
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
- };