Skip to content

Commit 6b18c2f

Browse files
authored
test(release): add unit tests when searching for a release (#603)
* fix(release): break when draft release is found when a release with the desired tag_name is found, break out of the loop that looks for it. this prevents the case where accidentally overwrite a detected release on successive iterations of the for loop fixes: #602 * include built output * add tests for finding tag from releases add tests for updated functionality to break when we find a release. the logic has been extracted into its own function, to make testing simpler by avoiding over mocking/stubbing of network calls that would create or update a release. the tests that were added use jest's describe/it blocks, but use node's assert function to align with other tests. there isn't any prior art for mocking function calls in the codebase, so for now we use simple promises in "mock" objects that adhere to the Releaser interface * refactor findTagFromReleases purely a sytlistic choice to not have to pre-declare the _release variable, and not have to check using `typeof _release === "string"` when detecting a found release * reset dist/index.js to master * update impl after merge with master * update dist
1 parent e2b105c commit 6b18c2f

File tree

3 files changed

+287
-13
lines changed

3 files changed

+287
-13
lines changed

__tests__/github.test.ts

+254-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import * as assert from "assert";
2-
import { text } from "stream/consumers";
3-
import { mimeOrDefault, asset } from "../src/github";
2+
import {
3+
mimeOrDefault,
4+
asset,
5+
Releaser,
6+
Release,
7+
findTagFromReleases,
8+
} from "../src/github";
49

510
describe("github", () => {
611
describe("mimeOrDefault", () => {
@@ -20,4 +25,251 @@ describe("github", () => {
2025
assert.equal(size, 10);
2126
});
2227
});
28+
29+
describe("findTagFromReleases", () => {
30+
const owner = "owner";
31+
const repo = "repo";
32+
33+
const mockRelease: Release = {
34+
id: 1,
35+
upload_url: `https://api.github.com/repos/${owner}/${repo}/releases/1/assets`,
36+
html_url: `https://github.com/${owner}/${repo}/releases/tag/v1.0.0`,
37+
tag_name: "v1.0.0",
38+
name: "Test Release",
39+
body: "Test body",
40+
target_commitish: "main",
41+
draft: false,
42+
prerelease: false,
43+
assets: [],
44+
} as const;
45+
46+
const mockReleaser: Releaser = {
47+
getReleaseByTag: () => Promise.reject("Not implemented"),
48+
createRelease: () => Promise.reject("Not implemented"),
49+
updateRelease: () => Promise.reject("Not implemented"),
50+
allReleases: async function* () {
51+
yield { data: [mockRelease] };
52+
},
53+
} as const;
54+
55+
describe("when the tag_name is not an empty string", () => {
56+
const targetTag = "v1.0.0";
57+
58+
it("finds a matching release in first batch of results", async () => {
59+
const targetRelease = {
60+
...mockRelease,
61+
owner,
62+
repo,
63+
tag_name: targetTag,
64+
};
65+
const otherRelease = {
66+
...mockRelease,
67+
owner,
68+
repo,
69+
tag_name: "v1.0.1",
70+
};
71+
72+
const releaser = {
73+
...mockReleaser,
74+
allReleases: async function* () {
75+
yield { data: [targetRelease] };
76+
yield { data: [otherRelease] };
77+
},
78+
};
79+
80+
const result = await findTagFromReleases(
81+
releaser,
82+
owner,
83+
repo,
84+
targetTag,
85+
);
86+
87+
assert.deepStrictEqual(result, targetRelease);
88+
});
89+
90+
it("finds a matching release in second batch of results", async () => {
91+
const targetRelease = {
92+
...mockRelease,
93+
owner,
94+
repo,
95+
tag_name: targetTag,
96+
};
97+
const otherRelease = {
98+
...mockRelease,
99+
owner,
100+
repo,
101+
tag_name: "v1.0.1",
102+
};
103+
104+
const releaser = {
105+
...mockReleaser,
106+
allReleases: async function* () {
107+
yield { data: [otherRelease] };
108+
yield { data: [targetRelease] };
109+
},
110+
};
111+
112+
const result = await findTagFromReleases(
113+
releaser,
114+
owner,
115+
repo,
116+
targetTag,
117+
);
118+
assert.deepStrictEqual(result, targetRelease);
119+
});
120+
121+
it("returns undefined when a release is not found in any batch", async () => {
122+
const otherRelease = {
123+
...mockRelease,
124+
owner,
125+
repo,
126+
tag_name: "v1.0.1",
127+
};
128+
const releaser = {
129+
...mockReleaser,
130+
allReleases: async function* () {
131+
yield { data: [otherRelease] };
132+
yield { data: [otherRelease] };
133+
},
134+
};
135+
136+
const result = await findTagFromReleases(
137+
releaser,
138+
owner,
139+
repo,
140+
targetTag,
141+
);
142+
143+
assert.strictEqual(result, undefined);
144+
});
145+
146+
it("returns undefined when no releases are returned", async () => {
147+
const releaser = {
148+
...mockReleaser,
149+
allReleases: async function* () {
150+
yield { data: [] };
151+
},
152+
};
153+
154+
const result = await findTagFromReleases(
155+
releaser,
156+
owner,
157+
repo,
158+
targetTag,
159+
);
160+
161+
assert.strictEqual(result, undefined);
162+
});
163+
});
164+
165+
describe("when the tag_name is an empty string", () => {
166+
const emptyTag = "";
167+
168+
it("finds a matching release in first batch of results", async () => {
169+
const targetRelease = {
170+
...mockRelease,
171+
owner,
172+
repo,
173+
tag_name: emptyTag,
174+
};
175+
const otherRelease = {
176+
...mockRelease,
177+
owner,
178+
repo,
179+
tag_name: "v1.0.1",
180+
};
181+
182+
const releaser = {
183+
...mockReleaser,
184+
allReleases: async function* () {
185+
yield { data: [targetRelease] };
186+
yield { data: [otherRelease] };
187+
},
188+
};
189+
190+
const result = await findTagFromReleases(
191+
releaser,
192+
owner,
193+
repo,
194+
emptyTag,
195+
);
196+
197+
assert.deepStrictEqual(result, targetRelease);
198+
});
199+
200+
it("finds a matching release in second batch of results", async () => {
201+
const targetRelease = {
202+
...mockRelease,
203+
owner,
204+
repo,
205+
tag_name: emptyTag,
206+
};
207+
const otherRelease = {
208+
...mockRelease,
209+
owner,
210+
repo,
211+
tag_name: "v1.0.1",
212+
};
213+
214+
const releaser = {
215+
...mockReleaser,
216+
allReleases: async function* () {
217+
yield { data: [otherRelease] };
218+
yield { data: [targetRelease] };
219+
},
220+
};
221+
222+
const result = await findTagFromReleases(
223+
releaser,
224+
owner,
225+
repo,
226+
emptyTag,
227+
);
228+
assert.deepStrictEqual(result, targetRelease);
229+
});
230+
231+
it("returns undefined when a release is not found in any batch", async () => {
232+
const otherRelease = {
233+
...mockRelease,
234+
owner,
235+
repo,
236+
tag_name: "v1.0.1",
237+
};
238+
const releaser = {
239+
...mockReleaser,
240+
allReleases: async function* () {
241+
yield { data: [otherRelease] };
242+
yield { data: [otherRelease] };
243+
},
244+
};
245+
246+
const result = await findTagFromReleases(
247+
releaser,
248+
owner,
249+
repo,
250+
emptyTag,
251+
);
252+
253+
assert.strictEqual(result, undefined);
254+
});
255+
256+
it("returns undefined when no releases are returned", async () => {
257+
const releaser = {
258+
...mockReleaser,
259+
allReleases: async function* () {
260+
yield { data: [] };
261+
},
262+
};
263+
264+
const result = await findTagFromReleases(
265+
releaser,
266+
owner,
267+
repo,
268+
emptyTag,
269+
);
270+
271+
assert.strictEqual(result, undefined);
272+
});
273+
});
274+
});
23275
});

dist/index.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/github.ts

+32-10
Original file line numberDiff line numberDiff line change
@@ -225,18 +225,13 @@ export const release = async (
225225
const discussion_category_name = config.input_discussion_category_name;
226226
const generate_release_notes = config.input_generate_release_notes;
227227
try {
228-
// you can't get an existing draft by tag
229-
// so we must find one in the list of all releases
230-
let _release: Release | undefined = undefined;
231-
for await (const response of releaser.allReleases({
228+
const _release: Release | undefined = await findTagFromReleases(
229+
releaser,
232230
owner,
233231
repo,
234-
})) {
235-
_release = response.data.find((release) => release.tag_name === tag);
236-
if (_release !== undefined) {
237-
break;
238-
}
239-
}
232+
tag,
233+
);
234+
240235
if (_release === undefined) {
241236
return await createRelease(
242237
tag,
@@ -331,6 +326,33 @@ export const release = async (
331326
}
332327
};
333328

329+
/**
330+
* Finds a release by tag name from all a repository's releases.
331+
*
332+
* @param releaser - The GitHub API wrapper for release operations
333+
* @param owner - The owner of the repository
334+
* @param repo - The name of the repository
335+
* @param tag - The tag name to search for
336+
* @returns The release with the given tag name, or undefined if no release with that tag name is found
337+
*/
338+
export async function findTagFromReleases(
339+
releaser: Releaser,
340+
owner: string,
341+
repo: string,
342+
tag: string,
343+
): Promise<Release | undefined> {
344+
for await (const { data: releases } of releaser.allReleases({
345+
owner,
346+
repo,
347+
})) {
348+
const release = releases.find((release) => release.tag_name === tag);
349+
if (release) {
350+
return release;
351+
}
352+
}
353+
return undefined;
354+
}
355+
334356
async function createRelease(
335357
tag: string,
336358
config: Config,

0 commit comments

Comments
 (0)