diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 305e665..e576c8e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,8 @@ jobs: - name: "Run Biome" run: biome ci - build: - name: "Build" + test: + name: "Build & Test" runs-on: ubuntu-latest steps: - name: "Checkout the repository" @@ -40,7 +40,10 @@ jobs: run: pnpm install - name: "Run Build" - run: pnpm run build + run: pnpm build + + - name: "Run Tests" + run: pnpm test test-action: name: "Test Action" diff --git a/.gitignore b/.gitignore index 34477e4..b8e00cd 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,4 @@ lerna-debug.log* .eslintcache .turbo .build_cache -temp +.tmp/ diff --git a/README.md b/README.md index 9dc10ec..57eb0ab 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,10 @@ Install any tool or binary with full cache, dynamic latest version, easy to conf ## Presets | Name | URL | -| ----------------- | ------------------------------------------------------ | +|-------------------|--------------------------------------------------------| | `infisical-cli` | https://github.com/Infisical/infisical | | `cloud-sql-proxy` | https://github.com/GoogleCloudPlatform/cloud-sql-proxy | +| `github-cli` | https://github.com/cli/cli | ## Custom Config diff --git a/package.json b/package.json index 6dfd4d0..03bde79 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "dev": "pnpm build --watch", "build": "tsup src/index.ts --treeshake --minify --clean", "typecheck": "tsc", + "test": "vitest run", + "test:watch": "vitest", "lint": "biome lint", "lint:fix": "biome lint -w", "format": "biome format --write", diff --git a/src/index.ts b/src/index.ts index a285f01..1c56af2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,101 +1,3 @@ -// noinspection ExceptionCaughtLocallyJS - -import path from "node:path"; -import * as core from "@actions/core"; -import { shake, template } from "radash"; -import { downloadTool, getVersion } from "./install"; -import presets from "./presets"; -import type { Config } from "./types"; - -export async function main() { - try { - const templateArgs = { - os: process.platform === "win32" ? "windows" : process.platform, - arch: process.arch === "x64" ? "amd64" : process.arch, - arch2: process.arch, - exe: process.platform === "win32" ? ".exe" : "", - archive: process.platform === "win32" ? "zip" : "tar.gz", - }; - - // load config - let config: Config = { - id: core.getInput("id"), - preset: core.getInput("preset"), - repo: core.getInput("repo"), - version: core.getInput("version"), - versionUrl: core.getInput("version_url"), - versionRegex: core.getInput("version_regex"), - downloadUrl: core.getInput("download_url"), - downloadName: core.getInput("download_name"), - binPath: core.getInput("bin_path"), - cache: core.getBooleanInput("cache"), - }; - - core.debug(`loaded config: ${JSON.stringify(config)}`); - - // if preset is set, load preset config - if (config.preset) { - if (!presets[config.preset]) - throw new Error(`Preset not found: ${config.preset}`); - config = { - ...config, - ...presets[config.preset], - ...shake(config, (x) => x === ""), - }; - } - - // set defaults - if (config.versionRegex && typeof config.versionRegex === "string") - config.versionRegex = new RegExp(config.versionRegex); - if (!config.versionUrl && !config.versionPath && config.repo) - config.versionPath = "tag_name"; - if (!config.versionUrl && config.repo) - config.versionUrl = `https://api.github.com/repos/${config.repo}/releases/latest`; - - core.debug(`resolved config: ${JSON.stringify(config)}`); - - if (!config.downloadUrl) { - throw new Error("Download URL missing"); - } - - // fetch latest version if not fixed - if (config.version === "latest") { - const version = await getVersion(config); - if (!version) throw new Error("Version not found"); - config.version = version; - core.debug(`resolved version: ${config.version}`); - } - - // template download url - config.downloadUrl = template(config.downloadUrl, { - ...config, - ...templateArgs, - }); - if (config.downloadUrl.startsWith("/") && config.repo) - config.downloadUrl = `https://github.com/${config.repo}${config.downloadUrl}`; - - core.debug(`templated download url: ${config.downloadUrl}`); - - // download or pull from cache - const toolPath = await downloadTool(config); - - if (config.binPath !== "") { - config.binPath = template(config.binPath, { - ...config, - ...templateArgs, - }); - } - - core.debug(`cached path: ${toolPath}`); - core.addPath(path.join(toolPath, config.binPath)); - core.info(`Successfully installed version ${config.version}`); - - core.setOutput("path", toolPath); - core.setOutput("version", config.version); - } catch (error) { - // fail the workflow run if an error occurs - if (error instanceof Error) core.setFailed(error.message); - } -} +import { main } from "./main"; main(); diff --git a/src/main.test.ts b/src/main.test.ts new file mode 100644 index 0000000..38d4783 --- /dev/null +++ b/src/main.test.ts @@ -0,0 +1,75 @@ +import * as fs from "node:fs/promises"; +import path from "node:path"; +import * as core from "@actions/core"; +import { exec } from "@actions/exec"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + test, + vi, +} from "vitest"; +import { main } from "./main"; + +const RUNNER_TEMP = path.join(process.cwd(), ".tmp"); +const RUNNER_TOOL_CACHE = path.join(RUNNER_TEMP, "./tool-cache"); + +const verifyBinary = async (binPath: string | undefined, binary: string) => { + if (!binPath) throw new Error("Bin path not found"); + + const extension = process.platform === "win32" ? ".exe" : ""; + const filePath = path.join(binPath, binary + extension); + expect(filePath.startsWith(RUNNER_TOOL_CACHE)).toBe(true); + + // check if binary exists + await fs.lstat(filePath); + + // try executing the binary + await exec(filePath, ["--version"]); +}; + +describe("action", () => { + beforeAll(async () => { + await fs.mkdir(RUNNER_TEMP, { recursive: true }); + await fs.rm(RUNNER_TEMP, { recursive: true }); + }); + + beforeEach(() => { + // set default values + vi.stubEnv("INPUT_CACHE", "true"); + vi.stubEnv("INPUT_VERSION", "latest"); + vi.stubEnv("INPUT_VERSION_REGEX", "(?[\\d.]+)"); + vi.stubEnv("RUNNER_TOOL_CACHE", RUNNER_TOOL_CACHE); + vi.stubEnv("RUNNER_TEMP", RUNNER_TEMP); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("install infisical-cli", async () => { + vi.stubEnv("INPUT_PRESET", "infisical-cli"); + const spy = vi.spyOn(core, "addPath"); + await main(); + expect(spy).toHaveBeenCalled(); + await verifyBinary(spy.mock.lastCall?.[0], "infisical"); + }, 10000); + + test("install cloud-sql-proxy", async () => { + vi.stubEnv("INPUT_PRESET", "cloud-sql-proxy"); + const spy = vi.spyOn(core, "addPath"); + await main(); + expect(spy).toHaveBeenCalled(); + await verifyBinary(spy.mock.lastCall?.[0], "cloud-sql-proxy"); + }, 10000); + + test("install github-cli", async () => { + vi.stubEnv("INPUT_PRESET", "github-cli"); + const spy = vi.spyOn(core, "addPath"); + await main(); + expect(spy).toHaveBeenCalled(); + await verifyBinary(spy.mock.lastCall?.[0], "gh"); + }, 10000); +}); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..99caaea --- /dev/null +++ b/src/main.ts @@ -0,0 +1,106 @@ +// noinspection ExceptionCaughtLocallyJS + +import path from "node:path"; +import * as core from "@actions/core"; +import { shake, template } from "radash"; +import { downloadTool, getVersion } from "./install"; +import presets from "./presets"; +import type { Config } from "./types"; + +export async function main() { + try { + const templateArgs = { + os: process.platform === "win32" ? "windows" : process.platform, + arch: process.arch === "x64" ? "amd64" : process.arch, + arch2: process.arch, + exe: process.platform === "win32" ? ".exe" : "", + archive: process.platform === "win32" ? "zip" : "tar.gz", + }; + + // load config + let config: Config = { + id: core.getInput("id"), + preset: core.getInput("preset"), + repo: core.getInput("repo"), + version: core.getInput("version"), + versionUrl: core.getInput("version_url"), + versionRegex: core.getInput("version_regex"), + downloadUrl: core.getInput("download_url"), + downloadName: core.getInput("download_name"), + binPath: core.getInput("bin_path"), + cache: core.getBooleanInput("cache"), + }; + + core.debug(`loaded config: ${JSON.stringify(config)}`); + + // if preset is set, load preset config + if (config.preset) { + if (!presets[config.preset]) + throw new Error(`Preset not found: ${config.preset}`); + config = { + ...config, + ...presets[config.preset], + ...shake(config, (x) => x === ""), + }; + } + + // set defaults + if (config.versionRegex && typeof config.versionRegex === "string") + config.versionRegex = new RegExp(config.versionRegex); + if (!config.versionUrl && !config.versionPath && config.repo) + config.versionPath = "tag_name"; + if (!config.versionUrl && config.repo) + config.versionUrl = `https://api.github.com/repos/${config.repo}/releases/latest`; + + core.debug(`resolved config: ${JSON.stringify(config)}`); + + if (!config.downloadUrl) { + throw new Error("Download URL missing"); + } + + if (config.downloadName) { + config.downloadName = template(config.downloadName, { + ...config, + ...templateArgs, + }); + } + + // fetch latest version if not fixed + if (config.version === "latest") { + const version = await getVersion(config); + if (!version) throw new Error("Version not found"); + config.version = version; + core.debug(`resolved version: ${config.version}`); + } + + // template download url + config.downloadUrl = template(config.downloadUrl, { + ...config, + ...templateArgs, + }); + if (config.downloadUrl.startsWith("/") && config.repo) + config.downloadUrl = `https://github.com/${config.repo}${config.downloadUrl}`; + + core.debug(`templated download url: ${config.downloadUrl}`); + + // download or pull from cache + const toolPath = await downloadTool(config); + + if (config.binPath !== "") { + config.binPath = template(config.binPath, { + ...config, + ...templateArgs, + }); + } + + core.debug(`cached path: ${toolPath}`); + core.addPath(path.join(toolPath, config.binPath)); + core.info(`Successfully installed version ${config.version}`); + + core.setOutput("path", toolPath); + core.setOutput("version", config.version); + } catch (error) { + // fail the workflow run if an error occurs + if (error instanceof Error) core.setFailed(error.message); + } +} diff --git a/src/presets/index.ts b/src/presets/index.ts index ed43907..555decd 100644 --- a/src/presets/index.ts +++ b/src/presets/index.ts @@ -8,16 +8,17 @@ const presets: Record> = { }, "cloud-sql-proxy": { repo: "GoogleCloudPlatform/cloudsql-proxy", - downloadUrl: - "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v{{version}}/cloud-sql-proxy.{{os}}.{{arch}}", - downloadName: "cloud-sql-proxy", + downloadUrl: `https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v{{version}}/cloud-sql-proxy.${process.platform === "win32" ? "{{arch2}}.exe" : "{{os}}.{{arch}}"}`, + downloadName: "cloud-sql-proxy{{exe}}", }, "github-cli": { repo: "cli/cli", - version: "2.62.0", downloadUrl: "/releases/download/v{{version}}/gh_{{version}}_{{os}}_{{arch}}.{{archive}}", - binPath: "/gh_{{version}}_{{os}}_{{arch}}/bin", + binPath: + process.platform === "win32" + ? "/bin" + : "/gh_{{version}}_{{os}}_{{arch}}/bin", }, };