Skip to content

Commit bf25a32

Browse files
Simplify getLatestVersion function
1 parent 75dd491 commit bf25a32

File tree

5 files changed

+70
-110
lines changed

5 files changed

+70
-110
lines changed

src/version/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@ export enum ReleaseChannel {
22
Stable = "latest",
33
Beta = "latest-beta",
44
}
5+
6+
export type VersionResponse = {
7+
CLI2: {
8+
release: { version: string };
9+
beta: { version: string };
10+
};
11+
}

src/version/helper.test.ts

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,66 @@
11
import { getLatestVersion } from "./helper";
2-
import * as helper from "./helper";
32
import { ReleaseChannel } from "./constants";
43

54
describe("getLatestVersion", () => {
6-
afterEach(() => {
5+
beforeEach(() => {
76
jest.restoreAllMocks();
87
});
98

10-
it("returns latest stable version", async () => {
11-
jest.spyOn(helper, "loadHtml").mockResolvedValue(`
12-
<html><body>
13-
<article><h3> 2.31.1 </h3><span>build number and release date</span></article>
14-
<article><h3> 2.32.1 </h3><span>build number and release date</span></article>
15-
<article><h3> 2.33.2 </h3><span>build number and release date</span></article>
16-
<article><h3>2.33.1 </h3><span>build number and release date</span></article>
17-
</body></html>
18-
`);
9+
it("should return latest stable version", async () => {
10+
const mockResponse = {
11+
CLI2: {
12+
release: { version: "2.31.0" },
13+
beta: { version: "2.32.0-beta.01" },
14+
},
15+
};
16+
17+
jest.spyOn(global, "fetch").mockResolvedValueOnce({
18+
json: async () => mockResponse,
19+
} as Response);
20+
1921
const version = await getLatestVersion(ReleaseChannel.Stable);
20-
expect(version).toBe("2.33.2");
22+
expect(version).toBe("2.31.0");
2123
});
2224

23-
it("returns latest beta version", async () => {
24-
jest.spyOn(helper, "loadHtml").mockResolvedValue(`
25-
<html><body>
26-
<article class="beta"><h3> 1.32.0-beta.01 <span>build number and release date</span></h3></article>
27-
<article class="beta"><h3>2.32.0-beta.01 <span>build number and release date</span></h3></article>
28-
<article class="beta"><h3>3.32.0-beta.02<span>build number and release date</span></h3></article>
29-
<article class="beta"><h3>3.32.0-beta.01 <span>build number and release date</span></h3></article>
30-
</body></html>
31-
`);
25+
it("should return latest beta version", async () => {
26+
const mockResponse = {
27+
CLI2: {
28+
release: { version: "2.31.0" },
29+
beta: { version: "2.32.0-beta.01" },
30+
},
31+
};
32+
33+
jest.spyOn(global, "fetch").mockResolvedValueOnce({
34+
json: async () => mockResponse,
35+
} as Response);
36+
3237
const version = await getLatestVersion(ReleaseChannel.Beta);
33-
expect(version).toBe("3.32.0-beta.02");
38+
expect(version).toBe("2.32.0-beta.01");
3439
});
3540

36-
it("throws error when no versions found", async () => {
37-
jest.spyOn(helper, "loadHtml").mockResolvedValue("<html></html>");
38-
await expect(getLatestVersion(ReleaseChannel.Stable)).rejects.toThrow(
39-
`No ${ReleaseChannel.Stable} versions found`,
40-
);
41-
});
41+
it("should throw if no CLI2 field", async () => {
42+
jest.spyOn(global, "fetch").mockResolvedValueOnce({
43+
json: async () => ({}),
44+
} as Response);
4245

43-
it("throws error when HTML is invalid", async () => {
44-
jest
45-
.spyOn(helper, "loadHtml")
46-
.mockResolvedValue("<html><article></article></html>");
47-
await expect(getLatestVersion(ReleaseChannel.Stable)).rejects.toThrow(
48-
`No ${ReleaseChannel.Stable} versions found`,
49-
);
46+
await expect(
47+
getLatestVersion(ReleaseChannel.Stable),
48+
).rejects.toThrow(`No ${ReleaseChannel.Stable} versions found`);
5049
});
5150

52-
it("calls loadHtml once", async () => {
53-
const spy = jest.spyOn(helper, "loadHtml").mockResolvedValue(`
54-
<article><h3>2.31.1</h3></article>
55-
`);
56-
await getLatestVersion(ReleaseChannel.Stable);
57-
expect(spy).toHaveBeenCalledTimes(1);
51+
it("should throw if no stable version found", async () => {
52+
const mockResponse = {
53+
CLI2: {
54+
beta: { version: "2.32.0-beta.01" },
55+
},
56+
};
57+
58+
jest.spyOn(global, "fetch").mockResolvedValueOnce({
59+
json: async () => mockResponse,
60+
} as Response);
61+
62+
await expect(
63+
getLatestVersion(ReleaseChannel.Stable),
64+
).rejects.toThrow(`No ${ReleaseChannel.Stable} versions found`);
5865
});
5966
});

src/version/helper.ts

Lines changed: 14 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,23 @@
1-
import * as https from "https";
21
import * as core from "@actions/core";
3-
import * as cheerio from "cheerio";
4-
import semver from "semver";
5-
import { ReleaseChannel } from "./constants";
2+
import { ReleaseChannel, type VersionResponse } from "./constants";
63

7-
// Loads the HTML content from the 1Password CLI product history page.
8-
export const loadHtml = (): Promise<string> => {
9-
return new Promise((resolve, reject) => {
10-
https
11-
.get("https://app-updates.agilebits.com/product_history/CLI2", (res) => {
12-
let data = "";
13-
res.on("data", (chunk) => (data += chunk));
14-
res.on("end", () => {
15-
core.info("HTML loaded successfully");
16-
resolve(data);
17-
});
18-
})
19-
.on("error", (e) => {
20-
core.error(`Failed to load HTML: ${e.message}`);
21-
reject();
22-
});
23-
});
24-
};
25-
26-
// Finds and returns an array of version strings from the HTML content based on the provided query.
27-
// Version stored in the first <h3> element of each article element.
28-
const findVersions = ($: cheerio.CheerioAPI, query: string): string[] => {
29-
const versions: string[] = [];
30-
$(query).each((_, article) => {
31-
const h3 = $(article).find("h3").first();
32-
const version = h3
33-
.contents()
34-
.filter((_, el) => el.type === "text") // select only text nodes
35-
.text()
36-
.trim();
37-
if (version) {
38-
versions.push(version);
39-
}
40-
});
41-
return versions;
42-
};
43-
44-
export const normalizeForSemver = (version: string): string =>
45-
version.replace(/-beta\.0*(\d+)/, "-beta.$1");
46-
47-
// TODO: Use `GET https://app-updates.agilebits.com/latest` endpoint to get the latest version.
48-
// TODO: Use `CLI2` property from response object, as soon as it is available.
49-
// TODO: will be tackled in https://github.com/1Password/install-cli-action/issues/18
504
// Returns the latest version of the 1Password CLI based on the specified channel.
515
export const getLatestVersion = async (
52-
versionType: ReleaseChannel,
6+
channel: ReleaseChannel,
537
): Promise<string> => {
54-
core.info(`Getting ${versionType} version number`);
55-
const html = await loadHtml();
56-
const $ = cheerio.load(html);
57-
const versions =
58-
versionType === ReleaseChannel.Beta
59-
? findVersions($, "article.beta")
60-
: findVersions($, "article");
8+
core.info(`Getting ${channel} version number`);
9+
const res = await fetch("https://app-updates.agilebits.com/latest");
10+
const json: VersionResponse = await res.json();
11+
const latestStable = json?.CLI2?.release?.version;
12+
const latestBeta = json?.CLI2?.beta?.version;
13+
const version = channel === ReleaseChannel.Beta
14+
? latestBeta
15+
: latestStable;
6116

62-
if (versions.length === 0) {
63-
core.error(`No ${versionType} versions found`);
64-
throw new Error(`No ${versionType} versions found`);
17+
if (!version) {
18+
core.error(`No ${channel} versions found`);
19+
throw new Error(`No ${channel} versions found`);
6520
}
6621

67-
// Sort versions in descending order
68-
versions.sort((a: string, b: string): number => {
69-
const aNorm = new semver.SemVer(normalizeForSemver(a));
70-
const bNorm = new semver.SemVer(normalizeForSemver(b));
71-
return semver.rcompare(aNorm, bNorm);
72-
});
73-
74-
return versions[0]!;
22+
return version;
7523
};

src/version/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export { VersionResolver } from "./version-resolver";
2-
export { normalizeForSemver } from "./helper";

src/version/validate.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import semver from "semver";
22
import { ReleaseChannel } from "./constants";
3-
import { normalizeForSemver } from "./helper";
43

54
// Validates if the provided version type is a valid enum value or a valid semver version.
65
export const validateVersion = (input: string): void => {
@@ -13,7 +12,7 @@ export const validateVersion = (input: string): void => {
1312
// That's why we need to normalize them before validating.
1413
// Accepts valid semver versions like "2.18.0" or beta-releases like "2.19.0-beta.01"
1514
// or versions with 'v' prefix like "v2.19.0"
16-
const normalized = normalizeForSemver(input);
15+
const normalized = input.replace(/-beta\.0*(\d+)/, "-beta.$1");
1716
const normInput = new semver.SemVer(normalized);
1817
if (semver.valid(normInput)) {
1918
return;

0 commit comments

Comments
 (0)