@livekit/changesets-changelog-github 0.0.4 → 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 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 (0, import_get_github_info.getInfo)({
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 (0, import_get_github_info.getInfoFromPullRequest)({
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 (0, import_get_github_info.getInfo)({
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 getInfo({
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 getInfoFromPullRequest({
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 getInfo({
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.4",
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",
@@ -12,8 +12,8 @@
12
12
  "test": "jest"
13
13
  },
14
14
  "dependencies": {
15
- "@changesets/get-github-info": "^0.5.2",
16
- "@changesets/types": "^5.2.1",
15
+ "@changesets/get-github-info": "^0.8.0",
16
+ "@changesets/types": "^6.1.0",
17
17
  "dotenv": "^8.1.0"
18
18
  },
19
19
  "devDependencies": {
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 getInfo({
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 getInfoFromPullRequest({
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 getInfo({
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
+ });