@livekit/changesets-changelog-github 0.0.3 → 0.1.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/dist/index.js +40 -3
- package/dist/index.mjs +40 -3
- package/package.json +5 -4
- package/src/index.ts +52 -3
- package/src/retry.test.ts +99 -0
package/dist/index.js
CHANGED
|
@@ -25,6 +25,43 @@ module.exports = __toCommonJS(src_exports);
|
|
|
25
25
|
var import_dotenv = require("dotenv");
|
|
26
26
|
var import_get_github_info = require("@changesets/get-github-info");
|
|
27
27
|
(0, import_dotenv.config)();
|
|
28
|
+
var MAX_RETRIES = 3;
|
|
29
|
+
var RETRY_BASE_DELAY_MS = 200;
|
|
30
|
+
var TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
31
|
+
"ERR_STREAM_PREMATURE_CLOSE",
|
|
32
|
+
"ECONNRESET",
|
|
33
|
+
"ETIMEDOUT",
|
|
34
|
+
"ECONNREFUSED",
|
|
35
|
+
"EPIPE",
|
|
36
|
+
"ENOTFOUND",
|
|
37
|
+
"EAI_AGAIN"
|
|
38
|
+
]);
|
|
39
|
+
function isTransientError(error) {
|
|
40
|
+
if (!error || typeof error !== "object")
|
|
41
|
+
return false;
|
|
42
|
+
const code = error.code;
|
|
43
|
+
if (code && TRANSIENT_ERROR_CODES.has(code))
|
|
44
|
+
return true;
|
|
45
|
+
const message = error.message;
|
|
46
|
+
return typeof message === "string" && /premature close/i.test(message);
|
|
47
|
+
}
|
|
48
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
+
async function withRetry(fn) {
|
|
50
|
+
let lastError;
|
|
51
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
return await fn();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
lastError = error;
|
|
56
|
+
if (attempt === MAX_RETRIES || !isTransientError(error))
|
|
57
|
+
throw error;
|
|
58
|
+
await delay(RETRY_BASE_DELAY_MS * Math.pow(2, attempt));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw lastError;
|
|
62
|
+
}
|
|
63
|
+
var getInfoWithRetry = (request) => withRetry(() => (0, import_get_github_info.getInfo)(request));
|
|
64
|
+
var getInfoFromPullRequestWithRetry = (request) => withRetry(() => (0, import_get_github_info.getInfoFromPullRequest)(request));
|
|
28
65
|
var changelogFunctions = {
|
|
29
66
|
getDependencyReleaseLine: async (changesets, dependenciesUpdated, options) => {
|
|
30
67
|
if (!options.repo) {
|
|
@@ -37,7 +74,7 @@ var changelogFunctions = {
|
|
|
37
74
|
const changesetLink = `- Updated dependencies [${(await Promise.all(
|
|
38
75
|
changesets.map(async (cs) => {
|
|
39
76
|
if (cs.commit) {
|
|
40
|
-
let { links } = await (
|
|
77
|
+
let { links } = await getInfoWithRetry({
|
|
41
78
|
repo: options.repo,
|
|
42
79
|
commit: cs.commit
|
|
43
80
|
});
|
|
@@ -74,7 +111,7 @@ var changelogFunctions = {
|
|
|
74
111
|
const [firstLine, ...futureLines] = replacedChangelog.split("\n").map((l) => l.trimRight());
|
|
75
112
|
const links = await (async () => {
|
|
76
113
|
if (prFromSummary !== void 0) {
|
|
77
|
-
let { links: links2 } = await (
|
|
114
|
+
let { links: links2 } = await getInfoFromPullRequestWithRetry({
|
|
78
115
|
repo: options.repo,
|
|
79
116
|
pull: prFromSummary
|
|
80
117
|
});
|
|
@@ -88,7 +125,7 @@ var changelogFunctions = {
|
|
|
88
125
|
}
|
|
89
126
|
const commitToFetchFrom = commitFromSummary || changeset.commit;
|
|
90
127
|
if (commitToFetchFrom) {
|
|
91
|
-
let { links: links2 } = await (
|
|
128
|
+
let { links: links2 } = await getInfoWithRetry({
|
|
92
129
|
repo: options.repo,
|
|
93
130
|
commit: commitToFetchFrom
|
|
94
131
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
import { config } from "dotenv";
|
|
3
3
|
import { getInfo, getInfoFromPullRequest } from "@changesets/get-github-info";
|
|
4
4
|
config();
|
|
5
|
+
var MAX_RETRIES = 3;
|
|
6
|
+
var RETRY_BASE_DELAY_MS = 200;
|
|
7
|
+
var TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
8
|
+
"ERR_STREAM_PREMATURE_CLOSE",
|
|
9
|
+
"ECONNRESET",
|
|
10
|
+
"ETIMEDOUT",
|
|
11
|
+
"ECONNREFUSED",
|
|
12
|
+
"EPIPE",
|
|
13
|
+
"ENOTFOUND",
|
|
14
|
+
"EAI_AGAIN"
|
|
15
|
+
]);
|
|
16
|
+
function isTransientError(error) {
|
|
17
|
+
if (!error || typeof error !== "object")
|
|
18
|
+
return false;
|
|
19
|
+
const code = error.code;
|
|
20
|
+
if (code && TRANSIENT_ERROR_CODES.has(code))
|
|
21
|
+
return true;
|
|
22
|
+
const message = error.message;
|
|
23
|
+
return typeof message === "string" && /premature close/i.test(message);
|
|
24
|
+
}
|
|
25
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
async function withRetry(fn) {
|
|
27
|
+
let lastError;
|
|
28
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
29
|
+
try {
|
|
30
|
+
return await fn();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
lastError = error;
|
|
33
|
+
if (attempt === MAX_RETRIES || !isTransientError(error))
|
|
34
|
+
throw error;
|
|
35
|
+
await delay(RETRY_BASE_DELAY_MS * Math.pow(2, attempt));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw lastError;
|
|
39
|
+
}
|
|
40
|
+
var getInfoWithRetry = (request) => withRetry(() => getInfo(request));
|
|
41
|
+
var getInfoFromPullRequestWithRetry = (request) => withRetry(() => getInfoFromPullRequest(request));
|
|
5
42
|
var changelogFunctions = {
|
|
6
43
|
getDependencyReleaseLine: async (changesets, dependenciesUpdated, options) => {
|
|
7
44
|
if (!options.repo) {
|
|
@@ -14,7 +51,7 @@ var changelogFunctions = {
|
|
|
14
51
|
const changesetLink = `- Updated dependencies [${(await Promise.all(
|
|
15
52
|
changesets.map(async (cs) => {
|
|
16
53
|
if (cs.commit) {
|
|
17
|
-
let { links } = await
|
|
54
|
+
let { links } = await getInfoWithRetry({
|
|
18
55
|
repo: options.repo,
|
|
19
56
|
commit: cs.commit
|
|
20
57
|
});
|
|
@@ -51,7 +88,7 @@ var changelogFunctions = {
|
|
|
51
88
|
const [firstLine, ...futureLines] = replacedChangelog.split("\n").map((l) => l.trimRight());
|
|
52
89
|
const links = await (async () => {
|
|
53
90
|
if (prFromSummary !== void 0) {
|
|
54
|
-
let { links: links2 } = await
|
|
91
|
+
let { links: links2 } = await getInfoFromPullRequestWithRetry({
|
|
55
92
|
repo: options.repo,
|
|
56
93
|
pull: prFromSummary
|
|
57
94
|
});
|
|
@@ -65,7 +102,7 @@ var changelogFunctions = {
|
|
|
65
102
|
}
|
|
66
103
|
const commitToFetchFrom = commitFromSummary || changeset.commit;
|
|
67
104
|
if (commitToFetchFrom) {
|
|
68
|
-
let { links: links2 } = await
|
|
105
|
+
let { links: links2 } = await getInfoWithRetry({
|
|
69
106
|
repo: options.repo,
|
|
70
107
|
commit: commitToFetchFrom
|
|
71
108
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livekit/changesets-changelog-github",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A compact changelog entry generator based on the official changesets changelog generator",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
"repository": "https://github.com/livekit/changesets-changelog-github",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsup-node",
|
|
11
|
+
"publish": "tsup-node --onSuccess \"npm publish\"",
|
|
11
12
|
"test": "jest"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@changesets/get-github-info": "^0.
|
|
15
|
-
"@changesets/types": "^
|
|
15
|
+
"@changesets/get-github-info": "^0.8.0",
|
|
16
|
+
"@changesets/types": "^6.1.0",
|
|
16
17
|
"dotenv": "^8.1.0"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
|
-
"@changesets/parse": "
|
|
20
|
+
"@changesets/parse": "0.3.16",
|
|
20
21
|
"@types/jest": "29.1.1",
|
|
21
22
|
"changesets": "^1.0.2",
|
|
22
23
|
"changesets-cli": "^0.0.0",
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,55 @@ import { getInfo, getInfoFromPullRequest } from "@changesets/get-github-info";
|
|
|
5
5
|
|
|
6
6
|
config();
|
|
7
7
|
|
|
8
|
+
// `@changesets/get-github-info` makes a single `node-fetch@2` GraphQL call to
|
|
9
|
+
// GitHub and rethrows on failure. On GitHub-hosted runners (Node 24+), transient
|
|
10
|
+
// GitHub connection resets surface through node-fetch's gzip stream handling as a
|
|
11
|
+
// hard `ERR_STREAM_PREMATURE_CLOSE` ("Premature close") error, which aborts the
|
|
12
|
+
// whole `changeset version` step. These failures are environmental and transient,
|
|
13
|
+
// so we retry the fetch a few times with exponential backoff before giving up.
|
|
14
|
+
const MAX_RETRIES = 3;
|
|
15
|
+
const RETRY_BASE_DELAY_MS = 200;
|
|
16
|
+
|
|
17
|
+
const TRANSIENT_ERROR_CODES = new Set([
|
|
18
|
+
"ERR_STREAM_PREMATURE_CLOSE",
|
|
19
|
+
"ECONNRESET",
|
|
20
|
+
"ETIMEDOUT",
|
|
21
|
+
"ECONNREFUSED",
|
|
22
|
+
"EPIPE",
|
|
23
|
+
"ENOTFOUND",
|
|
24
|
+
"EAI_AGAIN",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function isTransientError(error: unknown): boolean {
|
|
28
|
+
if (!error || typeof error !== "object") return false;
|
|
29
|
+
const code = (error as { code?: string }).code;
|
|
30
|
+
if (code && TRANSIENT_ERROR_CODES.has(code)) return true;
|
|
31
|
+
const message = (error as { message?: string }).message;
|
|
32
|
+
return typeof message === "string" && /premature close/i.test(message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
|
|
37
|
+
async function withRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
38
|
+
let lastError: unknown;
|
|
39
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
40
|
+
try {
|
|
41
|
+
return await fn();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
lastError = error;
|
|
44
|
+
if (attempt === MAX_RETRIES || !isTransientError(error)) throw error;
|
|
45
|
+
await delay(RETRY_BASE_DELAY_MS * Math.pow(2, attempt));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw lastError;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const getInfoWithRetry: typeof getInfo = (request) =>
|
|
52
|
+
withRetry(() => getInfo(request));
|
|
53
|
+
const getInfoFromPullRequestWithRetry: typeof getInfoFromPullRequest = (
|
|
54
|
+
request
|
|
55
|
+
) => withRetry(() => getInfoFromPullRequest(request));
|
|
56
|
+
|
|
8
57
|
const changelogFunctions: ChangelogFunctions = {
|
|
9
58
|
getDependencyReleaseLine: async (
|
|
10
59
|
changesets,
|
|
@@ -22,7 +71,7 @@ const changelogFunctions: ChangelogFunctions = {
|
|
|
22
71
|
await Promise.all(
|
|
23
72
|
changesets.map(async (cs) => {
|
|
24
73
|
if (cs.commit) {
|
|
25
|
-
let { links } = await
|
|
74
|
+
let { links } = await getInfoWithRetry({
|
|
26
75
|
repo: options.repo,
|
|
27
76
|
commit: cs.commit,
|
|
28
77
|
});
|
|
@@ -73,7 +122,7 @@ const changelogFunctions: ChangelogFunctions = {
|
|
|
73
122
|
|
|
74
123
|
const links = await (async () => {
|
|
75
124
|
if (prFromSummary !== undefined) {
|
|
76
|
-
let { links } = await
|
|
125
|
+
let { links } = await getInfoFromPullRequestWithRetry({
|
|
77
126
|
repo: options.repo,
|
|
78
127
|
pull: prFromSummary,
|
|
79
128
|
});
|
|
@@ -87,7 +136,7 @@ const changelogFunctions: ChangelogFunctions = {
|
|
|
87
136
|
}
|
|
88
137
|
const commitToFetchFrom = commitFromSummary || changeset.commit;
|
|
89
138
|
if (commitToFetchFrom) {
|
|
90
|
-
let { links } = await
|
|
139
|
+
let { links } = await getInfoWithRetry({
|
|
91
140
|
repo: options.repo,
|
|
92
141
|
commit: commitToFetchFrom,
|
|
93
142
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import changelogFunctions from "./index";
|
|
2
|
+
import parse from "@changesets/parse";
|
|
3
|
+
|
|
4
|
+
// Auto-mock so we can drive `getInfo` / `getInfoFromPullRequest` per-test,
|
|
5
|
+
// simulating the transient `ERR_STREAM_PREMATURE_CLOSE` GitHub fetch failures.
|
|
6
|
+
jest.mock("@changesets/get-github-info");
|
|
7
|
+
|
|
8
|
+
import { getInfo, getInfoFromPullRequest } from "@changesets/get-github-info";
|
|
9
|
+
|
|
10
|
+
const mockedGetInfo = getInfo as jest.MockedFunction<typeof getInfo>;
|
|
11
|
+
const mockedGetInfoFromPullRequest =
|
|
12
|
+
getInfoFromPullRequest as jest.MockedFunction<typeof getInfoFromPullRequest>;
|
|
13
|
+
|
|
14
|
+
const getReleaseLine = changelogFunctions.getReleaseLine;
|
|
15
|
+
|
|
16
|
+
const data = {
|
|
17
|
+
commit: "a085003",
|
|
18
|
+
user: "Andarist",
|
|
19
|
+
pull: 1613,
|
|
20
|
+
repo: "emotion-js/emotion",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const links = {
|
|
24
|
+
user: `@${data.user}`,
|
|
25
|
+
pull: `[#${data.pull}](https://github.com/${data.repo}/pull/${data.pull})`,
|
|
26
|
+
commit: `[\`${data.commit}\`](https://github.com/${data.repo}/commit/${data.commit})`,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const expectedLine = `\n\n- something - [#1613](https://github.com/emotion-js/emotion/pull/1613) (@Andarist)\n`;
|
|
30
|
+
|
|
31
|
+
const prematureClose = () =>
|
|
32
|
+
Object.assign(new Error("Premature close"), {
|
|
33
|
+
code: "ERR_STREAM_PREMATURE_CLOSE",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const getChangeset = (content: string, commit: string | undefined) => {
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
...parse(
|
|
40
|
+
`---
|
|
41
|
+
pkg: "minor"
|
|
42
|
+
---
|
|
43
|
+
something
|
|
44
|
+
${content}
|
|
45
|
+
`
|
|
46
|
+
),
|
|
47
|
+
id: "some-id",
|
|
48
|
+
commit,
|
|
49
|
+
},
|
|
50
|
+
"minor",
|
|
51
|
+
{ repo: data.repo },
|
|
52
|
+
] as const;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
it("retries getInfo when the GitHub fetch fails with a premature close", async () => {
|
|
56
|
+
mockedGetInfo
|
|
57
|
+
.mockRejectedValueOnce(prematureClose())
|
|
58
|
+
.mockRejectedValueOnce(prematureClose())
|
|
59
|
+
.mockResolvedValue({ pull: data.pull, user: data.user, links });
|
|
60
|
+
|
|
61
|
+
const result = await getReleaseLine(...getChangeset("", data.commit));
|
|
62
|
+
|
|
63
|
+
expect(mockedGetInfo).toHaveBeenCalledTimes(3);
|
|
64
|
+
expect(result).toEqual(expectedLine);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("retries getInfoFromPullRequest when the GitHub fetch fails with a premature close", async () => {
|
|
68
|
+
mockedGetInfoFromPullRequest
|
|
69
|
+
.mockRejectedValueOnce(prematureClose())
|
|
70
|
+
.mockResolvedValue({ commit: data.commit, user: data.user, links });
|
|
71
|
+
|
|
72
|
+
const result = await getReleaseLine(
|
|
73
|
+
...getChangeset(`pr: #${data.pull}`, data.commit)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(mockedGetInfoFromPullRequest).toHaveBeenCalledTimes(2);
|
|
77
|
+
expect(result).toEqual(expectedLine);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("gives up after exhausting retries on a persistent transient failure", async () => {
|
|
81
|
+
mockedGetInfo.mockRejectedValue(prematureClose());
|
|
82
|
+
|
|
83
|
+
await expect(
|
|
84
|
+
getReleaseLine(...getChangeset("", data.commit))
|
|
85
|
+
).rejects.toMatchObject({ code: "ERR_STREAM_PREMATURE_CLOSE" });
|
|
86
|
+
|
|
87
|
+
// initial attempt + MAX_RETRIES (3)
|
|
88
|
+
expect(mockedGetInfo).toHaveBeenCalledTimes(4);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("does not retry non-transient errors", async () => {
|
|
92
|
+
mockedGetInfo.mockRejectedValue(new Error("Bad credentials"));
|
|
93
|
+
|
|
94
|
+
await expect(
|
|
95
|
+
getReleaseLine(...getChangeset("", data.commit))
|
|
96
|
+
).rejects.toThrow("Bad credentials");
|
|
97
|
+
|
|
98
|
+
expect(mockedGetInfo).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|