From 486890821c6e2772d0448e9a029f240a54013aa3 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 May 2024 03:26:03 +0000 Subject: [PATCH 01/20] add build:watch script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8296ef6..b027de3 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "scripts": { "build": "ncc build src/index.ts", + "build:watch": "ncc build --watch src/index.ts", "package": "cp action.yml README.md dist/", "test": "vitest", "lint": "biome ci .", From 663f5933d165d79bcdea0ebcb6b2ec04edd42474 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 May 2024 03:28:05 +0000 Subject: [PATCH 02/20] ignore dist for link --- biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biome.json b/biome.json index d0d80cc..db4c9e9 100644 --- a/biome.json +++ b/biome.json @@ -1,6 +1,6 @@ { "files": { - "ignore": ["__test__/data/", "package.json"] + "ignore": ["./dist/", "__test__/data/", "package.json"] }, "linter": { "enabled": true, From 6a91b0c2c9f3dcfa244010d49e3861b1c95fbfba Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 May 2024 03:26:11 +0000 Subject: [PATCH 03/20] fix install and download fixture only for browsers --- __test__/version_installer.test.ts | 2 +- src/channel_linux.ts | 7 +++++-- src/channel_macos.ts | 7 +++++-- src/channel_windows.ts | 7 +++++-- src/installer.ts | 8 ++++---- src/snapshot.ts | 18 ++++++++++++------ src/version_installer.ts | 7 +++++-- 7 files changed, 37 insertions(+), 19 deletions(-) diff --git a/__test__/version_installer.test.ts b/__test__/version_installer.test.ts index dd511fa..2fbb5c0 100644 --- a/__test__/version_installer.test.ts +++ b/__test__/version_installer.test.ts @@ -86,7 +86,7 @@ describe("KnownGoodVersionInstaller", () => { os: "linux", arch: "amd64", }); - const downloaded = await installer.download("120.0.6099.x"); + const downloaded = await installer.downloadBrowser("120.0.6099.x"); expect(downloaded?.archive).toEqual("/tmp/chromium.zip"); }); }); diff --git a/src/channel_linux.ts b/src/channel_linux.ts index 05f98a3..bd0af9a 100644 --- a/src/channel_linux.ts +++ b/src/channel_linux.ts @@ -19,7 +19,7 @@ export class LinuxChannelInstaller implements Installer { } } - async download(version: string): Promise { + async downloadBrowser(version: string): Promise { if (!isReleaseChannelName(version)) { throw new Error(`Unexpected version: ${version}`); } @@ -45,7 +45,10 @@ export class LinuxChannelInstaller implements Installer { return { archive }; } - async install(version: string, archive: string): Promise { + async installBrowser( + version: string, + archive: string, + ): Promise { if (!isReleaseChannelName(version)) { throw new Error(`Unexpected version: ${version}`); } diff --git a/src/channel_macos.ts b/src/channel_macos.ts index 2a97d84..f18c35b 100644 --- a/src/channel_macos.ts +++ b/src/channel_macos.ts @@ -21,7 +21,7 @@ export class MacOSChannelInstaller implements Installer { } } - async download(version: string): Promise { + async downloadBrowser(version: string): Promise { if (!isReleaseChannelName(version)) { throw new Error(`Unexpected version: ${version}`); } @@ -40,7 +40,10 @@ export class MacOSChannelInstaller implements Installer { return { archive }; } - async install(version: string, archive: string): Promise { + async installBrowser( + version: string, + archive: string, + ): Promise { if (!isReleaseChannelName(version)) { throw new Error(`Unexpected version: ${version}`); } diff --git a/src/channel_windows.ts b/src/channel_windows.ts index c5d3afc..58470d4 100644 --- a/src/channel_windows.ts +++ b/src/channel_windows.ts @@ -33,7 +33,7 @@ export class WindowsChannelInstaller implements Installer { return { root, bin: "chrome.exe" }; } - async download(version: string): Promise { + async downloadBrowser(version: string): Promise { if (!isReleaseChannelName(version)) { throw new Error(`Unexpected version: ${version}`); } @@ -101,7 +101,10 @@ export class WindowsChannelInstaller implements Installer { return { archive: `${archivePath}.exe` }; } - async install(version: string, archive: string): Promise { + async installBrowser( + version: string, + archive: string, + ): Promise { if (!isReleaseChannelName(version)) { throw new Error(`Unexpected version: ${version}`); } diff --git a/src/installer.ts b/src/installer.ts index a2f393b..6399ca8 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -20,9 +20,9 @@ export type DownloadResult = { export interface Installer { checkInstalled(version: string): Promise; - download(version: string): Promise; + downloadBrowser(version: string): Promise; - install(version: string, archive: string): Promise; + installBrowser(version: string, archive: string): Promise; } export const install = async ( @@ -58,10 +58,10 @@ export const install = async ( } core.info(`Attempting to download ${version}...`); - const { archive } = await installer.download(version); + const { archive } = await installer.downloadBrowser(version); core.info("Installing chromium..."); - const { root, bin } = await installer.install(version, archive); + const { root, bin } = await installer.installBrowser(version, archive); return path.join(root, bin); }; diff --git a/src/snapshot.ts b/src/snapshot.ts index 677fe95..ddf82c8 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -16,7 +16,7 @@ export class SnapshotInstaller implements Installer { } } - async download(version: string): Promise { + async downloadBrowser(version: string): Promise { const url = `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${makePlatformPart( this.platform, )}%2F${version}%2F${makeBasename(this.platform)}?alt=media`; @@ -26,7 +26,10 @@ export class SnapshotInstaller implements Installer { return { archive }; } - async install(version: string, archive: string): Promise { + async installBrowser( + version: string, + archive: string, + ): Promise { const extPath = await tc.extractZip(archive); let root = (() => { switch (this.platform.os) { @@ -70,7 +73,7 @@ export class LatestInstaller implements Installer { } } - async download(_version: string): Promise { + async downloadBrowser(_version: string): Promise { const latestVersionURL = `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${makePlatformPart( this.platform, )}%2FLAST_CHANGE?alt=media`; @@ -82,11 +85,14 @@ export class LatestInstaller implements Installer { } const version = await resp.readBody(); - return this.snapshotInstaller.download(version); + return this.snapshotInstaller.downloadBrowser(version); } - async install(version: string, archive: string): Promise { - return this.snapshotInstaller.install(version, archive); + async installBrowser( + version: string, + archive: string, + ): Promise { + return this.snapshotInstaller.installBrowser(version, archive); } } diff --git a/src/version_installer.ts b/src/version_installer.ts index 7854e0f..f31fab5 100644 --- a/src/version_installer.ts +++ b/src/version_installer.ts @@ -150,7 +150,7 @@ export class KnownGoodVersionInstaller implements Installer { } } - async download(version: string): Promise { + async downloadBrowser(version: string): Promise { const resolved = await this.versionResolver.resolve(version); if (!resolved) { throw new Error(`Version ${version} not found in known good versions`); @@ -165,7 +165,10 @@ export class KnownGoodVersionInstaller implements Installer { return { archive }; } - async install(version: string, archive: string): Promise { + async installBrowser( + version: string, + archive: string, + ): Promise { const resolved = await this.versionResolver.resolve(version); if (!resolved) { throw new Error(`Version ${version} not found in known good versions`); From e30499df29d08d43fc5c3dfa6547e538e4225ae6 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 May 2024 13:53:56 +0000 Subject: [PATCH 04/20] separate LatestInstaller and SnapshotInstaller --- src/installer.ts | 3 +- src/latest_installer.ts | 30 +++++++++ src/snapshot.ts | 125 -------------------------------------- src/snapshot_bucket.ts | 53 ++++++++++++++++ src/snapshot_installer.ts | 57 +++++++++++++++++ 5 files changed, 142 insertions(+), 126 deletions(-) create mode 100644 src/latest_installer.ts delete mode 100644 src/snapshot.ts create mode 100644 src/snapshot_bucket.ts create mode 100644 src/snapshot_installer.ts diff --git a/src/installer.ts b/src/installer.ts index 6399ca8..0b4578b 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -3,8 +3,9 @@ import * as core from "@actions/core"; import { LinuxChannelInstaller } from "./channel_linux"; import { MacOSChannelInstaller } from "./channel_macos"; import { WindowsChannelInstaller } from "./channel_windows"; +import { LatestInstaller } from "./latest_installer"; import { OS, type Platform } from "./platform"; -import { LatestInstaller, SnapshotInstaller } from "./snapshot"; +import { SnapshotInstaller } from "./snapshot_installer"; import { parse } from "./version"; import { KnownGoodVersionInstaller } from "./version_installer"; diff --git a/src/latest_installer.ts b/src/latest_installer.ts new file mode 100644 index 0000000..0e83e17 --- /dev/null +++ b/src/latest_installer.ts @@ -0,0 +1,30 @@ +import * as cache from "./cache"; +import type { DownloadResult, InstallResult, Installer } from "./installer"; +import type { Platform } from "./platform"; +import { resolveLatestVersion } from "./snapshot_bucket"; +import { SnapshotInstaller } from "./snapshot_installer"; + +export class LatestInstaller implements Installer { + private readonly snapshotInstaller = new SnapshotInstaller(this.platform); + + constructor(private readonly platform: Platform) {} + + async checkInstalled(version: string): Promise { + const root = await cache.find("chromium", version); + if (root) { + return { root, bin: "chrome" }; + } + } + + async downloadBrowser(_version: string): Promise { + const version = await resolveLatestVersion(this.platform); + return this.snapshotInstaller.downloadBrowser(version); + } + + async installBrowser( + version: string, + archive: string, + ): Promise { + return this.snapshotInstaller.installBrowser(version, archive); + } +} diff --git a/src/snapshot.ts b/src/snapshot.ts deleted file mode 100644 index ddf82c8..0000000 --- a/src/snapshot.ts +++ /dev/null @@ -1,125 +0,0 @@ -import path from "node:path"; -import * as core from "@actions/core"; -import * as httpm from "@actions/http-client"; -import * as tc from "@actions/tool-cache"; -import * as cache from "./cache"; -import type { DownloadResult, InstallResult, Installer } from "./installer"; -import { Arch, OS, type Platform } from "./platform"; - -export class SnapshotInstaller implements Installer { - constructor(private readonly platform: Platform) {} - - async checkInstalled(version: string): Promise { - const root = await cache.find("chromium", version); - if (root) { - return { root, bin: "chrome" }; - } - } - - async downloadBrowser(version: string): Promise { - const url = `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${makePlatformPart( - this.platform, - )}%2F${version}%2F${makeBasename(this.platform)}?alt=media`; - - core.info(`Acquiring ${version} from ${url}`); - const archive = await tc.downloadTool(url); - return { archive }; - } - - async installBrowser( - version: string, - archive: string, - ): Promise { - const extPath = await tc.extractZip(archive); - let root = (() => { - switch (this.platform.os) { - case OS.DARWIN: - return path.join(extPath, "chrome-mac"); - case OS.LINUX: - return path.join(extPath, "chrome-linux"); - case OS.WINDOWS: - return path.join(extPath, "chrome-win"); - } - })(); - const bin = (() => { - switch (this.platform.os) { - case OS.DARWIN: - return "Chromium.app/Contents/MacOS/Chromium"; - case OS.LINUX: - return "chrome"; - case OS.WINDOWS: - return "chrome.exe"; - } - })(); - - root = await cache.cacheDir(root, "chromium", version); - core.info(`Successfully Installed chromium to ${root}`); - - return { root, bin }; - } -} - -export class LatestInstaller implements Installer { - private readonly http = new httpm.HttpClient("setup-chrome"); - - private readonly snapshotInstaller = new SnapshotInstaller(this.platform); - - constructor(private readonly platform: Platform) {} - - async checkInstalled(version: string): Promise { - const root = await cache.find("chromium", version); - if (root) { - return { root, bin: "chrome" }; - } - } - - async downloadBrowser(_version: string): Promise { - const latestVersionURL = `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${makePlatformPart( - this.platform, - )}%2FLAST_CHANGE?alt=media`; - const resp = await this.http.get(latestVersionURL); - if (resp.message.statusCode !== httpm.HttpCodes.OK) { - throw new Error( - `Failed to get latest version: server returns ${resp.message.statusCode}`, - ); - } - const version = await resp.readBody(); - - return this.snapshotInstaller.downloadBrowser(version); - } - - async installBrowser( - version: string, - archive: string, - ): Promise { - return this.snapshotInstaller.installBrowser(version, archive); - } -} - -const makeBasename = ({ os }: Platform): string => { - switch (os) { - case OS.DARWIN: - return "chrome-mac.zip"; - case OS.LINUX: - return "chrome-linux.zip"; - case OS.WINDOWS: - return "chrome-win.zip"; - } -}; - -const makePlatformPart = ({ os, arch }: Platform): string => { - if (os === OS.DARWIN && arch === Arch.AMD64) { - return "Mac"; - } else if (os === OS.DARWIN && arch === Arch.ARM64) { - return "Mac_Arm"; - } else if (os === OS.LINUX && arch === Arch.I686) { - return "Linux"; - } else if (os === OS.LINUX && arch === Arch.AMD64) { - return "Linux_x64"; - } else if (os === OS.WINDOWS && arch === Arch.I686) { - return "Win"; - } else if (os === OS.WINDOWS && arch === Arch.AMD64) { - return "Win_x64"; - } - throw new Error(`Unsupported platform "${os}" "${arch}"`); -}; diff --git a/src/snapshot_bucket.ts b/src/snapshot_bucket.ts new file mode 100644 index 0000000..0f35fbc --- /dev/null +++ b/src/snapshot_bucket.ts @@ -0,0 +1,53 @@ +import * as httpm from "@actions/http-client"; +import { Arch, OS, type Platform } from "./platform"; + +export const resolveLatestVersion = async ( + platform: Platform, +): Promise => { + const url = `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${makePlatformPart( + platform, + )}%2FLAST_CHANGE?alt=media`; + + const http = new httpm.HttpClient("setup-chrome"); + const resp = await http.get(url); + if (resp.message.statusCode !== httpm.HttpCodes.OK) { + throw new Error( + `Failed to get latest version: server returns ${resp.message.statusCode}`, + ); + } + return resp.readBody(); +}; + +export const downloadURL = (platform: Platform, version: string): string => { + return `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${makePlatformPart( + platform, + )}%2F${version}%2F${makeBasename(platform)}?alt=media`; +}; + +const makeBasename = ({ os }: Platform): string => { + switch (os) { + case OS.DARWIN: + return "chrome-mac.zip"; + case OS.LINUX: + return "chrome-linux.zip"; + case OS.WINDOWS: + return "chrome-win.zip"; + } +}; + +const makePlatformPart = ({ os, arch }: Platform): string => { + if (os === OS.DARWIN && arch === Arch.AMD64) { + return "Mac"; + } else if (os === OS.DARWIN && arch === Arch.ARM64) { + return "Mac_Arm"; + } else if (os === OS.LINUX && arch === Arch.I686) { + return "Linux"; + } else if (os === OS.LINUX && arch === Arch.AMD64) { + return "Linux_x64"; + } else if (os === OS.WINDOWS && arch === Arch.I686) { + return "Win"; + } else if (os === OS.WINDOWS && arch === Arch.AMD64) { + return "Win_x64"; + } + throw new Error(`Unsupported platform "${os}" "${arch}"`); +}; diff --git a/src/snapshot_installer.ts b/src/snapshot_installer.ts new file mode 100644 index 0000000..2d20eca --- /dev/null +++ b/src/snapshot_installer.ts @@ -0,0 +1,57 @@ +import * as path from "node:path"; +import * as core from "@actions/core"; +import * as tc from "@actions/tool-cache"; +import * as cache from "./cache"; +import type { DownloadResult, InstallResult, Installer } from "./installer"; +import { OS, type Platform } from "./platform"; +import { downloadURL } from "./snapshot_bucket"; + +export class SnapshotInstaller implements Installer { + constructor(private readonly platform: Platform) {} + + async checkInstalled(version: string): Promise { + const root = await cache.find("chromium", version); + if (root) { + return { root, bin: "chrome" }; + } + } + + async downloadBrowser(version: string): Promise { + const url = downloadURL(this.platform, version); + core.info(`Acquiring ${version} from ${url}`); + const archive = await tc.downloadTool(url); + return { archive }; + } + + async installBrowser( + version: string, + archive: string, + ): Promise { + const extPath = await tc.extractZip(archive); + let root = (() => { + switch (this.platform.os) { + case OS.DARWIN: + return path.join(extPath, "chrome-mac"); + case OS.LINUX: + return path.join(extPath, "chrome-linux"); + case OS.WINDOWS: + return path.join(extPath, "chrome-win"); + } + })(); + const bin = (() => { + switch (this.platform.os) { + case OS.DARWIN: + return "Chromium.app/Contents/MacOS/Chromium"; + case OS.LINUX: + return "chrome"; + case OS.WINDOWS: + return "chrome.exe"; + } + })(); + + root = await cache.cacheDir(root, "chromium", version); + core.info(`Successfully Installed chromium to ${root}`); + + return { root, bin }; + } +} From 28d15d420f6116818f46279f3b64b5a231e4670c Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 25 May 2024 13:36:14 +0000 Subject: [PATCH 05/20] add installer tests --- __test__/channel_linux.test.ts | 95 ++++++++++++++++++++++++++++ __test__/channel_macos.test.ts | 89 ++++++++++++++++++++++++++ __test__/channel_windows.test.ts | 98 +++++++++++++++++++++++++++++ __test__/snapshot_installer.test.ts | 58 +++++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 __test__/channel_linux.test.ts create mode 100644 __test__/channel_macos.test.ts create mode 100644 __test__/channel_windows.test.ts create mode 100644 __test__/snapshot_installer.test.ts diff --git a/__test__/channel_linux.test.ts b/__test__/channel_linux.test.ts new file mode 100644 index 0000000..c6b7ca6 --- /dev/null +++ b/__test__/channel_linux.test.ts @@ -0,0 +1,95 @@ +import * as fs from "node:fs"; +import * as exec from "@actions/exec"; +import * as tc from "@actions/tool-cache"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import * as cache from "../src/cache"; +import { LinuxChannelInstaller } from "../src/channel_linux"; + +const cacheFindSpy = vi.spyOn(cache, "find"); +const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); +const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const execSpy = vi.spyOn(exec, "exec"); +const fsMkdtempSpy = vi.spyOn(fs.promises, "mkdtemp"); +const fsUnlinkSpy = vi.spyOn(fs.promises, "unlink"); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("LinuxChannelInstaller", () => { + const installer = new LinuxChannelInstaller({ os: "linux", arch: "amd64" }); + + describe("checkInstalled", () => { + test("return undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalled("stable"); + + expect(result).toBeUndefined(); + }); + + test("return install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/chromium"); + + const result = await installer.checkInstalled("stable"); + + expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); + }); + }); + + describe("downloadBrowser", () => { + test("throw error if version is not release channel", async () => { + await expect(installer.downloadBrowser("foo")).rejects.toThrowError( + "Unexpected version: foo", + ); + }); + + test("throw error if version is canary", async () => { + await expect(installer.downloadBrowser("canary")).rejects.toThrowError( + "Chrome canary not supported for platform linux amd64", + ); + }); + + test("download stable version", async () => { + tcDownloadToolSpy.mockResolvedValue("/path/to/downloaded.deb"); + + const result = await installer.downloadBrowser("stable"); + + expect(result).toEqual({ archive: "/path/to/downloaded.deb" }); + expect(tcDownloadToolSpy).toHaveBeenCalled(); + }); + }); + + describe("installBrowser", () => { + test("throw error if version is not release channel", async () => { + await expect( + installer.installBrowser("foo", "/path/to/downloaded.deb"), + ).rejects.toThrowError("Unexpected version: foo"); + }); + + test("throw error if version is canary", async () => { + await expect( + installer.installBrowser("canary", "/path/to/downloaded.deb"), + ).rejects.toThrowError("Chrome canary not supported for Linux"); + }); + + test("install stable version", async () => { + fsMkdtempSpy.mockResolvedValue("/deb-abcdef"); + fsUnlinkSpy.mockResolvedValue(undefined); + execSpy.mockResolvedValue(0); + cacheCacheDirSpy.mockResolvedValue("/path/to/chromium"); + + const result = await installer.installBrowser( + "stable", + "/path/to/downloaded.deb", + ); + + expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); + expect(cacheCacheDirSpy).toHaveBeenCalledWith( + "/deb-abcdef", + "chromium", + "stable", + ); + }); + }); +}); diff --git a/__test__/channel_macos.test.ts b/__test__/channel_macos.test.ts new file mode 100644 index 0000000..2243256 --- /dev/null +++ b/__test__/channel_macos.test.ts @@ -0,0 +1,89 @@ +import * as fs from "node:fs"; +import * as exec from "@actions/exec"; +import * as tc from "@actions/tool-cache"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import * as cache from "../src/cache"; +import { MacOSChannelInstaller } from "../src/channel_macos"; + +const cacheFindSpy = vi.spyOn(cache, "find"); +const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); +const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const fsSymlinkSpy = vi.spyOn(fs.promises, "symlink"); +const execSpy = vi.spyOn(exec, "exec"); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("MacOSChannelInstaller", () => { + const installer = new MacOSChannelInstaller({ + os: "darwin", + arch: "amd64", + }); + + describe("checkInstalled", () => { + test("return undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalled("stable"); + + expect(result).toBeUndefined(); + }); + + test("return install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/Chromium.app"); + + const result = await installer.checkInstalled("stable"); + + expect(result).toEqual({ + root: "/path/to/Chromium.app", + bin: "Contents/MacOS/chrome", + }); + }); + }); + + describe("downloadBrowser", () => { + test("throw error if version is not release channel", async () => { + await expect(installer.downloadBrowser("foo")).rejects.toThrowError( + "Unexpected version: foo", + ); + }); + + test("download stable version", async () => { + tcDownloadToolSpy.mockResolvedValue("/path/to/downloaded.dmg"); + + const result = await installer.downloadBrowser("stable"); + + expect(result).toEqual({ archive: "/path/to/downloaded.dmg" }); + }); + }); + + describe("installBrowser", () => { + test("throw error if version is not release channel", async () => { + await expect( + installer.installBrowser("foo", "/path/to/downloaded.dmg"), + ).rejects.toThrowError("Unexpected version: foo"); + }); + + test("install stable version", async () => { + execSpy.mockResolvedValue(0); + fsSymlinkSpy.mockResolvedValue(); + cacheCacheDirSpy.mockResolvedValue("/path/to/Chromium.app"); + + const result = await installer.installBrowser( + "stable", + "/path/to/downloaded.dmg", + ); + + expect(result).toEqual({ + root: "/path/to/Chromium.app", + bin: "Contents/MacOS/chrome", + }); + expect(cacheCacheDirSpy).toHaveBeenCalledWith( + "/Volumes/downloaded.dmg/Google Chrome.app", + "chromium", + "stable", + ); + }); + }); +}); diff --git a/__test__/channel_windows.test.ts b/__test__/channel_windows.test.ts new file mode 100644 index 0000000..8445e4f --- /dev/null +++ b/__test__/channel_windows.test.ts @@ -0,0 +1,98 @@ +import * as fs from "node:fs"; +import * as exec from "@actions/exec"; +import * as tc from "@actions/tool-cache"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { WindowsChannelInstaller } from "../src/channel_windows"; + +const fsStatSpy = vi.spyOn(fs.promises, "stat"); +const fsRenameSpy = vi.spyOn(fs.promises, "rename"); +const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const execSpy = vi.spyOn(exec, "exec"); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("WindowsChannelInstaller", () => { + const installer = new WindowsChannelInstaller({ + os: "windows", + arch: "amd64", + }); + + describe("checkInstalled", () => { + test("returns undefined if the root directory does not exist", async () => { + const result = await installer.checkInstalled("stable"); + expect(result).toBe(undefined); + }); + + test("returns the root directory and bin path if the root directory exists", async () => { + fsStatSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalled("stable"); + expect(result).toEqual({ + root: "C:\\Program Files\\Google\\Chrome\\Application", + bin: "chrome.exe", + }); + expect(fsStatSpy).toHaveBeenCalledWith( + "C:\\Program Files\\Google\\Chrome\\Application", + ); + }); + }); + + describe("downloadBrowser", () => { + test("throws an error if the version is not a release channel name", async () => { + await expect(installer.downloadBrowser("foo")).rejects.toThrow( + "Unexpected version: foo", + ); + }); + + test("downloads the stable version of Chrome", async () => { + tcDownloadToolSpy.mockResolvedValue("C:\\path\\to\\downloaded\\file"); + fsRenameSpy.mockResolvedValue(undefined); + + await installer.downloadBrowser("stable"); + expect(tcDownloadToolSpy).toHaveBeenCalled(); + }); + }); + + describe("installBrowser", () => { + test("throws an error if the version is not a release channel name", async () => { + expect(() => + installer.installBrowser( + "foo", + "C:\\path\\to\\downloaded\\installer.exe", + ), + ).rejects.toThrow("Unexpected version: foo"); + }); + + test("install the stable version of Chrome", async () => { + execSpy.mockResolvedValue(undefined); + fsRenameSpy.mockResolvedValue(undefined); + + const result = await installer.installBrowser( + "stable", + "C:\\path\\to\\downloaded\\installer.exe", + ); + expect(result).toEqual({ + root: "C:\\Program Files\\Google\\Chrome\\Application", + bin: "chrome.exe", + }); + expect(execSpy).toHaveBeenCalledWith( + "C:\\path\\to\\downloaded\\installer.exe", + ["/silent", "/install"], + ); + }); + }); + + describe("unsupported platform", () => { + test("throws an error if the platform is not supported", async () => { + const installer2 = new WindowsChannelInstaller({ + os: "windows", + arch: "arm64", + }); + await expect(installer2.downloadBrowser("stable")).rejects.toThrow( + 'Chrome stable not supported for platform "windows" "arm64"', + ); + }); + }); +}); diff --git a/__test__/snapshot_installer.test.ts b/__test__/snapshot_installer.test.ts new file mode 100644 index 0000000..6e51648 --- /dev/null +++ b/__test__/snapshot_installer.test.ts @@ -0,0 +1,58 @@ +import * as tc from "@actions/tool-cache"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import * as cache from "../src/cache"; +import { SnapshotInstaller } from "../src/snapshot_installer"; + +const cacheFindSpy = vi.spyOn(cache, "find"); +const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); +const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const tcExtractZipSpy = vi.spyOn(tc, "extractZip"); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("SnapshotInstaller", () => { + const installer = new SnapshotInstaller({ os: "linux", arch: "amd64" }); + + describe("checkInstalled", () => { + test("returns undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalled("123"); + expect(result).toBe(undefined); + }); + + test("returns install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/chromium"); + + const result = await installer.checkInstalled("123"); + expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); + }); + }); + + describe("downloadBrowser", () => { + test("downloads the browser", async () => { + tcDownloadToolSpy.mockResolvedValue("/path/to/downloaded.zip"); + + const result = await installer.downloadBrowser("123456"); + expect(result).toEqual({ archive: "/path/to/downloaded.zip" }); + expect(tcDownloadToolSpy).toHaveBeenCalled(); + }); + }); + + describe("installBrowser", () => { + test("installs the browser", async () => { + tcExtractZipSpy.mockResolvedValue("/path/to/ext"); + cacheCacheDirSpy.mockResolvedValue("/path/to/chromium"); + + const result = await installer.installBrowser( + "123456", + "/path/to/archive", + ); + expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); + expect(tcExtractZipSpy).toHaveBeenCalled(); + expect(cacheCacheDirSpy).toHaveBeenCalled(); + }); + }); +}); From ee5815ae454e107ed7615a1739df8dac0fefbfb0 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 02:39:33 +0000 Subject: [PATCH 06/20] resolve version and url together --- __test__/version_installer.test.ts | 31 ++++-------- src/version_installer.ts | 78 ++++++++---------------------- 2 files changed, 30 insertions(+), 79 deletions(-) diff --git a/__test__/version_installer.test.ts b/__test__/version_installer.test.ts index 2fbb5c0..b95206e 100644 --- a/__test__/version_installer.test.ts +++ b/__test__/version_installer.test.ts @@ -32,22 +32,15 @@ afterEach(() => { describe("VersionResolver", () => { test.each` - spec | resolved - ${"120.0.6099.5"} | ${"120.0.6099.5"} - ${"120.0.6099.x"} | ${"120.0.6099.56"} - ${"1234.0.6099.x"} | ${undefined} - `("should resolve known good versions", async ({ spec, resolved }) => { + spec | version | url + ${"120.0.6099.5"} | ${"120.0.6099.5"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.5/linux64/chrome-linux64.zip"} + ${"120.0.6099.x"} | ${"120.0.6099.56"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chrome-linux64.zip"} + ${"1234.0.6099.x"} | ${undefined} | ${undefined} + `("should resolve known good versions for $spec", async ({ spec, version, url }) => { const resolver = new KnownGoodVersionResolver("linux64"); - const version = await resolver.resolve(spec); - expect(version?.toString()).toEqual(resolved); - }); - - test("should resolve an url for a known good version", async () => { - const resolver = new KnownGoodVersionResolver("linux64"); - const url = await resolver.resolveUrl("120.0.6099.x"); - expect(url).toEqual( - "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chrome-linux64.zip", - ); + const resolved = await resolver.resolve(spec); + expect(resolved?.version).toEqual(version); + expect(resolved?.chromeDownloadURL).toEqual(url); }); test("should cache known good versions", async () => { @@ -63,11 +56,7 @@ describe("KnownGoodVersionInstaller", () => { const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); test("should return installed path if installed", async () => { - tcFindSpy.mockImplementation((name: string, version: string) => { - return Promise.resolve( - `/opt/hostedtoolcache/setup-chrome/${name}/${version}/x64`, - ); - }); + tcFindSpy.mockResolvedValue("/opt/hostedtoolcache/setup-chrome/chromium/120.0.6099.56/x64"); const installer = new KnownGoodVersionInstaller({ os: "linux", @@ -77,7 +66,7 @@ describe("KnownGoodVersionInstaller", () => { expect(installed?.root).toEqual( "/opt/hostedtoolcache/setup-chrome/chromium/120.0.6099.56/x64", ); - expect(tcFindSpy).toHaveBeenCalledWith("chromium", "120.0.6099.56"); + expect(tcFindSpy).toHaveBeenCalledWith("chromium", "120.0.6099.x"); }); test("should download zip archive", async () => { diff --git a/src/version_installer.ts b/src/version_installer.ts index f31fab5..c86283f 100644 --- a/src/version_installer.ts +++ b/src/version_installer.ts @@ -26,13 +26,18 @@ export type KnownGoodVersion = { version: string; revision: string; downloads: { - chrome: Array<{ + chrome?: Array<{ platform: KnownGoodVersionPlatform; url: string; }>; }; }; +type ResolvedVersion = { + version: string; + chromeDownloadURL: string; +}; + export class KnownGoodVersionResolver { private readonly http = new httpm.HttpClient("setup-chrome"); @@ -40,55 +45,24 @@ export class KnownGoodVersionResolver { private knownGoodVersionsCache?: KnownGoodVersion[]; - private readonly resolvedVersions = new Map(); - constructor(platform: KnownGoodVersionPlatform) { this.platform = platform; } - async resolve(version: string): Promise { + async resolve(version: string): Promise { const spec = parse(version); - if (this.resolvedVersions.has(spec.toString())) { - return this.resolvedVersions.get(spec.toString()); - } const knownGoodVersions = await this.getKnownGoodVersions(); for (const version of knownGoodVersions) { - if (!spec.satisfies(version.version)) { - continue; - } - const found = version.downloads.chrome.find( - ({ platform }) => platform === this.platform, - ); - if (found) { - this.resolvedVersions.set(spec.toString(), version.version); - return version.version; + if (spec.satisfies(version.version) && version.downloads.chrome) { + const found = version.downloads.chrome.find( + ({ platform }) => platform === this.platform, + ); + if (found) { + return { version: version.version, chromeDownloadURL: found.url }; + } } } - return undefined; - } - - async resolveUrl(version: string): Promise { - const resolved = await this.resolve(version); - if (!resolved) { - return undefined; - } - - const knownGoodVersions = await this.getKnownGoodVersions(); - const knownGoodVersion = knownGoodVersions.find( - (v) => v.version === resolved.toString(), - ); - if (!knownGoodVersion) { - return undefined; - } - - const found = knownGoodVersion.downloads.chrome.find( - ({ platform }) => platform === this.platform, - ); - if (!found) { - return undefined; - } - return found.url; } private async getKnownGoodVersions(): Promise { @@ -139,12 +113,7 @@ export class KnownGoodVersionInstaller implements Installer { } async checkInstalled(version: string): Promise { - const resolved = await this.versionResolver.resolve(version); - if (!resolved) { - return undefined; - } - - const root = await cache.find("chromium", resolved.toString()); + const root = await cache.find("chromium", version); if (root) { return { root, bin: "chrome" }; } @@ -155,13 +124,10 @@ export class KnownGoodVersionInstaller implements Installer { if (!resolved) { throw new Error(`Version ${version} not found in known good versions`); } - - const url = await this.versionResolver.resolveUrl(version); - if (!url) { - throw new Error(`Version ${version} not found in known good versions`); - } - const archive = await tc.downloadTool(url); - core.info(`Acquiring ${resolved} from ${url}`); + core.info( + `Acquiring ${resolved.version} from ${resolved.chromeDownloadURL}`, + ); + const archive = await tc.downloadTool(resolved.chromeDownloadURL); return { archive }; } @@ -179,11 +145,7 @@ export class KnownGoodVersionInstaller implements Installer { `chrome-${this.knownGoodVersionPlatform}`, ); - const root = await cache.cacheDir( - extAppRoot, - "chromium", - resolved.toString(), - ); + const root = await cache.cacheDir(extAppRoot, "chromium", resolved.version); core.info(`Successfully Installed chromium to ${root}`); const bin = (() => { switch (this.platform.os) { From 745bf2a2f0b54c414995c37ba5b6fd4a07a34230 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 03:33:30 +0000 Subject: [PATCH 07/20] LatestInstaller, SnapshotInstaller, and VersionResolver supports chromedirver installation --- __test__/snapshot_installer.test.ts | 34 +++++++++- __test__/version_installer.test.ts | 97 ++++++++++++++++++++++------- src/installer.ts | 4 ++ src/latest_installer.ts | 9 +-- src/snapshot_bucket.ts | 29 ++++++++- src/snapshot_installer.ts | 43 ++++++++++++- src/version_installer.ts | 85 ++++++++++++++++++++++--- 7 files changed, 253 insertions(+), 48 deletions(-) diff --git a/__test__/snapshot_installer.test.ts b/__test__/snapshot_installer.test.ts index 6e51648..6fa7289 100644 --- a/__test__/snapshot_installer.test.ts +++ b/__test__/snapshot_installer.test.ts @@ -33,10 +33,10 @@ describe("SnapshotInstaller", () => { describe("downloadBrowser", () => { test("downloads the browser", async () => { - tcDownloadToolSpy.mockResolvedValue("/path/to/downloaded.zip"); + tcDownloadToolSpy.mockResolvedValue("/tmp/chrome.zip"); const result = await installer.downloadBrowser("123456"); - expect(result).toEqual({ archive: "/path/to/downloaded.zip" }); + expect(result).toEqual({ archive: "/tmp/chrome.zip" }); expect(tcDownloadToolSpy).toHaveBeenCalled(); }); }); @@ -48,11 +48,39 @@ describe("SnapshotInstaller", () => { const result = await installer.installBrowser( "123456", - "/path/to/archive", + "/tmp/chrome.zip", ); expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); expect(tcExtractZipSpy).toHaveBeenCalled(); expect(cacheCacheDirSpy).toHaveBeenCalled(); }); }); + + describe("downloadDriver", () => { + test("downloads the driver", async () => { + tcDownloadToolSpy.mockResolvedValue("/tmp/chromedriver.zip"); + + const result = await installer.downloadDriver("123456"); + expect(result).toEqual({ archive: "/tmp/chromedriver.zip" }); + expect(tcDownloadToolSpy).toHaveBeenCalled(); + }); + }); + + describe("installDriver", () => { + test("installs the driver", async () => { + tcExtractZipSpy.mockResolvedValue("/path/to/ext"); + cacheCacheDirSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.installDriver( + "123456", + "/tmp/chromedriver.zip", + ); + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + expect(tcExtractZipSpy).toHaveBeenCalled(); + expect(cacheCacheDirSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/__test__/version_installer.test.ts b/__test__/version_installer.test.ts index b95206e..beb9d71 100644 --- a/__test__/version_installer.test.ts +++ b/__test__/version_installer.test.ts @@ -10,6 +10,10 @@ import { } from "../src/version_installer"; const getJsonSpy = vi.spyOn(httpm.HttpClient.prototype, "getJson"); +const tcExtractZipSpy = vi.spyOn(tc, "extractZip"); +const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const cacheFindSpy = vi.spyOn(cache, "find"); +const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); beforeEach(() => { const mockDataPath = path.join( @@ -32,16 +36,20 @@ afterEach(() => { describe("VersionResolver", () => { test.each` - spec | version | url - ${"120.0.6099.5"} | ${"120.0.6099.5"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.5/linux64/chrome-linux64.zip"} - ${"120.0.6099.x"} | ${"120.0.6099.56"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chrome-linux64.zip"} - ${"1234.0.6099.x"} | ${undefined} | ${undefined} - `("should resolve known good versions for $spec", async ({ spec, version, url }) => { - const resolver = new KnownGoodVersionResolver("linux64"); - const resolved = await resolver.resolve(spec); - expect(resolved?.version).toEqual(version); - expect(resolved?.chromeDownloadURL).toEqual(url); - }); + spec | version | browserURL | driverURL + ${"120.0.6099.5"} | ${"120.0.6099.5"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.5/linux64/chrome-linux64.zip"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.5/linux64/chromedriver-linux64.zip"} + ${"120.0.6099.x"} | ${"120.0.6099.56"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chrome-linux64.zip"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chromedriver-linux64.zip"} + ${"1234.0.6099.x"} | ${undefined} | ${undefined} | ${undefined} + `( + "should resolve known good versions for $spec", + async ({ spec, version, browserURL, driverURL }) => { + const resolver = new KnownGoodVersionResolver("linux64"); + const resolved = await resolver.resolve(spec); + expect(resolved?.version).toEqual(version); + expect(resolved?.browserDownloadURL).toEqual(browserURL); + expect(resolved?.driverDownloadURL).toEqual(driverURL); + }, + ); test("should cache known good versions", async () => { const resolver = new KnownGoodVersionResolver("linux64"); @@ -52,30 +60,71 @@ describe("VersionResolver", () => { }); describe("KnownGoodVersionInstaller", () => { - const tcFindSpy = vi.spyOn(cache, "find"); - const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); + const installer = new KnownGoodVersionInstaller({ + os: "linux", + arch: "amd64", + }); - test("should return installed path if installed", async () => { - tcFindSpy.mockResolvedValue("/opt/hostedtoolcache/setup-chrome/chromium/120.0.6099.56/x64"); + test("checkInstalled should return installed path if installed", async () => { + cacheFindSpy.mockResolvedValue( + "/opt/hostedtoolcache/setup-chrome/chromium/120.0.6099.56/x64", + ); - const installer = new KnownGoodVersionInstaller({ - os: "linux", - arch: "amd64", - }); const installed = await installer.checkInstalled("120.0.6099.x"); expect(installed?.root).toEqual( "/opt/hostedtoolcache/setup-chrome/chromium/120.0.6099.56/x64", ); - expect(tcFindSpy).toHaveBeenCalledWith("chromium", "120.0.6099.x"); + expect(cacheFindSpy).toHaveBeenCalledWith("chromium", "120.0.6099.x"); }); - test("should download zip archive", async () => { + test("downloadBrowser should download browser archive", async () => { tcDownloadToolSpy.mockImplementation(async () => "/tmp/chromium.zip"); - const installer = new KnownGoodVersionInstaller({ - os: "linux", - arch: "amd64", - }); + const downloaded = await installer.downloadBrowser("120.0.6099.x"); expect(downloaded?.archive).toEqual("/tmp/chromium.zip"); + expect(tcDownloadToolSpy).toHaveBeenCalled(); + }); + + test("installDriver should install browser", async () => { + tcExtractZipSpy.mockImplementation(async () => "/tmp/extracted"); + cacheCacheDirSpy.mockImplementation(async () => "/path/to/chromium"); + + const installed = await installer.installBrowser( + "120.0.6099.x", + "/tmp/chromium.zip", + ); + expect(installed).toEqual({ root: "/path/to/chromium", bin: "chrome" }); + expect(cacheCacheDirSpy).toHaveBeenCalledWith( + "/tmp/extracted/chrome-linux64", + "chromium", + "120.0.6099.56", + ); + }); + + test("downloadDriver should download driver archive", async () => { + tcDownloadToolSpy.mockImplementation(async () => "/tmp/chromedriver.zip"); + + const downloaded = await installer.downloadDriver("120.0.6099.x"); + expect(downloaded?.archive).toEqual("/tmp/chromedriver.zip"); + expect(tcDownloadToolSpy).toHaveBeenCalled(); + }); + + test("installDriver should install driver", async () => { + tcExtractZipSpy.mockImplementation(async () => "/tmp/extracted"); + cacheCacheDirSpy.mockImplementation(async () => "/path/to/chromedriver"); + + const installed = await installer.installDriver( + "120.0.6099.x", + "/tmp/chromedriver.zip", + ); + expect(installed).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + expect(cacheCacheDirSpy).toHaveBeenCalledWith( + "/tmp/extracted/chromedriver-linux64", + "chromedriver", + "120.0.6099.56", + ); }); }); diff --git a/src/installer.ts b/src/installer.ts index 0b4578b..f907407 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -24,6 +24,10 @@ export interface Installer { downloadBrowser(version: string): Promise; installBrowser(version: string, archive: string): Promise; + + downloadDriver(version: string): Promise; + + installDriver(version: string, archive: string): Promise; } export const install = async ( diff --git a/src/latest_installer.ts b/src/latest_installer.ts index 0e83e17..9b64760 100644 --- a/src/latest_installer.ts +++ b/src/latest_installer.ts @@ -21,10 +21,7 @@ export class LatestInstaller implements Installer { return this.snapshotInstaller.downloadBrowser(version); } - async installBrowser( - version: string, - archive: string, - ): Promise { - return this.snapshotInstaller.installBrowser(version, archive); - } + installBrowser = this.snapshotInstaller.installBrowser; + downloadDriver = this.snapshotInstaller.downloadDriver; + installDriver = this.snapshotInstaller.installDriver; } diff --git a/src/snapshot_bucket.ts b/src/snapshot_bucket.ts index 0f35fbc..e8f4c79 100644 --- a/src/snapshot_bucket.ts +++ b/src/snapshot_bucket.ts @@ -18,13 +18,25 @@ export const resolveLatestVersion = async ( return resp.readBody(); }; -export const downloadURL = (platform: Platform, version: string): string => { +export const browserDownloadURL = ( + platform: Platform, + version: string, +): string => { return `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${makePlatformPart( platform, - )}%2F${version}%2F${makeBasename(platform)}?alt=media`; + )}%2F${version}%2F${browserFileName(platform)}?alt=media`; }; -const makeBasename = ({ os }: Platform): string => { +export const driverDownloadURL = ( + platform: Platform, + version: string, +): string => { + return `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${makePlatformPart( + platform, + )}%2F${version}%2F${driverFileName(platform)}?alt=media`; +}; + +const browserFileName = ({ os }: Platform): string => { switch (os) { case OS.DARWIN: return "chrome-mac.zip"; @@ -35,6 +47,17 @@ const makeBasename = ({ os }: Platform): string => { } }; +const driverFileName = ({ os }: Platform): string => { + switch (os) { + case OS.DARWIN: + return "chromedriver_mac64.zip"; + case OS.LINUX: + return "chromedriver_linux64.zip"; + case OS.WINDOWS: + return "chromedriver_win32.zip"; + } +}; + const makePlatformPart = ({ os, arch }: Platform): string => { if (os === OS.DARWIN && arch === Arch.AMD64) { return "Mac"; diff --git a/src/snapshot_installer.ts b/src/snapshot_installer.ts index 2d20eca..10d1426 100644 --- a/src/snapshot_installer.ts +++ b/src/snapshot_installer.ts @@ -4,7 +4,7 @@ import * as tc from "@actions/tool-cache"; import * as cache from "./cache"; import type { DownloadResult, InstallResult, Installer } from "./installer"; import { OS, type Platform } from "./platform"; -import { downloadURL } from "./snapshot_bucket"; +import { browserDownloadURL, driverDownloadURL } from "./snapshot_bucket"; export class SnapshotInstaller implements Installer { constructor(private readonly platform: Platform) {} @@ -17,7 +17,7 @@ export class SnapshotInstaller implements Installer { } async downloadBrowser(version: string): Promise { - const url = downloadURL(this.platform, version); + const url = browserDownloadURL(this.platform, version); core.info(`Acquiring ${version} from ${url}`); const archive = await tc.downloadTool(url); return { archive }; @@ -54,4 +54,43 @@ export class SnapshotInstaller implements Installer { return { root, bin }; } + + async downloadDriver(version: string): Promise { + const url = driverDownloadURL(this.platform, version); + core.info(`Acquiring ${version} from ${url}`); + const archive = await tc.downloadTool(url); + return { archive }; + } + + async installDriver( + version: string, + archive: string, + ): Promise { + const extPath = await tc.extractZip(archive); + let root = (() => { + switch (this.platform.os) { + case OS.DARWIN: + return path.join(extPath, "chromedriver_mac64"); + case OS.LINUX: + return path.join(extPath, "chromedriver_linux64"); + case OS.WINDOWS: + return path.join(extPath, "chromedriver_win32"); + } + })(); + const bin = (() => { + switch (this.platform.os) { + case OS.DARWIN: + return "chromedriver"; + case OS.LINUX: + return "chromedriver"; + case OS.WINDOWS: + return "chromedriver.exe"; + } + })(); + + root = await cache.cacheDir(root, "chromedriver", version); + core.info(`Successfully Installed chromedriver to ${root}`); + + return { root, bin }; + } } diff --git a/src/version_installer.ts b/src/version_installer.ts index c86283f..5e1ff7c 100644 --- a/src/version_installer.ts +++ b/src/version_installer.ts @@ -30,12 +30,17 @@ export type KnownGoodVersion = { platform: KnownGoodVersionPlatform; url: string; }>; + chromedriver?: Array<{ + platform: KnownGoodVersionPlatform; + url: string; + }>; }; }; type ResolvedVersion = { version: string; - chromeDownloadURL: string; + browserDownloadURL: string; + driverDownloadURL: string; }; export class KnownGoodVersionResolver { @@ -54,13 +59,26 @@ export class KnownGoodVersionResolver { const knownGoodVersions = await this.getKnownGoodVersions(); for (const version of knownGoodVersions) { - if (spec.satisfies(version.version) && version.downloads.chrome) { - const found = version.downloads.chrome.find( - ({ platform }) => platform === this.platform, - ); - if (found) { - return { version: version.version, chromeDownloadURL: found.url }; - } + if ( + !spec.satisfies(version.version) || + !version.downloads.chrome || + !version.downloads.chromedriver + ) { + continue; + } + const browser = version.downloads.chrome.find( + ({ platform }) => platform === this.platform, + ); + const driver = version.downloads.chromedriver.find( + ({ platform }) => platform === this.platform, + ); + + if (browser && driver) { + return { + version: version.version, + browserDownloadURL: browser.url, + driverDownloadURL: driver.url, + }; } } } @@ -124,10 +142,11 @@ export class KnownGoodVersionInstaller implements Installer { if (!resolved) { throw new Error(`Version ${version} not found in known good versions`); } + core.info( - `Acquiring ${resolved.version} from ${resolved.chromeDownloadURL}`, + `Acquiring ${resolved.version} from ${resolved.browserDownloadURL}`, ); - const archive = await tc.downloadTool(resolved.chromeDownloadURL); + const archive = await tc.downloadTool(resolved.browserDownloadURL); return { archive }; } @@ -159,4 +178,50 @@ export class KnownGoodVersionInstaller implements Installer { })(); return { root: root, bin }; } + + async downloadDriver(version: string): Promise { + const resolved = await this.versionResolver.resolve(version); + if (!resolved) { + throw new Error(`Version ${version} not found in known good versions`); + } + + core.info( + `Acquiring ${resolved.version} from ${resolved.driverDownloadURL}`, + ); + const archive = await tc.downloadTool(resolved.driverDownloadURL); + return { archive }; + } + + async installDriver( + version: string, + archive: string, + ): Promise { + const resolved = await this.versionResolver.resolve(version); + if (!resolved) { + throw new Error(`Version ${version} not found in known good versions`); + } + const extPath = await tc.extractZip(archive); + const extAppRoot = path.join( + extPath, + `chromedriver-${this.knownGoodVersionPlatform}`, + ); + + const root = await cache.cacheDir( + extAppRoot, + "chromedriver", + resolved.version, + ); + core.info(`Successfully Installed chromedriver to ${root}`); + const bin = (() => { + switch (this.platform.os) { + case OS.DARWIN: + return "chromedriver"; + case OS.LINUX: + return "chromedriver"; + case OS.WINDOWS: + return "chromedriver.exe"; + } + })(); + return { root: root, bin }; + } } From 79a09eebc74edc7eeb114c8f44f1ed0252e2ea1c Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 05:48:54 +0000 Subject: [PATCH 08/20] support chromedirver installation on all installers --- __test__/channel_linux.test.ts | 40 ++++ __test__/channel_macos.test.ts | 39 ++++ __test__/channel_windows.test.ts | 32 ++- __test__/chrome_for_testing.test.ts | 119 ++++++++++ ...st-known-good-versions-with-downloads.json | 1 + __test__/version_installer.test.ts | 30 +-- src/channel_linux.ts | 43 +++- src/channel_macos.ts | 41 +++- src/channel_windows.ts | 47 +++- src/chrome_for_testing.ts | 210 ++++++++++++++++++ src/version_installer.ts | 122 +--------- 11 files changed, 565 insertions(+), 159 deletions(-) create mode 100644 __test__/chrome_for_testing.test.ts create mode 100644 __test__/data/last-known-good-versions-with-downloads.json create mode 100644 src/chrome_for_testing.ts diff --git a/__test__/channel_linux.test.ts b/__test__/channel_linux.test.ts index c6b7ca6..941a1d7 100644 --- a/__test__/channel_linux.test.ts +++ b/__test__/channel_linux.test.ts @@ -8,6 +8,7 @@ import { LinuxChannelInstaller } from "../src/channel_linux"; const cacheFindSpy = vi.spyOn(cache, "find"); const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const tcExtractZipSpy = vi.spyOn(tc, "extractZip"); const execSpy = vi.spyOn(exec, "exec"); const fsMkdtempSpy = vi.spyOn(fs.promises, "mkdtemp"); const fsUnlinkSpy = vi.spyOn(fs.promises, "unlink"); @@ -92,4 +93,43 @@ describe("LinuxChannelInstaller", () => { ); }); }); + + describe("downloadDriver", () => { + test("throw error if version is not release channel", async () => { + await expect(installer.downloadDriver("foo")).rejects.toThrowError( + "Invalid version: foo", + ); + }); + + test("download stable version", async () => { + tcDownloadToolSpy.mockResolvedValue("/tmp/chromedirver.zip"); + + const result = await installer.downloadDriver("stable"); + + expect(result).toEqual({ archive: "/tmp/chromedirver.zip" }); + expect(tcDownloadToolSpy).toHaveBeenCalled(); + }); + }); + + describe("installDriver", () => { + test("install stable version", async () => { + tcExtractZipSpy.mockResolvedValue("/tmp/chromedriver"); + cacheCacheDirSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.installDriver( + "stable", + "/path/to/downloaded.deb", + ); + + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + expect(cacheCacheDirSpy).toHaveBeenCalledWith( + "/tmp/chromedriver/chromedriver-linux64", + "chromedriver", + "stable", + ); + }); + }); }); diff --git a/__test__/channel_macos.test.ts b/__test__/channel_macos.test.ts index 2243256..1804aa1 100644 --- a/__test__/channel_macos.test.ts +++ b/__test__/channel_macos.test.ts @@ -8,6 +8,7 @@ import { MacOSChannelInstaller } from "../src/channel_macos"; const cacheFindSpy = vi.spyOn(cache, "find"); const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const tcExtractZipSpy = vi.spyOn(tc, "extractZip"); const fsSymlinkSpy = vi.spyOn(fs.promises, "symlink"); const execSpy = vi.spyOn(exec, "exec"); @@ -86,4 +87,42 @@ describe("MacOSChannelInstaller", () => { ); }); }); + + describe("downloadDriver", () => { + test("throw error if version is not release channel", async () => { + await expect(installer.downloadDriver("foo")).rejects.toThrowError( + "Invalid version: foo", + ); + }); + + test("download driver", async () => { + tcDownloadToolSpy.mockResolvedValue("/path/to/downloaded.zip"); + + const result = await installer.downloadDriver("stable"); + + expect(result).toEqual({ archive: "/path/to/downloaded.zip" }); + }); + }); + + describe("installDriver", () => { + test("install driver", async () => { + cacheCacheDirSpy.mockResolvedValue("/path/to/chromedriver"); + tcExtractZipSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.installDriver( + "stable", + "/path/to/chromedriver.zip", + ); + + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + expect(cacheCacheDirSpy).toHaveBeenCalledWith( + "/path/to/chromedriver/chromedriver-mac-x64", + "chromedriver", + "stable", + ); + }); + }); }); diff --git a/__test__/channel_windows.test.ts b/__test__/channel_windows.test.ts index 8445e4f..0af80b5 100644 --- a/__test__/channel_windows.test.ts +++ b/__test__/channel_windows.test.ts @@ -2,11 +2,14 @@ import * as fs from "node:fs"; import * as exec from "@actions/exec"; import * as tc from "@actions/tool-cache"; import { afterEach, describe, expect, test, vi } from "vitest"; +import * as cache from "../src/cache"; import { WindowsChannelInstaller } from "../src/channel_windows"; const fsStatSpy = vi.spyOn(fs.promises, "stat"); const fsRenameSpy = vi.spyOn(fs.promises, "rename"); const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const tcExtractZipSpy = vi.spyOn(tc, "extractZip"); +const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); const execSpy = vi.spyOn(exec, "exec"); afterEach(() => { @@ -84,15 +87,28 @@ describe("WindowsChannelInstaller", () => { }); }); - describe("unsupported platform", () => { - test("throws an error if the platform is not supported", async () => { - const installer2 = new WindowsChannelInstaller({ - os: "windows", - arch: "arm64", - }); - await expect(installer2.downloadBrowser("stable")).rejects.toThrow( - 'Chrome stable not supported for platform "windows" "arm64"', + describe("downloadDriver", () => { + test("downloads the stable chromedriver", async () => { + tcDownloadToolSpy.mockResolvedValue("C:\\path\\to\\downloaded\\file.zip"); + + const result = await installer.downloadDriver("stable"); + expect(result).toEqual({ archive: "C:\\path\\to\\downloaded\\file.zip" }); + }); + }); + + describe("installDriver", () => { + test("installs the stable chromedriver", async () => { + tcExtractZipSpy.mockResolvedValue("C:\\path\\to\\extract\\directory"); + cacheCacheDirSpy.mockResolvedValue("C:\\path\\to\\chromedriver"); + + const result = await installer.installDriver( + "stable", + "C:\\path\\to\\downloaded\\file.zip", ); + expect(result).toEqual({ + root: "C:\\path\\to\\chromedriver", + bin: "chromedriver.exe", + }); }); }); }); diff --git a/__test__/chrome_for_testing.test.ts b/__test__/chrome_for_testing.test.ts new file mode 100644 index 0000000..67fc729 --- /dev/null +++ b/__test__/chrome_for_testing.test.ts @@ -0,0 +1,119 @@ +import fs from "node:fs"; +import path from "node:path"; +import * as httpm from "@actions/http-client"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { + KnownGoodVersionResolver, + LastKnownGoodVersionResolver, +} from "../src/chrome_for_testing"; + +const getJsonSpy = vi.spyOn(httpm.HttpClient.prototype, "getJson"); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("KnownGoodVersionResolver", () => { + beforeEach(() => { + const mockDataPath = path.join( + __dirname, + "data/known-good-versions-with-downloads.json", + ); + + getJsonSpy.mockImplementation(async () => { + return { + statusCode: 200, + headers: {}, + result: JSON.parse(await fs.promises.readFile(mockDataPath, "utf-8")), + }; + }); + }); + + test.each` + spec | version | browserURL | driverURL + ${"120.0.6099.5"} | ${"120.0.6099.5"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.5/linux64/chrome-linux64.zip"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.5/linux64/chromedriver-linux64.zip"} + ${"120.0.6099.x"} | ${"120.0.6099.56"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chrome-linux64.zip"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chromedriver-linux64.zip"} + ${"1234.0.6099.x"} | ${undefined} | ${undefined} | ${undefined} + `( + "should resolve known good versions for $spec", + async ({ spec, version, browserURL, driverURL }) => { + const resolver = new KnownGoodVersionResolver({ + os: "linux", + arch: "amd64", + }); + const resolved = await resolver.resolve(spec); + expect(resolved?.version).toEqual(version); + expect(resolved?.browserDownloadURL).toEqual(browserURL); + expect(resolved?.driverDownloadURL).toEqual(driverURL); + }, + ); + + test("should cache known good versions", async () => { + const resolver = new KnownGoodVersionResolver({ + os: "linux", + arch: "amd64", + }); + await resolver.resolve("120.0.6099.5"); + await resolver.resolve("120.0.6099.18"); + expect(getJsonSpy).toHaveBeenCalledTimes(1); + }); + + test("unsupported platform", async () => { + expect(() => { + new LastKnownGoodVersionResolver({ + os: "windows", + arch: "arm64", + }); + }).toThrow("Unsupported platform: windows arm64"); + }); +}); + +describe("LastKnownGoodVersionResolver", () => { + beforeEach(() => { + const mockDataPath = path.join( + __dirname, + "data/last-known-good-versions-with-downloads.json", + ); + + getJsonSpy.mockImplementation(async () => { + return { + statusCode: 200, + headers: {}, + result: JSON.parse(await fs.promises.readFile(mockDataPath, "utf-8")), + }; + }); + }); + + test("should resolve last known good versions", async () => { + const resolver = new LastKnownGoodVersionResolver({ + os: "linux", + arch: "amd64", + }); + const resolved = await resolver.resolve("stable"); + expect(resolved?.browserDownloadURL).toEqual( + "https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/linux64/chrome-linux64.zip", + ); + expect(resolved?.driverDownloadURL).toEqual( + "https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/linux64/chromedriver-linux64.zip", + ); + }); + + test("should cache known good versions", async () => { + const resolver = new LastKnownGoodVersionResolver({ + os: "linux", + arch: "amd64", + }); + await resolver.resolve("stable"); + await resolver.resolve("beta"); + expect(getJsonSpy).toHaveBeenCalledTimes(1); + }); + + test("unsupported platform", async () => { + expect(() => { + new LastKnownGoodVersionResolver({ + os: "windows", + arch: "arm64", + }); + }).toThrow("Unsupported platform: windows arm64"); + }); +}); diff --git a/__test__/data/last-known-good-versions-with-downloads.json b/__test__/data/last-known-good-versions-with-downloads.json new file mode 100644 index 0000000..8e5fc20 --- /dev/null +++ b/__test__/data/last-known-good-versions-with-downloads.json @@ -0,0 +1 @@ +{"timestamp":"2024-05-25T20:09:13.017Z","channels":{"Stable":{"channel":"Stable","version":"125.0.6422.78","revision":"1287751","downloads":{"chrome":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/linux64/chrome-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/mac-arm64/chrome-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/mac-x64/chrome-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/win32/chrome-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/win64/chrome-win64.zip"}],"chromedriver":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/linux64/chromedriver-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/mac-arm64/chromedriver-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/mac-x64/chromedriver-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/win32/chromedriver-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/win64/chromedriver-win64.zip"}],"chrome-headless-shell":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/linux64/chrome-headless-shell-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/mac-arm64/chrome-headless-shell-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/mac-x64/chrome-headless-shell-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/win32/chrome-headless-shell-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/125.0.6422.78/win64/chrome-headless-shell-win64.zip"}]}},"Beta":{"channel":"Beta","version":"126.0.6478.17","revision":"1300313","downloads":{"chrome":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/linux64/chrome-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/mac-arm64/chrome-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/mac-x64/chrome-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/win32/chrome-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/win64/chrome-win64.zip"}],"chromedriver":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/linux64/chromedriver-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/mac-arm64/chromedriver-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/mac-x64/chromedriver-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/win32/chromedriver-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/win64/chromedriver-win64.zip"}],"chrome-headless-shell":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/linux64/chrome-headless-shell-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/mac-arm64/chrome-headless-shell-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/mac-x64/chrome-headless-shell-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/win32/chrome-headless-shell-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/126.0.6478.17/win64/chrome-headless-shell-win64.zip"}]}},"Dev":{"channel":"Dev","version":"127.0.6485.0","revision":"1302521","downloads":{"chrome":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/linux64/chrome-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/mac-arm64/chrome-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/mac-x64/chrome-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/win32/chrome-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/win64/chrome-win64.zip"}],"chromedriver":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/linux64/chromedriver-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/mac-arm64/chromedriver-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/mac-x64/chromedriver-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/win32/chromedriver-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/win64/chromedriver-win64.zip"}],"chrome-headless-shell":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/linux64/chrome-headless-shell-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/mac-arm64/chrome-headless-shell-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/mac-x64/chrome-headless-shell-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/win32/chrome-headless-shell-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6485.0/win64/chrome-headless-shell-win64.zip"}]}},"Canary":{"channel":"Canary","version":"127.0.6501.0","revision":"1306090","downloads":{"chrome":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/linux64/chrome-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/mac-arm64/chrome-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/mac-x64/chrome-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/win32/chrome-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/win64/chrome-win64.zip"}],"chromedriver":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/linux64/chromedriver-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/mac-arm64/chromedriver-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/mac-x64/chromedriver-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/win32/chromedriver-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/win64/chromedriver-win64.zip"}],"chrome-headless-shell":[{"platform":"linux64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/linux64/chrome-headless-shell-linux64.zip"},{"platform":"mac-arm64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/mac-arm64/chrome-headless-shell-mac-arm64.zip"},{"platform":"mac-x64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/mac-x64/chrome-headless-shell-mac-x64.zip"},{"platform":"win32","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/win32/chrome-headless-shell-win32.zip"},{"platform":"win64","url":"https://storage.googleapis.com/chrome-for-testing-public/127.0.6501.0/win64/chrome-headless-shell-win64.zip"}]}}}} diff --git a/__test__/version_installer.test.ts b/__test__/version_installer.test.ts index beb9d71..dc66a47 100644 --- a/__test__/version_installer.test.ts +++ b/__test__/version_installer.test.ts @@ -4,10 +4,7 @@ import * as httpm from "@actions/http-client"; import * as tc from "@actions/tool-cache"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import * as cache from "../src/cache"; -import { - KnownGoodVersionInstaller, - KnownGoodVersionResolver, -} from "../src/version_installer"; +import { KnownGoodVersionInstaller } from "../src/version_installer"; const getJsonSpy = vi.spyOn(httpm.HttpClient.prototype, "getJson"); const tcExtractZipSpy = vi.spyOn(tc, "extractZip"); @@ -34,31 +31,6 @@ afterEach(() => { vi.resetAllMocks(); }); -describe("VersionResolver", () => { - test.each` - spec | version | browserURL | driverURL - ${"120.0.6099.5"} | ${"120.0.6099.5"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.5/linux64/chrome-linux64.zip"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.5/linux64/chromedriver-linux64.zip"} - ${"120.0.6099.x"} | ${"120.0.6099.56"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chrome-linux64.zip"} | ${"https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/linux64/chromedriver-linux64.zip"} - ${"1234.0.6099.x"} | ${undefined} | ${undefined} | ${undefined} - `( - "should resolve known good versions for $spec", - async ({ spec, version, browserURL, driverURL }) => { - const resolver = new KnownGoodVersionResolver("linux64"); - const resolved = await resolver.resolve(spec); - expect(resolved?.version).toEqual(version); - expect(resolved?.browserDownloadURL).toEqual(browserURL); - expect(resolved?.driverDownloadURL).toEqual(driverURL); - }, - ); - - test("should cache known good versions", async () => { - const resolver = new KnownGoodVersionResolver("linux64"); - await resolver.resolve("120.0.6099.5"); - await resolver.resolve("120.0.6099.18"); - expect(getJsonSpy).toHaveBeenCalledTimes(1); - }); -}); - describe("KnownGoodVersionInstaller", () => { const installer = new KnownGoodVersionInstaller({ os: "linux", diff --git a/src/channel_linux.ts b/src/channel_linux.ts index bd0af9a..b650c7b 100644 --- a/src/channel_linux.ts +++ b/src/channel_linux.ts @@ -5,12 +5,23 @@ import * as core from "@actions/core"; import * as exec from "@actions/exec"; import * as tc from "@actions/tool-cache"; import * as cache from "./cache"; +import { LastKnownGoodVersionResolver } from "./chrome_for_testing"; import type { DownloadResult, InstallResult, Installer } from "./installer"; import type { Platform } from "./platform"; import { isReleaseChannelName } from "./version"; export class LinuxChannelInstaller implements Installer { - constructor(private readonly platform: Platform) {} + private readonly platform: Platform; + private readonly versionResolver: LastKnownGoodVersionResolver; + + constructor(platform: Platform) { + if (platform.os !== "linux") { + throw new Error(`Unexpected OS: ${platform.os}`); + } + + this.platform = platform; + this.versionResolver = new LastKnownGoodVersionResolver(platform); + } async checkInstalled(version: string): Promise { const root = await cache.find("chromium", version); @@ -77,4 +88,34 @@ export class LinuxChannelInstaller implements Installer { return { root, bin: "chrome" }; } + + async downloadDriver(version: string): Promise { + const resolved = await this.versionResolver.resolve(version); + if (!resolved) { + throw new Error( + `Version ${version} not found in the known good versions`, + ); + } + + core.info( + `Acquiring ${resolved.version} from ${resolved.driverDownloadURL}`, + ); + const archive = await tc.downloadTool(resolved.driverDownloadURL); + return { archive }; + } + + async installDriver( + version: string, + archive: string, + ): Promise { + const extPath = await tc.extractZip(archive); + const extAppRoot = path.join( + extPath, + `chromedriver-${this.versionResolver.platformString}`, + ); + + const root = await cache.cacheDir(extAppRoot, "chromedriver", version); + core.info(`Successfully Installed chromedriver to ${root}`); + return { root, bin: "chromedriver" }; + } } diff --git a/src/channel_macos.ts b/src/channel_macos.ts index f18c35b..8c0445a 100644 --- a/src/channel_macos.ts +++ b/src/channel_macos.ts @@ -4,12 +4,21 @@ import * as core from "@actions/core"; import * as exec from "@actions/exec"; import * as tc from "@actions/tool-cache"; import * as cache from "./cache"; +import { LastKnownGoodVersionResolver } from "./chrome_for_testing"; import type { DownloadResult, InstallResult, Installer } from "./installer"; import type { Platform } from "./platform"; import { isReleaseChannelName } from "./version"; export class MacOSChannelInstaller implements Installer { - constructor(private readonly platform: Platform) {} + private readonly versionResolver: LastKnownGoodVersionResolver; + + constructor(platform: Platform) { + if (platform.os !== "darwin") { + throw new Error(`Unexpected OS: ${platform.os}`); + } + + this.versionResolver = new LastKnownGoodVersionResolver(platform); + } async checkInstalled(version: string): Promise { if (!isReleaseChannelName(version)) { @@ -90,4 +99,34 @@ export class MacOSChannelInstaller implements Installer { return { root, bin: bin2 }; } + + async downloadDriver(version: string): Promise { + const resolved = await this.versionResolver.resolve(version); + if (!resolved) { + throw new Error( + `Version ${version} not found in the known good versions`, + ); + } + + core.info( + `Acquiring ${resolved.version} from ${resolved.driverDownloadURL}`, + ); + const archive = await tc.downloadTool(resolved.driverDownloadURL); + return { archive }; + } + + async installDriver( + version: string, + archive: string, + ): Promise { + const extPath = await tc.extractZip(archive); + const extAppRoot = path.join( + extPath, + `chromedriver-${this.versionResolver.platformString}`, + ); + + const root = await cache.cacheDir(extAppRoot, "chromedriver", version); + core.info(`Successfully Installed chromedriver to ${root}`); + return { root, bin: "chromedriver" }; + } } diff --git a/src/channel_windows.ts b/src/channel_windows.ts index 58470d4..4a28eb7 100644 --- a/src/channel_windows.ts +++ b/src/channel_windows.ts @@ -1,7 +1,10 @@ -import fs from "node:fs"; +import * as fs from "node:fs"; +import * as path from "node:path"; import * as core from "@actions/core"; import * as exec from "@actions/exec"; import * as tc from "@actions/tool-cache"; +import * as cache from "./cache"; +import { LastKnownGoodVersionResolver } from "./chrome_for_testing"; import type { DownloadResult, InstallResult, Installer } from "./installer"; import { Arch, type Platform } from "./platform"; import { type ReleaseChannelName, isReleaseChannelName } from "./version"; @@ -13,7 +16,17 @@ const isENOENT = (e: unknown): boolean => { }; export class WindowsChannelInstaller implements Installer { - constructor(private readonly platform: Platform) {} + private readonly platform: Platform; + private readonly versionResolver: LastKnownGoodVersionResolver; + + constructor(platform: Platform) { + if (platform.os !== "windows") { + throw new Error(`Unexpected OS: ${platform.os}`); + } + + this.platform = platform; + this.versionResolver = new LastKnownGoodVersionResolver(platform); + } async checkInstalled(version: string): Promise { if (!isReleaseChannelName(version)) { @@ -125,4 +138,34 @@ export class WindowsChannelInstaller implements Installer { return "C:\\Program Files\\Google\\Chrome SxS\\Application"; } } + + async downloadDriver(version: string): Promise { + const resolved = await this.versionResolver.resolve(version); + if (!resolved) { + throw new Error( + `Version ${version} not found in the known good versions`, + ); + } + + core.info( + `Acquiring ${resolved.version} from ${resolved.driverDownloadURL}`, + ); + const archive = await tc.downloadTool(resolved.driverDownloadURL); + return { archive }; + } + + async installDriver( + version: string, + archive: string, + ): Promise { + const extPath = await tc.extractZip(archive); + const extAppRoot = path.join( + extPath, + `chromedriver-${this.versionResolver.platformString}`, + ); + + const root = await cache.cacheDir(extAppRoot, "chromedriver", version); + core.info(`Successfully Installed chromedriver to ${root}`); + return { root, bin: "chromedriver.exe" }; + } } diff --git a/src/chrome_for_testing.ts b/src/chrome_for_testing.ts new file mode 100644 index 0000000..b9070fb --- /dev/null +++ b/src/chrome_for_testing.ts @@ -0,0 +1,210 @@ +import * as httpm from "@actions/http-client"; +import { Arch, OS, type Platform } from "./platform"; +import { parse } from "./version"; + +const KNOWN_GOOD_VERSIONS_URL = + "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"; + +const LAST_KNOWN_GOOD_VERSION = + "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"; + +export type PlatformString = + | "linux64" + | "mac-arm64" + | "mac-x64" + | "win32" + | "win64"; + +export type LastKnownGoodVersionsJson = { + timestamp: string; + channels: { + Stable: LastKnownGoodVersion; + Beta: LastKnownGoodVersion; + Dev: LastKnownGoodVersion; + Canary: LastKnownGoodVersion; + }; +}; + +type LastKnownGoodVersion = { + channel: "Stable" | "Beta" | "Dev" | "Canary"; + version: string; + revision: string; + downloads: { + chrome?: Array<{ + platform: PlatformString; + url: string; + }>; + chromedriver?: Array<{ + platform: PlatformString; + url: string; + }>; + }; +}; + +export type KnownGoodVersionsJson = { + timestamp: string; + versions: KnownGoodVersion[]; +}; + +export type KnownGoodVersion = { + version: string; + revision: string; + downloads: { + chrome?: Array<{ + platform: PlatformString; + url: string; + }>; + chromedriver?: Array<{ + platform: PlatformString; + url: string; + }>; + }; +}; + +const platformString = (platform: Platform): PlatformString => { + if (platform.os === OS.LINUX && platform.arch === Arch.AMD64) { + return "linux64"; + } else if (platform.os === OS.DARWIN && platform.arch === Arch.AMD64) { + return "mac-x64"; + } else if (platform.os === OS.DARWIN && platform.arch === Arch.ARM64) { + return "mac-arm64"; + } else if (platform.os === OS.WINDOWS && platform.arch === Arch.AMD64) { + return "win64"; + } else if (platform.os === OS.WINDOWS && platform.arch === Arch.I686) { + return "win32"; + } + throw new Error(`Unsupported platform: ${platform.os} ${platform.arch}`); +}; + +type ResolvedVersion = { + version: string; + browserDownloadURL: string; + driverDownloadURL: string; +}; + +export class KnownGoodVersionResolver { + private readonly http = new httpm.HttpClient("setup-chrome"); + + private cache?: KnownGoodVersion[]; + + public readonly platformString: PlatformString; + + constructor(platform: Platform) { + this.platformString = platformString(platform); + } + + async resolve(version: string): Promise { + const spec = parse(version); + + const knownGoodVersions = await this.getKnownGoodVersions(); + for (const version of knownGoodVersions) { + if (!spec.satisfies(version.version)) { + continue; + } + const browser = version.downloads.chrome?.find( + ({ platform }) => platform === this.platformString, + ); + const driver = version.downloads.chromedriver?.find( + ({ platform }) => platform === this.platformString, + ); + + if (browser && driver) { + return { + version: version.version, + browserDownloadURL: browser.url, + driverDownloadURL: driver.url, + }; + } + } + } + + private async getKnownGoodVersions(): Promise { + if (this.cache) { + return this.cache; + } + + const resp = await this.http.getJson( + KNOWN_GOOD_VERSIONS_URL, + ); + if (resp.statusCode !== httpm.HttpCodes.OK) { + throw new Error(`Failed to get known good versions: ${resp.statusCode}`); + } + if (resp.result === null) { + throw new Error("Failed to get known good versions"); + } + + this.cache = resp.result.versions.reverse(); + + return resp.result.versions; + } +} + +export class LastKnownGoodVersionResolver { + private readonly http = new httpm.HttpClient("setup-chrome"); + + private cache?: LastKnownGoodVersionsJson; + + public readonly platformString: PlatformString; + + constructor(platform: Platform) { + this.platformString = platformString(platform); + } + + async resolve(version: string): Promise { + const spec = parse(version); + if (spec.value.type !== "channel") { + throw new Error(`Unexpected version: ${version}`); + } + + const lastKnownGoodVersions = await this.getLastKnownGoodVersions(); + const downloads = (() => { + switch (spec.value.channel) { + case "stable": + return lastKnownGoodVersions.channels.Stable.downloads; + case "beta": + return lastKnownGoodVersions.channels.Beta.downloads; + case "dev": + return lastKnownGoodVersions.channels.Dev.downloads; + case "canary": + return lastKnownGoodVersions.channels.Canary.downloads; + } + })(); + + const browser = downloads.chrome?.find( + ({ platform }) => platform === this.platformString, + ); + const driver = downloads.chromedriver?.find( + ({ platform }) => platform === this.platformString, + ); + + if (browser && driver) { + return { + version: spec.value.channel, + browserDownloadURL: browser.url, + driverDownloadURL: driver.url, + }; + } + } + + private async getLastKnownGoodVersions(): Promise { + if (this.cache) { + return this.cache; + } + + const resp = await this.http.getJson( + LAST_KNOWN_GOOD_VERSION, + ); + if (resp.statusCode !== httpm.HttpCodes.OK) { + throw new Error( + `Failed to get last known good versions: ${resp.statusCode}`, + ); + } + if (resp.result === null) { + throw new Error("Failed to get last known good versions"); + } + + this.cache = resp.result; + + return resp.result; + } +} diff --git a/src/version_installer.ts b/src/version_installer.ts index 5e1ff7c..8dc1652 100644 --- a/src/version_installer.ts +++ b/src/version_installer.ts @@ -1,133 +1,19 @@ import path from "node:path"; import * as core from "@actions/core"; -import * as httpm from "@actions/http-client"; import * as tc from "@actions/tool-cache"; import * as cache from "./cache"; +import { KnownGoodVersionResolver } from "./chrome_for_testing"; import type { DownloadResult, InstallResult, Installer } from "./installer"; import { Arch, OS, type Platform } from "./platform"; -import { parse } from "./version"; - -const KNOWN_GOOD_VERSIONS_URL = - "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"; - -export type KnownGoodVersionsJson = { - timestamp: string; - versions: KnownGoodVersion[]; -}; - -export type KnownGoodVersionPlatform = - | "linux64" - | "mac-arm64" - | "mac-x64" - | "win32" - | "win64"; - -export type KnownGoodVersion = { - version: string; - revision: string; - downloads: { - chrome?: Array<{ - platform: KnownGoodVersionPlatform; - url: string; - }>; - chromedriver?: Array<{ - platform: KnownGoodVersionPlatform; - url: string; - }>; - }; -}; - -type ResolvedVersion = { - version: string; - browserDownloadURL: string; - driverDownloadURL: string; -}; - -export class KnownGoodVersionResolver { - private readonly http = new httpm.HttpClient("setup-chrome"); - - private readonly platform: KnownGoodVersionPlatform; - - private knownGoodVersionsCache?: KnownGoodVersion[]; - - constructor(platform: KnownGoodVersionPlatform) { - this.platform = platform; - } - - async resolve(version: string): Promise { - const spec = parse(version); - - const knownGoodVersions = await this.getKnownGoodVersions(); - for (const version of knownGoodVersions) { - if ( - !spec.satisfies(version.version) || - !version.downloads.chrome || - !version.downloads.chromedriver - ) { - continue; - } - const browser = version.downloads.chrome.find( - ({ platform }) => platform === this.platform, - ); - const driver = version.downloads.chromedriver.find( - ({ platform }) => platform === this.platform, - ); - - if (browser && driver) { - return { - version: version.version, - browserDownloadURL: browser.url, - driverDownloadURL: driver.url, - }; - } - } - } - - private async getKnownGoodVersions(): Promise { - if (this.knownGoodVersionsCache) { - return this.knownGoodVersionsCache; - } - - const resp = await this.http.getJson( - KNOWN_GOOD_VERSIONS_URL, - ); - if (resp.statusCode !== httpm.HttpCodes.OK) { - throw new Error(`Failed to get known good versions: ${resp.statusCode}`); - } - if (resp.result === null) { - throw new Error("Failed to get known good versions"); - } - - this.knownGoodVersionsCache = resp.result.versions.reverse(); - - return resp.result.versions; - } -} export class KnownGoodVersionInstaller implements Installer { private readonly versionResolver: KnownGoodVersionResolver; private readonly platform: Platform; - private readonly knownGoodVersionPlatform: KnownGoodVersionPlatform; constructor(platform: Platform) { this.platform = platform; - if (platform.os === OS.LINUX && platform.arch === Arch.AMD64) { - this.knownGoodVersionPlatform = "linux64"; - } else if (platform.os === OS.DARWIN && platform.arch === Arch.AMD64) { - this.knownGoodVersionPlatform = "mac-x64"; - } else if (platform.os === OS.DARWIN && platform.arch === Arch.ARM64) { - this.knownGoodVersionPlatform = "mac-arm64"; - } else if (platform.os === OS.WINDOWS && platform.arch === Arch.AMD64) { - this.knownGoodVersionPlatform = "win64"; - } else if (platform.os === OS.WINDOWS && platform.arch === Arch.I686) { - this.knownGoodVersionPlatform = "win32"; - } else { - throw new Error(`Unsupported platform: ${platform.os} ${platform.arch}`); - } - this.versionResolver = new KnownGoodVersionResolver( - this.knownGoodVersionPlatform, - ); + this.versionResolver = new KnownGoodVersionResolver(this.platform); } async checkInstalled(version: string): Promise { @@ -161,7 +47,7 @@ export class KnownGoodVersionInstaller implements Installer { const extPath = await tc.extractZip(archive); const extAppRoot = path.join( extPath, - `chrome-${this.knownGoodVersionPlatform}`, + `chrome-${this.versionResolver.platformString}`, ); const root = await cache.cacheDir(extAppRoot, "chromium", resolved.version); @@ -203,7 +89,7 @@ export class KnownGoodVersionInstaller implements Installer { const extPath = await tc.extractZip(archive); const extAppRoot = path.join( extPath, - `chromedriver-${this.knownGoodVersionPlatform}`, + `chromedriver-${this.versionResolver.platformString}`, ); const root = await cache.cacheDir( From 3ffc657044a4ef01f764953ee49ac061b93f7144 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 06:44:12 +0000 Subject: [PATCH 09/20] resolve snapshot version for latest and use it for cache key --- __test__/latest_installer.test.ts | 104 ++++++++++++++++++++++++++++++ src/latest_installer.ts | 46 ++++++++++--- 2 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 __test__/latest_installer.test.ts diff --git a/__test__/latest_installer.test.ts b/__test__/latest_installer.test.ts new file mode 100644 index 0000000..c1fe9e0 --- /dev/null +++ b/__test__/latest_installer.test.ts @@ -0,0 +1,104 @@ +import * as tc from "@actions/tool-cache"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import * as cache from "../src/cache"; +import { LatestInstaller } from "../src/latest_installer"; + +const cacheFindSpy = vi.spyOn(cache, "find"); +const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); +const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); +const tcExtractZipSpy = vi.spyOn(tc, "extractZip"); +vi.mock("../src/snapshot_bucket", () => ({ + resolveLatestVersion: () => Promise.resolve("123456"), + browserDownloadURL: () => "https://example.com/chrome.zip", + driverDownloadURL: () => "https://example.com/chromedriver.zip", +})); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("LatestInstaller", () => { + const installer = new LatestInstaller({ os: "linux", arch: "amd64" }); + + describe("checkInstalled", () => { + test("returns undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalled("latest"); + expect(result).toBe(undefined); + expect(cacheFindSpy).toHaveBeenCalledWith("chromium", "123456"); + }); + + test("returns install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/chromium"); + + const result = await installer.checkInstalled("latest"); + expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); + }); + }); + + describe("downloadBrowser", () => { + test("downloads the browser", async () => { + tcDownloadToolSpy.mockResolvedValue("/tmp/chrome.zip"); + + const result = await installer.downloadBrowser("latest"); + expect(result).toEqual({ archive: "/tmp/chrome.zip" }); + expect(tcDownloadToolSpy).toHaveBeenCalledWith( + "https://example.com/chrome.zip", + ); + }); + }); + + describe("installBrowser", () => { + test("installs the browser", async () => { + tcExtractZipSpy.mockResolvedValue("/path/to/ext"); + cacheCacheDirSpy.mockResolvedValue("/path/to/chromium"); + + const result = await installer.installBrowser( + "latest", + "/tmp/chrome.zip", + ); + expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); + expect(tcExtractZipSpy).toHaveBeenCalled(); + expect(cacheCacheDirSpy).toHaveBeenCalledWith( + "/path/to/ext/chrome-linux", + "chromium", + "123456", + ); + }); + }); + + describe("downloadDriver", () => { + test("downloads the driver", async () => { + tcDownloadToolSpy.mockResolvedValue("/tmp/chromedriver.zip"); + + const result = await installer.downloadDriver("latest"); + expect(result).toEqual({ archive: "/tmp/chromedriver.zip" }); + expect(tcDownloadToolSpy).toHaveBeenCalledWith( + "https://example.com/chromedriver.zip", + ); + }); + }); + + describe("installDriver", () => { + test("installs the driver", async () => { + tcExtractZipSpy.mockResolvedValue("/path/to/ext"); + cacheCacheDirSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.installDriver( + "latest", + "/tmp/chromedriver.zip", + ); + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + expect(tcExtractZipSpy).toHaveBeenCalled(); + expect(cacheCacheDirSpy).toHaveBeenCalledWith( + "/path/to/ext/chromedriver_linux64", + "chromedriver", + "123456", + ); + }); + }); +}); diff --git a/src/latest_installer.ts b/src/latest_installer.ts index 9b64760..6f26743 100644 --- a/src/latest_installer.ts +++ b/src/latest_installer.ts @@ -7,21 +7,47 @@ import { SnapshotInstaller } from "./snapshot_installer"; export class LatestInstaller implements Installer { private readonly snapshotInstaller = new SnapshotInstaller(this.platform); + private latestSnapshotCache: string | undefined; + constructor(private readonly platform: Platform) {} - async checkInstalled(version: string): Promise { - const root = await cache.find("chromium", version); - if (root) { - return { root, bin: "chrome" }; - } + async checkInstalled(_version: string): Promise { + const snapshot = await this.getLatestSnapshot(); + return this.snapshotInstaller.checkInstalled(snapshot); } async downloadBrowser(_version: string): Promise { - const version = await resolveLatestVersion(this.platform); - return this.snapshotInstaller.downloadBrowser(version); + const snapshot = await this.getLatestSnapshot(); + return this.snapshotInstaller.downloadBrowser(snapshot); + } + + async installBrowser( + _version: string, + archive: string, + ): Promise { + const snapshot = await this.getLatestSnapshot(); + return this.snapshotInstaller.installBrowser(snapshot, archive); + } + + async downloadDriver(_version: string): Promise { + const version = await this.getLatestSnapshot(); + return this.snapshotInstaller.downloadDriver(version); } - installBrowser = this.snapshotInstaller.installBrowser; - downloadDriver = this.snapshotInstaller.downloadDriver; - installDriver = this.snapshotInstaller.installDriver; + async installDriver( + _version: string, + archive: string, + ): Promise { + const snapshot = await this.getLatestSnapshot(); + return this.snapshotInstaller.installDriver(snapshot, archive); + } + + private async getLatestSnapshot(): Promise { + if (this.latestSnapshotCache) { + return Promise.resolve(this.latestSnapshotCache); + } + + this.latestSnapshotCache = await resolveLatestVersion(this.platform); + return this.latestSnapshotCache; + } } From f877f5192a97cae6a31eae8bde61fc5ac00f57c3 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 06:46:15 +0000 Subject: [PATCH 10/20] implement checkInstalledDriver --- __test__/channel_linux.test.ts | 25 ++++++++++++++++++++++--- __test__/channel_macos.test.ts | 25 ++++++++++++++++++++++--- __test__/channel_windows.test.ts | 26 +++++++++++++++++++++++--- __test__/latest_installer.test.ts | 24 ++++++++++++++++++++++-- __test__/snapshot_installer.test.ts | 25 ++++++++++++++++++++++--- __test__/version_installer.test.ts | 23 +++++++++++++++++++++-- src/channel_linux.ts | 13 ++++++++++++- src/channel_macos.ts | 13 ++++++++++++- src/channel_windows.ts | 19 +++++++++++++++---- src/installer.ts | 8 +++----- src/latest_installer.ts | 13 +++++++++++-- src/snapshot_installer.ts | 13 ++++++++++++- src/version_installer.ts | 13 ++++++++++++- 13 files changed, 209 insertions(+), 31 deletions(-) diff --git a/__test__/channel_linux.test.ts b/__test__/channel_linux.test.ts index 941a1d7..c725b3b 100644 --- a/__test__/channel_linux.test.ts +++ b/__test__/channel_linux.test.ts @@ -20,11 +20,11 @@ afterEach(() => { describe("LinuxChannelInstaller", () => { const installer = new LinuxChannelInstaller({ os: "linux", arch: "amd64" }); - describe("checkInstalled", () => { + describe("checkInstalledBrowser", () => { test("return undefined if not installed", async () => { cacheFindSpy.mockResolvedValue(undefined); - const result = await installer.checkInstalled("stable"); + const result = await installer.checkInstalledBrowser("stable"); expect(result).toBeUndefined(); }); @@ -32,7 +32,7 @@ describe("LinuxChannelInstaller", () => { test("return install result if installed", async () => { cacheFindSpy.mockResolvedValue("/path/to/chromium"); - const result = await installer.checkInstalled("stable"); + const result = await installer.checkInstalledBrowser("stable"); expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); }); @@ -94,6 +94,25 @@ describe("LinuxChannelInstaller", () => { }); }); + describe("checkInstalledDriver", () => { + test("return undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalledDriver("stable"); + expect(result).toBeUndefined(); + }); + + test("return install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.checkInstalledDriver("stable"); + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + }); + }); + describe("downloadDriver", () => { test("throw error if version is not release channel", async () => { await expect(installer.downloadDriver("foo")).rejects.toThrowError( diff --git a/__test__/channel_macos.test.ts b/__test__/channel_macos.test.ts index 1804aa1..9c88cf9 100644 --- a/__test__/channel_macos.test.ts +++ b/__test__/channel_macos.test.ts @@ -22,11 +22,11 @@ describe("MacOSChannelInstaller", () => { arch: "amd64", }); - describe("checkInstalled", () => { + describe("checkInstalledBrowser", () => { test("return undefined if not installed", async () => { cacheFindSpy.mockResolvedValue(undefined); - const result = await installer.checkInstalled("stable"); + const result = await installer.checkInstalledBrowser("stable"); expect(result).toBeUndefined(); }); @@ -34,7 +34,7 @@ describe("MacOSChannelInstaller", () => { test("return install result if installed", async () => { cacheFindSpy.mockResolvedValue("/path/to/Chromium.app"); - const result = await installer.checkInstalled("stable"); + const result = await installer.checkInstalledBrowser("stable"); expect(result).toEqual({ root: "/path/to/Chromium.app", @@ -88,6 +88,25 @@ describe("MacOSChannelInstaller", () => { }); }); + describe("checkInstalledDriver", () => { + test("return undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalledDriver("stable"); + expect(result).toBeUndefined(); + }); + + test("return install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.checkInstalledDriver("stable"); + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + }); + }); + describe("downloadDriver", () => { test("throw error if version is not release channel", async () => { await expect(installer.downloadDriver("foo")).rejects.toThrowError( diff --git a/__test__/channel_windows.test.ts b/__test__/channel_windows.test.ts index 0af80b5..1659882 100644 --- a/__test__/channel_windows.test.ts +++ b/__test__/channel_windows.test.ts @@ -9,6 +9,7 @@ const fsStatSpy = vi.spyOn(fs.promises, "stat"); const fsRenameSpy = vi.spyOn(fs.promises, "rename"); const tcDownloadToolSpy = vi.spyOn(tc, "downloadTool"); const tcExtractZipSpy = vi.spyOn(tc, "extractZip"); +const cacheFindSpy = vi.spyOn(cache, "find"); const cacheCacheDirSpy = vi.spyOn(cache, "cacheDir"); const execSpy = vi.spyOn(exec, "exec"); @@ -22,16 +23,16 @@ describe("WindowsChannelInstaller", () => { arch: "amd64", }); - describe("checkInstalled", () => { + describe("checkInstalledBrowser", () => { test("returns undefined if the root directory does not exist", async () => { - const result = await installer.checkInstalled("stable"); + const result = await installer.checkInstalledBrowser("stable"); expect(result).toBe(undefined); }); test("returns the root directory and bin path if the root directory exists", async () => { fsStatSpy.mockResolvedValue(undefined); - const result = await installer.checkInstalled("stable"); + const result = await installer.checkInstalledBrowser("stable"); expect(result).toEqual({ root: "C:\\Program Files\\Google\\Chrome\\Application", bin: "chrome.exe", @@ -87,6 +88,25 @@ describe("WindowsChannelInstaller", () => { }); }); + describe("checkInstalledDriver", () => { + test("return undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalledDriver("stable"); + expect(result).toBeUndefined(); + }); + + test("return install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.checkInstalledDriver("stable"); + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + }); + }); + describe("downloadDriver", () => { test("downloads the stable chromedriver", async () => { tcDownloadToolSpy.mockResolvedValue("C:\\path\\to\\downloaded\\file.zip"); diff --git a/__test__/latest_installer.test.ts b/__test__/latest_installer.test.ts index c1fe9e0..de2f586 100644 --- a/__test__/latest_installer.test.ts +++ b/__test__/latest_installer.test.ts @@ -24,7 +24,7 @@ describe("LatestInstaller", () => { test("returns undefined if not installed", async () => { cacheFindSpy.mockResolvedValue(undefined); - const result = await installer.checkInstalled("latest"); + const result = await installer.checkInstalledBrowser("latest"); expect(result).toBe(undefined); expect(cacheFindSpy).toHaveBeenCalledWith("chromium", "123456"); }); @@ -32,7 +32,7 @@ describe("LatestInstaller", () => { test("returns install result if installed", async () => { cacheFindSpy.mockResolvedValue("/path/to/chromium"); - const result = await installer.checkInstalled("latest"); + const result = await installer.checkInstalledBrowser("latest"); expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); }); }); @@ -68,6 +68,26 @@ describe("LatestInstaller", () => { }); }); + describe("checkInstalledDriver", () => { + test("returns undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalledDriver("latest"); + expect(result).toBe(undefined); + expect(cacheFindSpy).toHaveBeenCalledWith("chromedriver", "123456"); + }); + + test("returns install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.checkInstalledDriver("latest"); + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + }); + }); + describe("downloadDriver", () => { test("downloads the driver", async () => { tcDownloadToolSpy.mockResolvedValue("/tmp/chromedriver.zip"); diff --git a/__test__/snapshot_installer.test.ts b/__test__/snapshot_installer.test.ts index 6fa7289..b2bee99 100644 --- a/__test__/snapshot_installer.test.ts +++ b/__test__/snapshot_installer.test.ts @@ -15,18 +15,18 @@ afterEach(() => { describe("SnapshotInstaller", () => { const installer = new SnapshotInstaller({ os: "linux", arch: "amd64" }); - describe("checkInstalled", () => { + describe("checkInstalledBrowser", () => { test("returns undefined if not installed", async () => { cacheFindSpy.mockResolvedValue(undefined); - const result = await installer.checkInstalled("123"); + const result = await installer.checkInstalledBrowser("123456"); expect(result).toBe(undefined); }); test("returns install result if installed", async () => { cacheFindSpy.mockResolvedValue("/path/to/chromium"); - const result = await installer.checkInstalled("123"); + const result = await installer.checkInstalledBrowser("123456"); expect(result).toEqual({ root: "/path/to/chromium", bin: "chrome" }); }); }); @@ -56,6 +56,25 @@ describe("SnapshotInstaller", () => { }); }); + describe("checkInstalledDriver", () => { + test("returns undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const result = await installer.checkInstalledDriver("123456"); + expect(result).toBe(undefined); + }); + + test("returns install result if installed", async () => { + cacheFindSpy.mockResolvedValue("/path/to/chromedriver"); + + const result = await installer.checkInstalledDriver("123456"); + expect(result).toEqual({ + root: "/path/to/chromedriver", + bin: "chromedriver", + }); + }); + }); + describe("downloadDriver", () => { test("downloads the driver", async () => { tcDownloadToolSpy.mockResolvedValue("/tmp/chromedriver.zip"); diff --git a/__test__/version_installer.test.ts b/__test__/version_installer.test.ts index dc66a47..0e2b950 100644 --- a/__test__/version_installer.test.ts +++ b/__test__/version_installer.test.ts @@ -37,12 +37,12 @@ describe("KnownGoodVersionInstaller", () => { arch: "amd64", }); - test("checkInstalled should return installed path if installed", async () => { + test("checkInstalledBrowser should return installed path if installed", async () => { cacheFindSpy.mockResolvedValue( "/opt/hostedtoolcache/setup-chrome/chromium/120.0.6099.56/x64", ); - const installed = await installer.checkInstalled("120.0.6099.x"); + const installed = await installer.checkInstalledBrowser("120.0.6099.x"); expect(installed?.root).toEqual( "/opt/hostedtoolcache/setup-chrome/chromium/120.0.6099.56/x64", ); @@ -73,6 +73,25 @@ describe("KnownGoodVersionInstaller", () => { ); }); + test("checkInstalledDriver should return undefined if not installed", async () => { + cacheFindSpy.mockResolvedValue(undefined); + + const installed = await installer.checkInstalledDriver("120.0.6099.x"); + expect(installed).toBeUndefined(); + }); + + test("checkInstalledDriver should return installed path if installed", async () => { + cacheFindSpy.mockResolvedValue( + "/opt/hostedtoolcache/setup-chrome/chromedriver/120.0.6099.56/x64", + ); + + const installed = await installer.checkInstalledDriver("120.0.6099.x"); + expect(installed?.root).toEqual( + "/opt/hostedtoolcache/setup-chrome/chromedriver/120.0.6099.56/x64", + ); + expect(cacheFindSpy).toHaveBeenCalledWith("chromedriver", "120.0.6099.x"); + }); + test("downloadDriver should download driver archive", async () => { tcDownloadToolSpy.mockImplementation(async () => "/tmp/chromedriver.zip"); diff --git a/src/channel_linux.ts b/src/channel_linux.ts index b650c7b..92d2987 100644 --- a/src/channel_linux.ts +++ b/src/channel_linux.ts @@ -23,7 +23,9 @@ export class LinuxChannelInstaller implements Installer { this.versionResolver = new LastKnownGoodVersionResolver(platform); } - async checkInstalled(version: string): Promise { + async checkInstalledBrowser( + version: string, + ): Promise { const root = await cache.find("chromium", version); if (root) { return { root, bin: "chrome" }; @@ -89,6 +91,15 @@ export class LinuxChannelInstaller implements Installer { return { root, bin: "chrome" }; } + async checkInstalledDriver( + version: string, + ): Promise { + const root = await cache.find("chromedriver", version); + if (root) { + return { root, bin: "chromedriver" }; + } + } + async downloadDriver(version: string): Promise { const resolved = await this.versionResolver.resolve(version); if (!resolved) { diff --git a/src/channel_macos.ts b/src/channel_macos.ts index 8c0445a..171d5c7 100644 --- a/src/channel_macos.ts +++ b/src/channel_macos.ts @@ -20,7 +20,9 @@ export class MacOSChannelInstaller implements Installer { this.versionResolver = new LastKnownGoodVersionResolver(platform); } - async checkInstalled(version: string): Promise { + async checkInstalledBrowser( + version: string, + ): Promise { if (!isReleaseChannelName(version)) { throw new Error(`Unexpected version: ${version}`); } @@ -100,6 +102,15 @@ export class MacOSChannelInstaller implements Installer { return { root, bin: bin2 }; } + async checkInstalledDriver( + version: string, + ): Promise { + const root = await cache.find("chromedriver", version); + if (root) { + return { root, bin: "chromedriver" }; + } + } + async downloadDriver(version: string): Promise { const resolved = await this.versionResolver.resolve(version); if (!resolved) { diff --git a/src/channel_windows.ts b/src/channel_windows.ts index 4a28eb7..ffd705b 100644 --- a/src/channel_windows.ts +++ b/src/channel_windows.ts @@ -28,12 +28,14 @@ export class WindowsChannelInstaller implements Installer { this.versionResolver = new LastKnownGoodVersionResolver(platform); } - async checkInstalled(version: string): Promise { + async checkInstalledBrowser( + version: string, + ): Promise { if (!isReleaseChannelName(version)) { throw new Error(`Unexpected version: ${version}`); } - const root = this.rootDir(version); + const root = this.browserRootDir(version); try { await fs.promises.stat(root); } catch (e) { @@ -123,10 +125,10 @@ export class WindowsChannelInstaller implements Installer { } await exec.exec(archive, ["/silent", "/install"]); - return { root: this.rootDir(version), bin: "chrome.exe" }; + return { root: this.browserRootDir(version), bin: "chrome.exe" }; } - private rootDir(version: ReleaseChannelName) { + private browserRootDir(version: ReleaseChannelName) { switch (version) { case "stable": return "C:\\Program Files\\Google\\Chrome\\Application"; @@ -139,6 +141,15 @@ export class WindowsChannelInstaller implements Installer { } } + async checkInstalledDriver( + version: string, + ): Promise { + const root = await cache.find("chromedriver", version); + if (root) { + return { root, bin: "chromedriver" }; + } + } + async downloadDriver(version: string): Promise { const resolved = await this.versionResolver.resolve(version); if (!resolved) { diff --git a/src/installer.ts b/src/installer.ts index f907407..972005c 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -19,14 +19,12 @@ export type DownloadResult = { }; export interface Installer { - checkInstalled(version: string): Promise; - + checkInstalledBrowser(version: string): Promise; downloadBrowser(version: string): Promise; - installBrowser(version: string, archive: string): Promise; + checkInstalledDriver(version: string): Promise; downloadDriver(version: string): Promise; - installDriver(version: string, archive: string): Promise; } @@ -56,7 +54,7 @@ export const install = async ( } })(); - const cache = await installer.checkInstalled(version); + const cache = await installer.checkInstalledBrowser(version); if (cache) { core.info(`Found in cache @ ${cache.root}`); return path.join(cache.root, cache.bin); diff --git a/src/latest_installer.ts b/src/latest_installer.ts index 6f26743..c0ee5dc 100644 --- a/src/latest_installer.ts +++ b/src/latest_installer.ts @@ -11,9 +11,11 @@ export class LatestInstaller implements Installer { constructor(private readonly platform: Platform) {} - async checkInstalled(_version: string): Promise { + async checkInstalledBrowser( + _version: string, + ): Promise { const snapshot = await this.getLatestSnapshot(); - return this.snapshotInstaller.checkInstalled(snapshot); + return this.snapshotInstaller.checkInstalledBrowser(snapshot); } async downloadBrowser(_version: string): Promise { @@ -29,6 +31,13 @@ export class LatestInstaller implements Installer { return this.snapshotInstaller.installBrowser(snapshot, archive); } + async checkInstalledDriver( + _version: string, + ): Promise { + const snapshot = await this.getLatestSnapshot(); + return this.snapshotInstaller.checkInstalledDriver(snapshot); + } + async downloadDriver(_version: string): Promise { const version = await this.getLatestSnapshot(); return this.snapshotInstaller.downloadDriver(version); diff --git a/src/snapshot_installer.ts b/src/snapshot_installer.ts index 10d1426..791e65d 100644 --- a/src/snapshot_installer.ts +++ b/src/snapshot_installer.ts @@ -9,7 +9,9 @@ import { browserDownloadURL, driverDownloadURL } from "./snapshot_bucket"; export class SnapshotInstaller implements Installer { constructor(private readonly platform: Platform) {} - async checkInstalled(version: string): Promise { + async checkInstalledBrowser( + version: string, + ): Promise { const root = await cache.find("chromium", version); if (root) { return { root, bin: "chrome" }; @@ -55,6 +57,15 @@ export class SnapshotInstaller implements Installer { return { root, bin }; } + async checkInstalledDriver( + version: string, + ): Promise { + const root = await cache.find("chromedriver", version); + if (root) { + return { root, bin: "chromedriver" }; + } + } + async downloadDriver(version: string): Promise { const url = driverDownloadURL(this.platform, version); core.info(`Acquiring ${version} from ${url}`); diff --git a/src/version_installer.ts b/src/version_installer.ts index 8dc1652..0a912eb 100644 --- a/src/version_installer.ts +++ b/src/version_installer.ts @@ -16,7 +16,9 @@ export class KnownGoodVersionInstaller implements Installer { this.versionResolver = new KnownGoodVersionResolver(this.platform); } - async checkInstalled(version: string): Promise { + async checkInstalledBrowser( + version: string, + ): Promise { const root = await cache.find("chromium", version); if (root) { return { root, bin: "chrome" }; @@ -65,6 +67,15 @@ export class KnownGoodVersionInstaller implements Installer { return { root: root, bin }; } + async checkInstalledDriver( + version: string, + ): Promise { + const root = await cache.find("chromedriver", version); + if (root) { + return { root, bin: "chromedriver" }; + } + } + async downloadDriver(version: string): Promise { const resolved = await this.versionResolver.resolve(version); if (!resolved) { From d047f6e822135cbf3705726a07c58d834a51502b Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 07:08:37 +0000 Subject: [PATCH 11/20] Support chromedriver at all --- action.yml | 8 ++++ src/index.ts | 109 +++++++++++++++++++++++++++++++++++++++++------ src/installer.ts | 52 ---------------------- 3 files changed, 103 insertions(+), 66 deletions(-) diff --git a/action.yml b/action.yml index a5883f4..fbe9060 100644 --- a/action.yml +++ b/action.yml @@ -11,6 +11,10 @@ inputs: description: |- Install dependent packages for Google Chrome/Chromium (Linux only). default: false + no-install-chromedriver: + description: |- + Install only Google Chrome/Chromium browser and skip installing chromedriver + default: false no-sudo: description: |- Do not use sudo to install Google Chrome/Chromium (Linux only). @@ -20,6 +24,10 @@ outputs: description: 'The installed Google Chrome/Chromium version. Useful when given a latest version.' chrome-path: description: 'The installed Google Chrome/Chromium path.' + chromedriver-version: + description: 'The installed ChromeDriver version. Useful when given a latest version.' + chromedriver-path: + description: 'The installed ChromeDriver path.' runs: using: 'node20' main: 'index.js' diff --git a/src/index.ts b/src/index.ts index c77fe5b..19bb28b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,75 @@ import path from "node:path"; import * as core from "@actions/core"; import * as exec from "@actions/exec"; +import { LinuxChannelInstaller } from "./channel_linux"; +import { MacOSChannelInstaller } from "./channel_macos"; +import { WindowsChannelInstaller } from "./channel_windows"; import { installDependencies } from "./dependencies"; -import * as installer from "./installer"; +import type { Installer } from "./installer"; +import { LatestInstaller } from "./latest_installer"; import { OS, type Platform, getPlatform } from "./platform"; +import { SnapshotInstaller } from "./snapshot_installer"; +import { parse } from "./version"; +import { KnownGoodVersionInstaller } from "./version_installer"; const hasErrorMessage = (e: unknown): e is { message: string | Error } => { return typeof e === "object" && e !== null && "message" in e; }; +const getInstaller = (platform: Platform, version: string) => { + const spec = parse(version); + switch (spec.value.type) { + case "latest": + return new LatestInstaller(platform); + case "channel": + switch (platform.os) { + case OS.LINUX: + return new LinuxChannelInstaller(platform); + case OS.DARWIN: + return new MacOSChannelInstaller(platform); + case OS.WINDOWS: + return new WindowsChannelInstaller(platform); + } + break; + case "snapshot": + return new SnapshotInstaller(platform); + case "four-parts": + return new KnownGoodVersionInstaller(platform); + } +}; + +const installBrowser = async (installer: Installer, version: string) => { + const cache = await installer.checkInstalledBrowser(version); + if (cache) { + core.info(`Found in cache of chrome ${version} @ ${cache.root}`); + return path.join(cache.root, cache.bin); + } + + core.info(`Attempting to download chrome ${version}...`); + const { archive } = await installer.downloadBrowser(version); + + core.info("Installing chrome..."); + const { root, bin } = await installer.installBrowser(version, archive); + + return path.join(root, bin); +}; + +const installDriver = async (installer: Installer, version: string) => { + const cache = await installer.checkInstalledDriver(version); + if (cache) { + core.info(`Found in cache of chromedriver ${version} @ ${cache.root}`); + return path.join(cache.root, cache.bin); + } + + core.info(`Attempting to download chromedriver ${version}...`); + const { archive } = await installer.downloadDriver(version); + + core.info("Installing chromedriver..."); + const { root, bin } = await installer.installDriver(version, archive); + + return path.join(root, bin); +}; + const testVersion = async ( platform: Platform, bin: string, @@ -29,18 +90,26 @@ const testVersion = async ( const output = await exec.getExecOutput(`"${bin}"`, ["--version"], {}); if (output.exitCode !== 0) { throw new Error( - `chromium exits with status ${output.exitCode}: ${output.stderr}`, + `${path.basename(bin)} exits with status ${output.exitCode}: ${ + output.stderr + }`, ); } + + const stdout = output.stdout.trim(); if ( - !output.stdout.startsWith("Chromium ") && - !output.stdout.startsWith("Google Chrome ") + !stdout.startsWith("Chromium ") && + !stdout.startsWith("Google Chrome ") && + !stdout.startsWith("ChromeDriver ") ) { - throw new Error(`chromium outputs unexpected results: ${output.stdout}`); + throw new Error( + `${path.basename(bin)} outputs unexpected results: ${stdout}`, + ); } - return output.stdout + return stdout .replace("Chromium ", "") .replace("Google Chrome ", "") + .replace("ChromeDriver ", "") .split(" ", 1)[0]; }; @@ -50,6 +119,8 @@ async function run(): Promise { const platform = getPlatform(); const flagInstallDependencies = core.getInput("install-dependencies") === "true"; + const noInstallChromedriver = + core.getInput("no-install-chromedriver") === "true"; const noSudo = core.getInput("no-sudo") === "true"; if (flagInstallDependencies) { @@ -57,19 +128,29 @@ async function run(): Promise { await installDependencies(platform, { noSudo }); } - core.info(`Setup chromium ${version}`); + core.info(`Setup chrome ${version}`); - const binPath = await installer.install(platform, version); - const installDir = path.dirname(binPath); + const installer = getInstaller(platform, version); + const browserBinPath = await installBrowser(installer, version); + const actualBrowserVersion = await testVersion(platform, browserBinPath); - core.addPath(path.join(installDir)); + core.addPath(path.dirname(browserBinPath)); + core.setOutput("chrome-path", browserBinPath); + core.setOutput("chrome-version", actualBrowserVersion); + core.info(`Successfully setup chromium ${actualBrowserVersion}`); - const actualVersion = await testVersion(platform, binPath); + if (!noInstallChromedriver) { + core.info(`Setup chromedriver ${version}`); - core.info(`Successfully setup chromium version ${actualVersion}`); + const driverInstaller = new SnapshotInstaller(platform); + const driverBinPath = await installDriver(driverInstaller, version); + const actualDriverVersion = await testVersion(platform, driverBinPath); - core.setOutput("chrome-version", actualVersion); - core.setOutput("chrome-path", binPath); + core.addPath(path.dirname(driverBinPath)); + core.setOutput("chromedriver-path", driverBinPath); + core.setOutput("chromedriver-version", actualDriverVersion); + core.info(`Successfully setup chromedriver ${actualDriverVersion}`); + } } catch (error) { if (hasErrorMessage(error)) { core.setFailed(error.message); diff --git a/src/installer.ts b/src/installer.ts index 972005c..bcb0bb4 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -1,14 +1,3 @@ -import path from "node:path"; -import * as core from "@actions/core"; -import { LinuxChannelInstaller } from "./channel_linux"; -import { MacOSChannelInstaller } from "./channel_macos"; -import { WindowsChannelInstaller } from "./channel_windows"; -import { LatestInstaller } from "./latest_installer"; -import { OS, type Platform } from "./platform"; -import { SnapshotInstaller } from "./snapshot_installer"; -import { parse } from "./version"; -import { KnownGoodVersionInstaller } from "./version_installer"; - export type InstallResult = { root: string; // root is a directory containing all contents for chromium bin: string; // bin is a sub-path to chromium executable binary from root @@ -27,44 +16,3 @@ export interface Installer { downloadDriver(version: string): Promise; installDriver(version: string, archive: string): Promise; } - -export const install = async ( - platform: Platform, - version: string, -): Promise => { - const installer = (() => { - const spec = parse(version); - switch (spec.value.type) { - case "latest": - return new LatestInstaller(platform); - case "channel": - switch (platform.os) { - case OS.LINUX: - return new LinuxChannelInstaller(platform); - case OS.DARWIN: - return new MacOSChannelInstaller(platform); - case OS.WINDOWS: - return new WindowsChannelInstaller(platform); - } - break; - case "snapshot": - return new SnapshotInstaller(platform); - case "four-parts": - return new KnownGoodVersionInstaller(platform); - } - })(); - - const cache = await installer.checkInstalledBrowser(version); - if (cache) { - core.info(`Found in cache @ ${cache.root}`); - return path.join(cache.root, cache.bin); - } - - core.info(`Attempting to download ${version}...`); - const { archive } = await installer.downloadBrowser(version); - - core.info("Installing chromium..."); - const { root, bin } = await installer.installBrowser(version, archive); - - return path.join(root, bin); -}; From fef2ad18945bb2864d4c5f20988faab6db95a5dc Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 07:10:36 +0000 Subject: [PATCH 12/20] Run chromedriver on CI --- .github/workflows/build.yml | 3 +++ .github/workflows/manual-test.yml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 193f093..f5c16db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,9 +45,11 @@ jobs: - if: runner.os == 'Linux' || runner.os == 'macOS' run: | "${{ steps.setup-chrome.outputs.chrome-path }}" --version + "${{ steps.setup-chrome.outputs.chromedriver-path }}" --version - if: runner.os == 'Windows' run: | (Get-Item (Get-Command "${{ steps.setup-chrome.outputs.chrome-path }}").Source).VersionInfo.ProductVersion + (Get-Item (Get-Command "${{ steps.setup-chrome.outputs.chromedriver-path }}").Source).VersionInfo.ProductVersion test-install-dependencies: needs: [build] @@ -86,3 +88,4 @@ jobs: id: setup-chrome - run: | "${{ steps.setup-chrome.outputs.chrome-path }}" --version + "${{ steps.setup-chrome.outputs.chromedriver-path }}" --version diff --git a/.github/workflows/manual-test.yml b/.github/workflows/manual-test.yml index 2ce2d71..7bfc574 100644 --- a/.github/workflows/manual-test.yml +++ b/.github/workflows/manual-test.yml @@ -55,6 +55,8 @@ jobs: - if: runner.os == 'Linux' || runner.os == 'macOS' run: | "${{ steps.setup-chrome.outputs.chrome-path }}" --version + "${{ steps.setup-chrome.outputs.chromedriver-path }}" --version - if: runner.os == 'Windows' run: | (Get-Item (Get-Command "${{ steps.setup-chrome.outputs.chrome-path }}").Source).VersionInfo.ProductVersion + (Get-Item (Get-Command "${{ steps.setup-chrome.outputs.chromedriver-path }}").Source).VersionInfo.ProductVersion From 400637c0f8cab25c29dd9d34a676fe53d13501d0 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 07:21:27 +0000 Subject: [PATCH 13/20] improve log message --- src/channel_linux.ts | 4 ++-- src/channel_macos.ts | 4 ++-- src/channel_windows.ts | 4 ++-- src/latest_installer.ts | 1 - src/snapshot_installer.ts | 4 ++-- src/version_installer.ts | 6 +++--- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/channel_linux.ts b/src/channel_linux.ts index 92d2987..3ade54d 100644 --- a/src/channel_linux.ts +++ b/src/channel_linux.ts @@ -53,7 +53,7 @@ export class LinuxChannelInstaller implements Installer { } })(); - core.info(`Acquiring ${version} from ${url}`); + core.info(`Acquiring chrome ${version} from ${url}`); const archive = await tc.downloadTool(url); return { archive }; } @@ -109,7 +109,7 @@ export class LinuxChannelInstaller implements Installer { } core.info( - `Acquiring ${resolved.version} from ${resolved.driverDownloadURL}`, + `Acquiring chromedriver ${resolved.version} from ${resolved.driverDownloadURL}`, ); const archive = await tc.downloadTool(resolved.driverDownloadURL); return { archive }; diff --git a/src/channel_macos.ts b/src/channel_macos.ts index 171d5c7..77fd502 100644 --- a/src/channel_macos.ts +++ b/src/channel_macos.ts @@ -46,7 +46,7 @@ export class MacOSChannelInstaller implements Installer { } })(); - core.info(`Acquiring ${version} from ${url}`); + core.info(`Acquiring chrome ${version} from ${url}`); const archive = await tc.downloadTool(url); return { archive }; } @@ -120,7 +120,7 @@ export class MacOSChannelInstaller implements Installer { } core.info( - `Acquiring ${resolved.version} from ${resolved.driverDownloadURL}`, + `Acquiring chromedriver ${resolved.version} from ${resolved.driverDownloadURL}`, ); const archive = await tc.downloadTool(resolved.driverDownloadURL); return { archive }; diff --git a/src/channel_windows.ts b/src/channel_windows.ts index ffd705b..ffb6c9a 100644 --- a/src/channel_windows.ts +++ b/src/channel_windows.ts @@ -109,7 +109,7 @@ export class WindowsChannelInstaller implements Installer { path[this.platform.arch][version] }`; - core.info(`Acquiring ${version} from ${url}`); + core.info(`Acquiring chrome ${version} from ${url}`); const archivePath = await tc.downloadTool(url); await fs.promises.rename(archivePath, `${archivePath}.exe`); @@ -159,7 +159,7 @@ export class WindowsChannelInstaller implements Installer { } core.info( - `Acquiring ${resolved.version} from ${resolved.driverDownloadURL}`, + `Acquiring chromedriver ${resolved.version} from ${resolved.driverDownloadURL}`, ); const archive = await tc.downloadTool(resolved.driverDownloadURL); return { archive }; diff --git a/src/latest_installer.ts b/src/latest_installer.ts index c0ee5dc..c9a81ec 100644 --- a/src/latest_installer.ts +++ b/src/latest_installer.ts @@ -1,4 +1,3 @@ -import * as cache from "./cache"; import type { DownloadResult, InstallResult, Installer } from "./installer"; import type { Platform } from "./platform"; import { resolveLatestVersion } from "./snapshot_bucket"; diff --git a/src/snapshot_installer.ts b/src/snapshot_installer.ts index 791e65d..39c5408 100644 --- a/src/snapshot_installer.ts +++ b/src/snapshot_installer.ts @@ -20,7 +20,7 @@ export class SnapshotInstaller implements Installer { async downloadBrowser(version: string): Promise { const url = browserDownloadURL(this.platform, version); - core.info(`Acquiring ${version} from ${url}`); + core.info(`Acquiring chrome ${version} from ${url}`); const archive = await tc.downloadTool(url); return { archive }; } @@ -68,7 +68,7 @@ export class SnapshotInstaller implements Installer { async downloadDriver(version: string): Promise { const url = driverDownloadURL(this.platform, version); - core.info(`Acquiring ${version} from ${url}`); + core.info(`Acquiring chromedriver ${version} from ${url}`); const archive = await tc.downloadTool(url); return { archive }; } diff --git a/src/version_installer.ts b/src/version_installer.ts index 0a912eb..47cf079 100644 --- a/src/version_installer.ts +++ b/src/version_installer.ts @@ -4,7 +4,7 @@ import * as tc from "@actions/tool-cache"; import * as cache from "./cache"; import { KnownGoodVersionResolver } from "./chrome_for_testing"; import type { DownloadResult, InstallResult, Installer } from "./installer"; -import { Arch, OS, type Platform } from "./platform"; +import { OS, type Platform } from "./platform"; export class KnownGoodVersionInstaller implements Installer { private readonly versionResolver: KnownGoodVersionResolver; @@ -32,7 +32,7 @@ export class KnownGoodVersionInstaller implements Installer { } core.info( - `Acquiring ${resolved.version} from ${resolved.browserDownloadURL}`, + `Acquiring chrome ${resolved.version} from ${resolved.browserDownloadURL}`, ); const archive = await tc.downloadTool(resolved.browserDownloadURL); return { archive }; @@ -83,7 +83,7 @@ export class KnownGoodVersionInstaller implements Installer { } core.info( - `Acquiring ${resolved.version} from ${resolved.driverDownloadURL}`, + `Acquiring chromedriver ${resolved.version} from ${resolved.driverDownloadURL}`, ); const archive = await tc.downloadTool(resolved.driverDownloadURL); return { archive }; From 9e94c531a50dfa6820b5b8ab7b5475c23719aa2a Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 07:26:54 +0000 Subject: [PATCH 14/20] handle version parse error --- src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 19bb28b..081e715 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,11 +106,15 @@ const testVersion = async ( `${path.basename(bin)} outputs unexpected results: ${stdout}`, ); } - return stdout + const v = stdout .replace("Chromium ", "") .replace("Google Chrome ", "") .replace("ChromeDriver ", "") .split(" ", 1)[0]; + if (!/^\d+\.\d+\.\d+\.\d+$/.test(v)) { + throw new Error(`Failed to parse version from: ${stdout}`); + } + return v; }; async function run(): Promise { From 0aa6d05359120f7ba7024da8f7e60a198bb0f9b0 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 07:31:25 +0000 Subject: [PATCH 15/20] Support "Google Chrome for Testing" versioning --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index 081e715..7ec3429 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,6 +100,7 @@ const testVersion = async ( if ( !stdout.startsWith("Chromium ") && !stdout.startsWith("Google Chrome ") && + !stdout.startsWith("Google Chrome for Testing ") && !stdout.startsWith("ChromeDriver ") ) { throw new Error( @@ -109,6 +110,7 @@ const testVersion = async ( const v = stdout .replace("Chromium ", "") .replace("Google Chrome ", "") + .replace("Google Chrome for Testing ", "") .replace("ChromeDriver ", "") .split(" ", 1)[0]; if (!/^\d+\.\d+\.\d+\.\d+$/.test(v)) { From 2686a3dd7183151d84715166f9a166b708d7a735 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 07:35:01 +0000 Subject: [PATCH 16/20] fix order --- src/chrome_for_testing.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chrome_for_testing.ts b/src/chrome_for_testing.ts index b9070fb..4035a33 100644 --- a/src/chrome_for_testing.ts +++ b/src/chrome_for_testing.ts @@ -133,8 +133,9 @@ export class KnownGoodVersionResolver { throw new Error("Failed to get known good versions"); } - this.cache = resp.result.versions.reverse(); + resp.result.versions.reverse(); + this.cache = resp.result.versions; return resp.result.versions; } } From 53097c464627841c688b774edd6ed6b1564dcafe Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 07:40:32 +0000 Subject: [PATCH 17/20] fix versioning parser --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7ec3429..14db8f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,8 +99,8 @@ const testVersion = async ( const stdout = output.stdout.trim(); if ( !stdout.startsWith("Chromium ") && - !stdout.startsWith("Google Chrome ") && !stdout.startsWith("Google Chrome for Testing ") && + !stdout.startsWith("Google Chrome ") && !stdout.startsWith("ChromeDriver ") ) { throw new Error( @@ -109,8 +109,8 @@ const testVersion = async ( } const v = stdout .replace("Chromium ", "") - .replace("Google Chrome ", "") .replace("Google Chrome for Testing ", "") + .replace("Google Chrome ", "") .replace("ChromeDriver ", "") .split(" ", 1)[0]; if (!/^\d+\.\d+\.\d+\.\d+$/.test(v)) { From 99092d232079702f93cb914f70e8001e9a585d6e Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 07:57:51 +0000 Subject: [PATCH 18/20] fix installer --- src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 14db8f6..6a74c5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -148,8 +148,7 @@ async function run(): Promise { if (!noInstallChromedriver) { core.info(`Setup chromedriver ${version}`); - const driverInstaller = new SnapshotInstaller(platform); - const driverBinPath = await installDriver(driverInstaller, version); + const driverBinPath = await installDriver(installer, version); const actualDriverVersion = await testVersion(platform, driverBinPath); core.addPath(path.dirname(driverBinPath)); From b63f8b4d68c810a91412b015078649a827f03649 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 08:09:03 +0000 Subject: [PATCH 19/20] Disable installing ChromeDriver by default --- .github/workflows/build.yml | 2 ++ action.yml | 4 ++-- src/index.ts | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5c16db..32a76ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,7 @@ jobs: uses: ./ with: chrome-version: ${{ matrix.version }} + install-chromedriver: true id: setup-chrome - if: runner.os == 'Linux' || runner.os == 'macOS' run: | @@ -84,6 +85,7 @@ jobs: uses: ./ with: chrome-version: 120 + install-chromedriver: true install-dependencies: true id: setup-chrome - run: | diff --git a/action.yml b/action.yml index fbe9060..f8d944a 100644 --- a/action.yml +++ b/action.yml @@ -11,9 +11,9 @@ inputs: description: |- Install dependent packages for Google Chrome/Chromium (Linux only). default: false - no-install-chromedriver: + install-chromedriver: description: |- - Install only Google Chrome/Chromium browser and skip installing chromedriver + Install the compatible version of ChromeDriver with the installed Google Chrome/Chromium. default: false no-sudo: description: |- diff --git a/src/index.ts b/src/index.ts index 6a74c5a..e7c7a26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,8 +125,8 @@ async function run(): Promise { const platform = getPlatform(); const flagInstallDependencies = core.getInput("install-dependencies") === "true"; - const noInstallChromedriver = - core.getInput("no-install-chromedriver") === "true"; + const flgInstallChromedriver = + core.getInput("install-chromedriver") === "true"; const noSudo = core.getInput("no-sudo") === "true"; if (flagInstallDependencies) { @@ -145,7 +145,7 @@ async function run(): Promise { core.setOutput("chrome-version", actualBrowserVersion); core.info(`Successfully setup chromium ${actualBrowserVersion}`); - if (!noInstallChromedriver) { + if (flgInstallChromedriver) { core.info(`Setup chromedriver ${version}`); const driverBinPath = await installDriver(installer, version); From 6a44268de767ef0f48342197eef213513c01bd26 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 26 May 2024 08:23:22 +0000 Subject: [PATCH 20/20] Improve docs --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b87398a..eefbdc8 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ # setup-chrome -This action sets by Google Chrome/Chromium for use in actions by: +This action sets-up Google Chrome/Chromium for GitHub Actions. This action supports the following features: -- [X] Install and setup latest Chromium -- [X] Cross platform runner (macOS, Linux, Windows) -- [X] Install Google Chrome by channel (stable, beta, dev, and canary) -- [X] Install by version number (88.0.4324, or 88.0) +- Install and setup the Google Chrome onto the runner. +- Install a specific version of Google Chrome/Chromium by the version number, commit position, and release channel. +- Cross-platform runner support (Windows, macOS, Linux) and self-hosted runner support. +- Install the compatible versions of ChromeDriver with the browser. ## Usage @@ -31,6 +31,17 @@ steps: chrome-version: 120 ``` +The action support installing the compatible ChromeDriver with the browser. +You can use the `install-chromedriver` to install the ChromeDriver. + +```yaml +steps: + - uses: browser-actions/setup-chrome@v1 + with: + chrome-version: 120 + install-chromedriver: true +``` + If you use the self-hosted runner, your runner may not have the required dependencies on the system. You can install the dependencies by using the `install-dependencies` parameter. It installs the required dependencies for the Google Chrome/Chromium to run automatically. @@ -77,11 +88,15 @@ steps: Default: `latest` - `install-dependencies`: *(Optional)* Install the required dependencies for the Google Chrome/Chromium to run. Default: `false` +- `install-chromedriver`: *(Optional)* Install the compatible ChromeDriver with the browser. + Default: `false` ### Outputs - `chrome-path`: The installed Google Chrome/Chromium binary path. - `chrome-version`: The installed Google Chrome/Chromium version. +- `chromedriver-path`: The installed ChromeDriver binary path. +- `chromedriver-version`: The installed ChromeDriver version. [snapshots]: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html