diff --git a/README.md b/README.md index f71b28f..9ae0b4a 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ steps: ``` If `version` is omitted, the action checks the repository root for `bun.lock`, -`pnpm-lock.yaml`, or `package-lock.json` and uses the declared `supabase` -version. If no supported lockfile is present, it falls back to `latest`. +`pnpm-lock.yaml`, or `package-lock.json` and installs the declared `supabase` +package version through npm. If the lockfile includes package integrity +metadata, the action verifies it against the npm registry before installing. If +no supported lockfile is present, it falls back to `latest`. A specific version of the `supabase` CLI can be installed: @@ -47,7 +49,6 @@ steps: - uses: supabase/setup-cli@v2 with: version: latest - github-token: ${{ github.token }} - run: supabase init - run: supabase db start ``` @@ -59,10 +60,10 @@ on Windows and macOS runners. The action supports the following inputs: -| Name | Type | Description | Default | Required | -| -------------- | ------ | -------------------------------------------------------------------------- | --------------------------------- | -------- | -| `version` | String | Supabase CLI version (or `latest`) | Root lockfile version or `latest` | false | -| `github-token` | String | GitHub token used to resolve `latest` without unauthenticated API limiting | | false | +| Name | Type | Description | Default | Required | +| -------------- | ------ | ---------------------------------------------------------------- | --------------------------------- | -------- | +| `version` | String | Supabase CLI version (or `latest`) | Root lockfile version or `latest` | false | +| `github-token` | String | Deprecated; no longer used now that installs resolve through npm | | false | ## Advanced Usage diff --git a/action.yml b/action.yml index 89c3955..d4b9bd0 100644 --- a/action.yml +++ b/action.yml @@ -6,7 +6,7 @@ inputs: description: Version of Supabase CLI to install. If omitted, detect from the root lockfile and otherwise use latest. required: false github-token: - description: GitHub token used to resolve the latest Supabase CLI release without hitting unauthenticated API limits. + description: Deprecated. The action now installs through npm and does not use GitHub release API requests. required: false outputs: version: @@ -112,5 +112,4 @@ runs: working-directory: ${{ github.action_path }} env: INPUT_VERSION: ${{ inputs.version }} - SUPABASE_CLI_GITHUB_TOKEN: ${{ inputs.github-token }} run: bun src/main.ts diff --git a/bun.lock b/bun.lock index ac85b2d..e23a0aa 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,6 @@ "name": "setup-cli", "dependencies": { "@actions/core": "^3.0.1", - "@actions/tool-cache": "^4.0.0", }, "devDependencies": { "@tsconfig/bun": "^1.0.10", @@ -27,8 +26,6 @@ "@actions/io": ["@actions/io@3.0.2", "", {}, "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw=="], - "@actions/tool-cache": ["@actions/tool-cache@4.0.0", "", { "dependencies": { "@actions/core": "^3.0.0", "@actions/exec": "^3.0.0", "@actions/http-client": "^4.0.0", "@actions/io": "^3.0.0", "semver": "^7.7.3" } }, "sha512-L8P9HbXvpvqjZDveb/fdsa55IVC0trfPgQ4ZwGo6r5af6YDVdM9vMGPZ7rgY2fAT9gGj4PSYd6bYlg3p3jD78A=="], - "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.49.0", "", { "os": "android", "cpu": "arm" }, "sha512-HbifJ84prIh9+55CTPAU35JdRQrwg47y16cGerCC+iejSKOuHXYo2WDql6l7cQlzrYVtc3f4UWY+dBj2lRmOeA=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.49.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ef7SKJqAaH2d7E6eXZZa2OffIShbhFMxnGK0zd93p4qiyTJr75B0qf7lrPD+qQOwcf04BrjYJ0JUxq8d5+yZwg=="], @@ -147,8 +144,6 @@ "oxlint-tsgolint": ["oxlint-tsgolint@0.22.1", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.22.1", "@oxlint-tsgolint/darwin-x64": "0.22.1", "@oxlint-tsgolint/linux-arm64": "0.22.1", "@oxlint-tsgolint/linux-x64": "0.22.1", "@oxlint-tsgolint/win32-arm64": "0.22.1", "@oxlint-tsgolint/win32-x64": "0.22.1" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-YUSGSLUnoolsu8gxISEDio3q1rtsCozwfOzASUn3DT2mR2EeQ93uEEnen7s+6LpF+lyTQFln1pQfqwBh/fsVEg=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], @@ -156,7 +151,5 @@ "undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@actions/tool-cache/@actions/core": ["@actions/core@3.0.0", "", { "dependencies": { "@actions/exec": "^3.0.0", "@actions/http-client": "^4.0.0" } }, "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg=="], } } diff --git a/package.json b/package.json index 95ddf46..3f1493a 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,7 @@ "typecheck": "bun x tsgo -p tsconfig.json --noEmit" }, "dependencies": { - "@actions/core": "^3.0.1", - "@actions/tool-cache": "^4.0.0" + "@actions/core": "^3.0.1" }, "devDependencies": { "@tsconfig/bun": "^1.0.10", diff --git a/src/main.test.ts b/src/main.test.ts index b830817..acca3b9 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -1,30 +1,26 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import process from "node:process"; -import { fileURLToPath } from "node:url"; import { afterEach, expect, mock, spyOn, test } from "bun:test"; import * as core from "@actions/core"; -import * as tc from "@actions/tool-cache"; -const repo = path.dirname(path.dirname(fileURLToPath(import.meta.url))); -const defaultEntrypoint = fileURLToPath(new URL("./main.ts", import.meta.url)); const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY"; -const GITHUB_RELEASES_API = "https://api.github.com/repos/supabase/cli/releases/latest"; -const GITHUB_TOKEN_ENV = "SUPABASE_CLI_GITHUB_TOKEN"; +const originalPath = process.env.PATH; +const originalRunnerTemp = process.env.RUNNER_TEMP; const originalWorkspace = process.env.GITHUB_WORKSPACE; -const originalGithubToken = process.env[GITHUB_TOKEN_ENV]; const tempDirs = new Set(); let mainModule: typeof import("./main.ts") | null = null; afterEach(() => { mock.restore(); + process.env.PATH = originalPath; + process.env.RUNNER_TEMP = originalRunnerTemp; process.env.GITHUB_WORKSPACE = originalWorkspace; - if (originalGithubToken === undefined) { - delete process.env[GITHUB_TOKEN_ENV]; - } else { - process.env[GITHUB_TOKEN_ENV] = originalGithubToken; - } + delete process.env.FAKE_CLI_VERSION; + delete process.env.FAKE_NPM_INTEGRITY; + delete process.env.FAKE_NPM_LOG; + delete process.env.SUPABASE_SETUP_CLI_NPM; for (const dir of tempDirs) { rmSync(dir, { force: true, recursive: true }); @@ -32,32 +28,14 @@ afterEach(() => { tempDirs.clear(); }); -function createFakeCli(versionOutput: string): string { - const dir = mkdtempSync(path.join(os.tmpdir(), "setup-cli-")); +function createTempDir(prefix: string): string { + const dir = mkdtempSync(path.join(os.tmpdir(), prefix)); tempDirs.add(dir); - - if (process.platform === "win32") { - writeFileSync( - path.join(dir, "supabase.cmd"), - versionOutput ? `@echo off\r\necho ${versionOutput}\r\n` : "@echo off\r\n", - ); - return dir; - } - - const escapedOutput = versionOutput.replaceAll("'", "'\"'\"'"); - writeFileSync( - path.join(dir, "supabase"), - versionOutput - ? `#!/usr/bin/env bash\nprintf '%s\\n' '${escapedOutput}'\n` - : "#!/usr/bin/env bash\n", - ); - Bun.spawnSync(["chmod", "+x", path.join(dir, "supabase")]); return dir; } function createWorkspace(files: Record): string { - const dir = mkdtempSync(path.join(os.tmpdir(), "setup-cli-workspace-")); - tempDirs.add(dir); + const dir = createTempDir("setup-cli-workspace-"); for (const [relativePath, content] of Object.entries(files)) { const filePath = path.join(dir, relativePath); @@ -73,6 +51,7 @@ function createBunLock( options: { includeDependency?: boolean; includePackageEntry?: boolean; + integrity?: string; useDevDependency?: boolean; } = {}, ): string { @@ -98,7 +77,7 @@ ${ "supabase@${version}", "", {}, - "sha512-test" + "${options.integrity ?? "sha512-bun"}" ]` : "" } @@ -109,7 +88,12 @@ ${ function createPnpmLock( version: string, - options: { asString?: boolean; includeVersion?: boolean; useDevDependency?: boolean } = {}, + options: { + asString?: boolean; + includeVersion?: boolean; + integrity?: string; + useDevDependency?: boolean; + } = {}, ): string { const dependencyKey = options.useDevDependency ? "devDependencies" : "dependencies"; @@ -127,11 +111,11 @@ ${options.includeVersion === false ? "" : ` version: ${version}`}` packages: supabase@${version}: resolution: - integrity: sha512-test + integrity: ${options.integrity ?? "sha512-pnpm"} `; } -function createPackageLock(version: string): string { +function createPackageLock(version: string, integrity = "sha512-package-lock"): string { return JSON.stringify( { name: "app", @@ -143,6 +127,7 @@ function createPackageLock(version: string): string { }, }, "node_modules/supabase": { + integrity, version, }, }, @@ -152,29 +137,105 @@ function createPackageLock(version: string): string { ); } -function createActionSpies(inputVersion: string, cliDir: string, expectedUrlFragment: string) { - return { - getInput: spyOn(core, "getInput").mockReturnValue(inputVersion), - setOutput: spyOn(core, "setOutput").mockImplementation(() => {}), - addPath: spyOn(core, "addPath").mockImplementation(() => {}), - exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}), - setFailed: spyOn(core, "setFailed").mockImplementation(() => {}), - downloadTool: spyOn(tc, "downloadTool").mockImplementation(async (url: string) => { - expect(url).toContain(expectedUrlFragment); - return path.join(os.tmpdir(), "supabase-cli.tar.gz"); - }), - extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir), - extractZip: spyOn(tc, "extractZip").mockImplementation(async () => cliDir), - }; +function createFakeNpm(): string { + const root = createTempDir("setup-cli-fake-npm-"); + const binDir = path.join(root, "bin"); + const scriptPath = path.join(root, "fake-npm.js"); + mkdirSync(binDir, { recursive: true }); + writeFileSync( + scriptPath, + `import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +const args = process.argv.slice(2); +appendFileSync(process.env.FAKE_NPM_LOG, JSON.stringify(args) + "\\n"); + +if (args[0] === "view") { + console.log(JSON.stringify(process.env.FAKE_NPM_INTEGRITY ?? "sha512-test")); + process.exit(0); } -function mockLatestRelease(version = "v2.99.0") { - return spyOn(globalThis, "fetch").mockResolvedValue( - new Response(JSON.stringify({ tag_name: version }), { - status: 200, - statusText: "OK", - }), +if (args[0] !== "install") { + console.error("Unexpected npm command: " + args.join(" ")); + process.exit(1); +} + +const prefixIndex = args.indexOf("--prefix"); +const prefix = prefixIndex === -1 ? undefined : args[prefixIndex + 1]; +if (!prefix) { + console.error("Missing --prefix"); + process.exit(1); +} + +const binDir = path.join(prefix, "node_modules", ".bin"); +mkdirSync(binDir, { recursive: true }); + +if (process.platform === "win32") { + writeFileSync( + path.join(binDir, "supabase.cmd"), + process.env.FAKE_CLI_VERSION ? "@echo off\\r\\necho " + process.env.FAKE_CLI_VERSION + "\\r\\n" : "@echo off\\r\\n", ); +} else { + writeFileSync( + path.join(binDir, "supabase"), + process.env.FAKE_CLI_VERSION + ? "#!/usr/bin/env bash\\nprintf '%s\\\\n' '" + process.env.FAKE_CLI_VERSION.replaceAll("'", "'\\\\''") + "'\\n" + : "#!/usr/bin/env bash\\n", + { mode: 0o755 }, + ); +} +`, + ); + + if (process.platform === "win32") { + writeFileSync( + path.join(binDir, "npm.cmd"), + `@echo off\r\n"${process.execPath}" "${scriptPath}" %*\r\n`, + ); + } else { + writeFileSync( + path.join(binDir, "npm"), + `#!/usr/bin/env bash\nexec "${process.execPath}" "${scriptPath}" "$@"\n`, + { mode: 0o755 }, + ); + } + + return binDir; +} + +function installFakeNpm(versionOutput = "supabase 2.101.0", integrity = "sha512-test"): string { + const binDir = createFakeNpm(); + const logPath = path.join(createTempDir("setup-cli-fake-npm-log-"), "npm.log"); + writeFileSync(logPath, ""); + process.env.FAKE_CLI_VERSION = versionOutput; + process.env.FAKE_NPM_INTEGRITY = integrity; + process.env.FAKE_NPM_LOG = logPath; + process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ""}`; + process.env.RUNNER_TEMP = createTempDir("setup-cli-runner-temp-"); + process.env.SUPABASE_SETUP_CLI_NPM = path.join( + binDir, + process.platform === "win32" ? "npm.cmd" : "npm", + ); + + return logPath; +} + +function readNpmCalls(logPath: string): string[][] { + return readFileSync(logPath, "utf8") + .trim() + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as string[]); +} + +function createActionSpies(inputVersion: string) { + return { + addPath: spyOn(core, "addPath").mockImplementation(() => {}), + exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}), + getInput: spyOn(core, "getInput").mockReturnValue(inputVersion), + setFailed: spyOn(core, "setFailed").mockImplementation(() => {}), + setOutput: spyOn(core, "setOutput").mockImplementation(() => {}), + }; } async function getMainModule(): Promise { @@ -185,193 +246,52 @@ async function getMainModule(): Promise { return mainModule; } -test("uses versioned tar archives for Supabase CLI v2.99.0 and later", async () => { - const { getDownloadArchive } = await getMainModule(); +test("uses an explicit npm package version when provided", async () => { + const { resolvePackage } = await getMainModule(); - const archive = await getDownloadArchive("2.99.0", "linux", "x64"); - - expect(archive).toEqual({ - url: "https://github.com/supabase/cli/releases/download/v2.99.0/supabase_2.99.0_linux_amd64.tar.gz", - format: "tar", + expect(resolvePackage("v2.101.0")).toEqual({ + spec: "supabase@2.101.0", + version: "2.101.0", }); }); -test("uses apk archives for Supabase CLI v2.99.0 and later on Linux musl", async () => { - const { getDownloadArchive } = await getMainModule(); - - const archive = await getDownloadArchive("2.100.1", "linux", "x64", true); - - expect(archive).toEqual({ - url: "https://github.com/supabase/cli/releases/download/v2.100.1/supabase_2.100.1_linux_amd64.apk", - format: "apk", - }); -}); - -test("keeps tar archives before Supabase CLI v2.99.0 on Linux musl", async () => { - const { getDownloadArchive } = await getMainModule(); - - const archive = await getDownloadArchive("2.98.2", "linux", "x64", true); - - expect(archive).toEqual({ - url: "https://github.com/supabase/cli/releases/download/v2.98.2/supabase_linux_amd64.tar.gz", - format: "tar", - }); -}); - -test("uses usr/bin as the CLI path for apk archives", async () => { - const { getCliPath } = await getMainModule(); - - expect(getCliPath("/tmp/extracted", "apk")).toBe(path.join("/tmp/extracted", "usr", "bin")); - expect(getCliPath("/tmp/extracted", "tar")).toBe("/tmp/extracted"); - expect(getCliPath("/tmp/extracted", "zip")).toBe("/tmp/extracted"); -}); - -test("keeps the unversioned tar archive layout before Supabase CLI v2.99.0", async () => { - const { getDownloadArchive } = await getMainModule(); - - const archive = await getDownloadArchive("2.98.2", "linux", "x64"); - - expect(archive).toEqual({ - url: "https://github.com/supabase/cli/releases/download/v2.98.2/supabase_linux_amd64.tar.gz", - format: "tar", - }); -}); - -test("uses versioned zip archives for Windows Supabase CLI v2.99.0 and later", async () => { - const { getDownloadArchive } = await getMainModule(); - - const archive = await getDownloadArchive("2.99.0", "win32", "x64"); - - expect(archive).toEqual({ - url: "https://github.com/supabase/cli/releases/download/v2.99.0/supabase_2.99.0_windows_amd64.zip", - format: "zip", - }); -}); - -test("resolves latest before choosing a versioned Supabase CLI archive", async () => { - mockLatestRelease("v2.99.0"); - const { getDownloadArchive } = await getMainModule(); - - const archive = await getDownloadArchive("latest", "darwin", "arm64"); - - expect(archive).toEqual({ - url: "https://github.com/supabase/cli/releases/download/v2.99.0/supabase_2.99.0_darwin_arm64.tar.gz", - format: "tar", - }); -}); - -test("authenticates latest release lookup when a GitHub token is provided", async () => { - process.env[GITHUB_TOKEN_ENV] = "ghs_test-token"; - const fetch = mockLatestRelease("v2.99.0"); - const { getDownloadArchive } = await getMainModule(); - - await getDownloadArchive("latest", "darwin", "arm64"); - - expect(fetch).toHaveBeenCalledWith(GITHUB_RELEASES_API, { - headers: expect.objectContaining({ - Accept: "application/vnd.github+json", - Authorization: "Bearer ghs_test-token", - "X-GitHub-Api-Version": "2022-11-28", - }), - }); -}); - -test("awaits the action entrypoint with omitted version and latest fallback", async () => { - process.env.GITHUB_WORKSPACE = repo; - mockLatestRelease(); - const cliDir = createFakeCli("supabase 2.84.2"); - let startDownload!: () => void; - let finishDownload!: () => void; - const downloadStarted = new Promise((resolve) => { - startDownload = resolve; - }); - const downloadFinished = new Promise((resolve) => { - finishDownload = () => resolve(path.join(os.tmpdir(), "supabase-cli.tar.gz")); - }); - const spies = { - getInput: spyOn(core, "getInput").mockReturnValue(""), - setOutput: spyOn(core, "setOutput").mockImplementation(() => {}), - addPath: spyOn(core, "addPath").mockImplementation(() => {}), - exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}), - setFailed: spyOn(core, "setFailed").mockImplementation(() => {}), - downloadTool: spyOn(tc, "downloadTool").mockImplementation(async (url: string) => { - expect(url).toContain("/download/v2.99.0/supabase_2.99.0_"); - startDownload(); - return downloadFinished; - }), - extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir), - extractZip: spyOn(tc, "extractZip").mockImplementation(async () => cliDir), - }; - const originalArgv1 = process.argv[1]; - process.argv[1] = defaultEntrypoint; - - try { - let importSettled = false; - const entrypoint = import(`./main.ts?entrypoint=${Date.now()}`).finally(() => { - importSettled = true; - }); - - await downloadStarted; - await Bun.sleep(0); - - expect(importSettled).toBe(false); - - finishDownload(); - await entrypoint; - } finally { - process.argv[1] = originalArgv1 ?? ""; - } - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2"); - expect(spies.addPath).toHaveBeenCalledWith(cliDir); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); -}); - -test("uses the root bun.lock version when version is omitted", async () => { +test("uses the root bun.lock resolution when version is omitted", async () => { process.env.GITHUB_WORKSPACE = createWorkspace({ - "bun.lock": createBunLock("2.41.0"), + "bun.lock": createBunLock("2.41.0", { integrity: "sha512-bun-lock" }), }); - const cliDir = createFakeCli("supabase 2.41.0"); - const spies = createActionSpies("", cliDir, "/download/v2.41.0/supabase_"); - const { run } = await getMainModule(); + const { resolvePackage } = await getMainModule(); - await run(); - - expect(spies.downloadTool).not.toHaveBeenCalledWith(expect.stringContaining("/latest/download/")); - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.41.0"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); + expect(resolvePackage("")).toEqual({ + integrity: "sha512-bun-lock", + spec: "supabase@2.41.0", + version: "2.41.0", + }); }); -test("uses the root pnpm-lock.yaml version when version is omitted", async () => { +test("uses the root pnpm-lock.yaml resolution when version is omitted", async () => { process.env.GITHUB_WORKSPACE = createWorkspace({ - "pnpm-lock.yaml": createPnpmLock("2.42.0"), + "pnpm-lock.yaml": createPnpmLock("2.42.0", { integrity: "sha512-pnpm-lock" }), }); - const cliDir = createFakeCli("supabase 2.42.0"); - const spies = createActionSpies("", cliDir, "/download/v2.42.0/supabase_"); - const { run } = await getMainModule(); + const { resolvePackage } = await getMainModule(); - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.42.0"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); + expect(resolvePackage("")).toEqual({ + integrity: "sha512-pnpm-lock", + spec: "supabase@2.42.0", + version: "2.42.0", + }); }); -test("uses the root package-lock.json version when version is omitted", async () => { +test("uses the root package-lock.json resolution when version is omitted", async () => { process.env.GITHUB_WORKSPACE = createWorkspace({ - "package-lock.json": createPackageLock("2.43.0"), + "package-lock.json": createPackageLock("2.43.0", "sha512-package-lock"), }); - const cliDir = createFakeCli("supabase 2.43.0"); - const spies = createActionSpies("", cliDir, "/download/v2.43.0/supabase_"); - const { run } = await getMainModule(); + const { resolvePackage } = await getMainModule(); - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.43.0"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); + expect(resolvePackage("")).toEqual({ + integrity: "sha512-package-lock", + spec: "supabase@2.43.0", + version: "2.43.0", + }); }); test("falls through malformed lockfiles and uses the next supported root lockfile", async () => { @@ -379,60 +299,47 @@ test("falls through malformed lockfiles and uses the next supported root lockfil "bun.lock": "{ not valid", "package-lock.json": createPackageLock("2.44.0"), }); - const cliDir = createFakeCli("supabase 2.44.0"); - const spies = createActionSpies("", cliDir, "/download/v2.44.0/supabase_"); - const { run } = await getMainModule(); + const { resolvePackage } = await getMainModule(); - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.44.0"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); + expect(resolvePackage("")).toEqual({ + integrity: "sha512-package-lock", + spec: "supabase@2.44.0", + version: "2.44.0", + }); }); test("falls back to latest when version is omitted and no supported root lockfile is present", async () => { process.env.GITHUB_WORKSPACE = createWorkspace({ "README.md": "# app\n", }); - mockLatestRelease(); - const cliDir = createFakeCli("supabase 2.84.2"); - const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_"); - const { run } = await getMainModule(); + const { resolvePackage } = await getMainModule(); - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); + expect(resolvePackage("")).toEqual({ + spec: "supabase@latest", + version: "latest", + }); }); test("falls back to latest when version is omitted and no workspace is available", async () => { delete process.env.GITHUB_WORKSPACE; - mockLatestRelease(); - const cliDir = createFakeCli("supabase 2.84.2"); - const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_"); - const { run } = await getMainModule(); + const { resolvePackage } = await getMainModule(); - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); + expect(resolvePackage("")).toEqual({ + spec: "supabase@latest", + version: "latest", + }); }); test("uses the declared bun.lock version when the resolved package entry is missing", async () => { process.env.GITHUB_WORKSPACE = createWorkspace({ "bun.lock": createBunLock("2.44.1", { includePackageEntry: false, useDevDependency: true }), }); - const cliDir = createFakeCli("supabase 2.44.1"); - const spies = createActionSpies("", cliDir, "/download/v2.44.1/supabase_"); - const { run } = await getMainModule(); + const { resolvePackage } = await getMainModule(); - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.44.1"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); + expect(resolvePackage("")).toEqual({ + spec: "supabase@2.44.1", + version: "2.44.1", + }); }); test("falls through bun.lock without supabase and uses a pnpm string dependency version", async () => { @@ -440,73 +347,119 @@ test("falls through bun.lock without supabase and uses a pnpm string dependency "bun.lock": createBunLock("2.47.0", { includeDependency: false }), "pnpm-lock.yaml": createPnpmLock("2.47.0", { asString: true }), }); - const cliDir = createFakeCli("supabase 2.47.0"); - const spies = createActionSpies("", cliDir, "/download/v2.47.0/supabase_"); - const { run } = await getMainModule(); + const { resolvePackage } = await getMainModule(); - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.47.0"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); -}); - -test("falls through malformed pnpm lockfiles and uses the next supported root lockfile", async () => { - process.env.GITHUB_WORKSPACE = createWorkspace({ - "pnpm-lock.yaml": "not: [valid", - "package-lock.json": createPackageLock("2.48.0"), + expect(resolvePackage("")).toEqual({ + integrity: "sha512-pnpm", + spec: "supabase@2.47.0", + version: "2.47.0", }); - const cliDir = createFakeCli("supabase 2.48.0"); - const spies = createActionSpies("", cliDir, "/download/v2.48.0/supabase_"); - const { run } = await getMainModule(); - - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.48.0"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); -}); - -test("falls through unreadable bun.lock paths and malformed package-lock files to latest", async () => { - const workspace = createWorkspace({ - "package-lock.json": "{ invalid", - }); - mkdirSync(path.join(workspace, "bun.lock"), { recursive: true }); - process.env.GITHUB_WORKSPACE = workspace; - mockLatestRelease(); - const cliDir = createFakeCli("supabase 2.84.2"); - const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_"); - const { run } = await getMainModule(); - - await run(); - - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2"); - expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); - expect(spies.setFailed).not.toHaveBeenCalled(); }); test("falls back to latest when a pnpm dependency entry has no concrete version", async () => { process.env.GITHUB_WORKSPACE = createWorkspace({ "pnpm-lock.yaml": createPnpmLock("2.49.0", { includeVersion: false }), }); - mockLatestRelease(); - const cliDir = createFakeCli("supabase 2.84.2"); - const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_"); + const { resolvePackage } = await getMainModule(); + + expect(resolvePackage("")).toEqual({ + spec: "supabase@latest", + version: "latest", + }); +}); + +test("installs the CLI with npm into an isolated prefix", async () => { + const logPath = installFakeNpm(); + const { installCli } = await getMainModule(); + + const cliPath = await installCli({ + spec: "supabase@2.101.0", + version: "2.101.0", + }); + + expect(cliPath).toContain(`${path.sep}node_modules${path.sep}.bin`); + expect(readNpmCalls(logPath)).toEqual([ + [ + "install", + "--prefix", + expect.any(String), + "--omit=dev", + "--no-audit", + "--no-fund", + "--no-package-lock", + "--ignore-scripts", + "supabase@2.101.0", + ], + ]); +}); + +test("verifies lockfile integrity before installing", async () => { + const logPath = installFakeNpm("supabase 2.101.0", "sha512-lock"); + const { installCli } = await getMainModule(); + + await installCli({ + integrity: "sha512-lock", + spec: "supabase@2.101.0", + version: "2.101.0", + }); + + expect(readNpmCalls(logPath)).toEqual([ + ["view", "supabase@2.101.0", "dist.integrity", "--json"], + [ + "install", + "--prefix", + expect.any(String), + "--omit=dev", + "--no-audit", + "--no-fund", + "--no-package-lock", + "--ignore-scripts", + "supabase@2.101.0", + ], + ]); +}); + +test("fails when lockfile integrity does not match the registry", async () => { + installFakeNpm("supabase 2.101.0", "sha512-registry"); + const { installCli } = await getMainModule(); + + try { + await installCli({ + integrity: "sha512-lock", + spec: "supabase@2.101.0", + version: "2.101.0", + }); + throw new Error("Expected installCli to reject"); + } catch (error) { + expect(error).toEqual( + new Error("Lockfile integrity for supabase@2.101.0 does not match the npm registry"), + ); + } +}); + +test("runs the action with a package-lock resolution", async () => { + const logPath = installFakeNpm("supabase 2.43.0", "sha512-package-lock"); + process.env.GITHUB_WORKSPACE = createWorkspace({ + "package-lock.json": createPackageLock("2.43.0", "sha512-package-lock"), + }); + const spies = createActionSpies(""); const { run } = await getMainModule(); await run(); - expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2"); + expect(readNpmCalls(logPath)[0]).toEqual(["view", "supabase@2.43.0", "dist.integrity", "--json"]); + expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.43.0"); + expect(spies.addPath).toHaveBeenCalledWith(expect.stringContaining("node_modules")); expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io"); expect(spies.setFailed).not.toHaveBeenCalled(); }); test("explicit version overrides detected root lockfiles", async () => { + installFakeNpm("supabase 1.0.0"); process.env.GITHUB_WORKSPACE = createWorkspace({ "bun.lock": createBunLock("2.45.0"), }); - const cliDir = createFakeCli("supabase 1.0.0"); - const spies = createActionSpies("1.0.0", cliDir, "/download/v1.0.0/supabase_1.0.0_"); + const spies = createActionSpies("1.0.0"); const { run } = await getMainModule(); await run(); @@ -517,11 +470,11 @@ test("explicit version overrides detected root lockfiles", async () => { }); test("fails when the installed CLI does not report a version", async () => { + installFakeNpm(""); process.env.GITHUB_WORKSPACE = createWorkspace({ - "package-lock.json": createPackageLock("2.46.0"), + "package-lock.json": createPackageLock("2.46.0", "sha512-test"), }); - const cliDir = createFakeCli(""); - const spies = createActionSpies("", cliDir, "/download/v2.46.0/supabase_"); + const spies = createActionSpies(""); const { run } = await getMainModule(); await run(); diff --git a/src/main.ts b/src/main.ts index 1f59bfb..f812e8c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,22 +1,20 @@ -import { $, semver } from "bun"; +import { semver } from "bun"; import * as core from "@actions/core"; -import * as tc from "@actions/tool-cache"; -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync } from "node:fs"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; export const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY"; const REGISTRY_VERSION = "1.28.0"; -const VERSIONED_ARCHIVE_VERSION = "2.99.0"; const DEFAULT_VERSION = "latest"; -const GITHUB_RELEASES_API = "https://api.github.com/repos/supabase/cli/releases/latest"; -const GITHUB_TOKEN_ENV = "SUPABASE_CLI_GITHUB_TOKEN"; +const NPM_PACKAGE = "supabase"; +const NPM_EXECUTABLE_ENV = "SUPABASE_SETUP_CLI_NPM"; -type ArchiveFormat = "apk" | "tar" | "zip"; - -type DownloadArchive = { - url: string; - format: ArchiveFormat; +type PackageResolution = { + spec: string; + version: string; + integrity?: string; }; type BunLock = { @@ -35,6 +33,12 @@ type PnpmDependency = version?: string; }; +type PnpmPackage = { + resolution?: { + integrity?: string; + }; +}; + type PnpmLock = { importers?: { ".": { @@ -42,21 +46,14 @@ type PnpmLock = { devDependencies?: Record; }; }; + packages?: Record; }; type PackageLock = { - packages?: Record; - dependencies?: Record; + packages?: Record; + dependencies?: Record; }; -function getArchivePlatform(platform: NodeJS.Platform): string { - return platform === "win32" ? "windows" : platform; -} - -function getArchiveArch(arch: NodeJS.Architecture): string { - return arch === "x64" ? "amd64" : arch; -} - function extractConcreteVersion(raw: string | undefined): string | null { if (!raw) { return null; @@ -70,6 +67,16 @@ function normalizeVersion(version: string): string { return version.replace(/^v/i, ""); } +function toPackageResolution(version: string, integrity?: string): PackageResolution { + const normalizedVersion = normalizeVersion(version); + + return { + spec: `${NPM_PACKAGE}@${normalizedVersion}`, + version: normalizedVersion, + integrity, + }; +} + function readWorkspaceLockfile(workspaceRoot: string, filename: string): string | null { const filePath = path.join(workspaceRoot, filename); @@ -84,7 +91,7 @@ function readWorkspaceLockfile(workspaceRoot: string, filename: string): string } } -function detectVersionFromBunLock(workspaceRoot: string): string | null { +function detectResolutionFromBunLock(workspaceRoot: string): PackageResolution | null { const text = readWorkspaceLockfile(workspaceRoot, "bun.lock"); if (!text) { @@ -95,24 +102,28 @@ function detectVersionFromBunLock(workspaceRoot: string): string | null { const lockfile = JSON.parse(text.replace(/,\s*([}\]])/g, "$1")) as BunLock; const rootWorkspace = lockfile.workspaces?.[""]; const declaredVersion = - rootWorkspace?.dependencies?.supabase ?? rootWorkspace?.devDependencies?.supabase; + rootWorkspace?.dependencies?.[NPM_PACKAGE] ?? rootWorkspace?.devDependencies?.[NPM_PACKAGE]; if (!declaredVersion) { return null; } - const resolvedPackage = lockfile.packages?.supabase; + const resolvedPackage = lockfile.packages?.[NPM_PACKAGE]; if (Array.isArray(resolvedPackage) && typeof resolvedPackage[0] === "string") { - return extractConcreteVersion(resolvedPackage[0]); + const version = extractConcreteVersion(resolvedPackage[0]); + const integrity = typeof resolvedPackage[3] === "string" ? resolvedPackage[3] : undefined; + + return version ? toPackageResolution(version, integrity) : null; } - return extractConcreteVersion(declaredVersion); + const version = extractConcreteVersion(declaredVersion); + return version ? toPackageResolution(version) : null; } catch { return null; } } -function detectVersionFromPnpmLock(workspaceRoot: string): string | null { +function detectResolutionFromPnpmLock(workspaceRoot: string): PackageResolution | null { const text = readWorkspaceLockfile(workspaceRoot, "pnpm-lock.yaml"); if (!text) { @@ -123,19 +134,29 @@ function detectVersionFromPnpmLock(workspaceRoot: string): string | null { const lockfile = Bun.YAML.parse(text) as PnpmLock; const rootImporter = lockfile.importers?.["."]; const dependency = - rootImporter?.dependencies?.supabase ?? rootImporter?.devDependencies?.supabase; + rootImporter?.dependencies?.[NPM_PACKAGE] ?? rootImporter?.devDependencies?.[NPM_PACKAGE]; + const version = + typeof dependency === "string" + ? extractConcreteVersion(dependency) + : extractConcreteVersion(dependency?.version); - if (typeof dependency === "string") { - return extractConcreteVersion(dependency); + if (!version) { + return null; } - return extractConcreteVersion(dependency?.version); + const integrity = Object.entries(lockfile.packages ?? {}).find( + ([packageKey]) => + packageKey === `${NPM_PACKAGE}@${version}` || + packageKey.startsWith(`/${NPM_PACKAGE}@${version}`), + )?.[1].resolution?.integrity; + + return toPackageResolution(version, integrity); } catch { return null; } } -function detectVersionFromPackageLock(workspaceRoot: string): string | null { +function detectResolutionFromPackageLock(workspaceRoot: string): PackageResolution | null { const text = readWorkspaceLockfile(workspaceRoot, "package-lock.json"); if (!text) { @@ -144,147 +165,97 @@ function detectVersionFromPackageLock(workspaceRoot: string): string | null { try { const lockfile = JSON.parse(text) as PackageLock; + const packageEntry = lockfile.packages?.[`node_modules/${NPM_PACKAGE}`]; + const dependencyEntry = lockfile.dependencies?.[NPM_PACKAGE]; + const version = + extractConcreteVersion(packageEntry?.version) ?? + extractConcreteVersion(dependencyEntry?.version); - return ( - extractConcreteVersion(lockfile.packages?.["node_modules/supabase"]?.version) ?? - extractConcreteVersion(lockfile.dependencies?.supabase?.version) - ); + return version + ? toPackageResolution(version, packageEntry?.integrity ?? dependencyEntry?.integrity) + : null; } catch { return null; } } -function resolveVersion(inputVersion: string): string { +export function resolvePackage(inputVersion: string): PackageResolution { const requestedVersion = inputVersion.trim(); if (requestedVersion) { - return requestedVersion; + return toPackageResolution(requestedVersion); } const workspaceRoot = process.env.GITHUB_WORKSPACE?.trim(); if (!workspaceRoot) { - return DEFAULT_VERSION; + return toPackageResolution(DEFAULT_VERSION); } return ( - detectVersionFromBunLock(workspaceRoot) ?? - detectVersionFromPnpmLock(workspaceRoot) ?? - detectVersionFromPackageLock(workspaceRoot) ?? - DEFAULT_VERSION + detectResolutionFromBunLock(workspaceRoot) ?? + detectResolutionFromPnpmLock(workspaceRoot) ?? + detectResolutionFromPackageLock(workspaceRoot) ?? + toPackageResolution(DEFAULT_VERSION) ); } -async function resolveLatestVersion(): Promise { - const headers: Record = { - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }; - const githubToken = process.env[GITHUB_TOKEN_ENV]?.trim(); - - if (githubToken) { - headers.Authorization = `Bearer ${githubToken}`; +async function verifyExpectedIntegrity(resolution: PackageResolution): Promise { + if (!resolution.integrity) { + return; } - const response = await fetch(GITHUB_RELEASES_API, { headers }); - if (!response.ok) { - throw new Error(`Failed to resolve latest Supabase CLI release: ${response.statusText}`); - } + const output = await runNpm(["view", resolution.spec, "dist.integrity", "--json"]); + const registryIntegrity = JSON.parse(output) as unknown; - const release = (await response.json()) as { tag_name?: unknown }; - if (typeof release.tag_name !== "string") { - throw new Error("Failed to resolve latest Supabase CLI release: missing tag name"); - } - - return normalizeVersion(release.tag_name); -} - -function getArchiveFormat( - version: string, - platform: NodeJS.Platform, - isMuslLinux: boolean, -): ArchiveFormat { - if ( - platform === "linux" && - isMuslLinux && - semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0 - ) { - return "apk"; - } - - if (platform === "win32" && semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0) { - return "zip"; - } - - return "tar"; -} - -function getArchiveFilename( - version: string, - platform: NodeJS.Platform, - arch: NodeJS.Architecture, - archiveFormat: ArchiveFormat, -): string { - const archivePlatform = getArchivePlatform(platform); - const archiveArch = getArchiveArch(arch); - - if (semver.order(version, REGISTRY_VERSION) === -1) { - return `supabase_${version}_${archivePlatform}_${archiveArch}.tar.gz`; - } - - if (platform === "linux" && archiveFormat === "apk") { - return `supabase_${version}_${archivePlatform}_${archiveArch}.apk`; - } - - if (semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0) { - const extension = platform === "win32" ? "zip" : "tar.gz"; - return `supabase_${version}_${archivePlatform}_${archiveArch}.${extension}`; - } - - return `supabase_${archivePlatform}_${archiveArch}.tar.gz`; -} - -export async function getDownloadArchive( - version: string, - platform = process.platform, - arch = process.arch, - isMuslLinux?: boolean, -): Promise { - const resolvedVersion = - version.toLowerCase() === "latest" ? await resolveLatestVersion() : normalizeVersion(version); - const format = getArchiveFormat( - resolvedVersion, - platform, - isMuslLinux ?? (await detectMuslLinux(platform)), - ); - const filename = getArchiveFilename(resolvedVersion, platform, arch, format); - - return { - url: `https://github.com/supabase/cli/releases/download/v${resolvedVersion}/${filename}`, - format, - }; -} - -async function detectMuslLinux(platform = process.platform): Promise { - if (platform !== "linux") { - return false; - } - - if (existsSync("/etc/alpine-release")) { - return true; - } - - try { - const output = await $`ldd --version`.quiet().text(); - return output.toLowerCase().includes("musl"); - } catch (error) { - const output = error instanceof Error ? error.message : String(error); - return output.toLowerCase().includes("musl"); + if (registryIntegrity !== resolution.integrity) { + throw new Error(`Lockfile integrity for ${resolution.spec} does not match the npm registry`); } } -export function getCliPath(extractedPath: string, archiveFormat: ArchiveFormat): string { - return archiveFormat === "apk" ? path.join(extractedPath, "usr", "bin") : extractedPath; +function createInstallRoot(): string { + const tempRoot = process.env.RUNNER_TEMP?.trim() || os.tmpdir(); + return mkdtempSync(path.join(tempRoot, "setup-cli-")); +} + +async function runNpm(args: string[]): Promise { + const executable = process.env[NPM_EXECUTABLE_ENV]?.trim() || "npm"; + const proc = Bun.spawn([executable, ...args], { + env: process.env, + stderr: "pipe", + stdout: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + if (exitCode !== 0) { + throw new Error(stderr.trim() || `npm ${args.join(" ")} failed`); + } + + return stdout; +} + +export async function installCli(resolution: PackageResolution): Promise { + await verifyExpectedIntegrity(resolution); + + const installRoot = createInstallRoot(); + + await runNpm([ + "install", + "--prefix", + installRoot, + "--omit=dev", + "--no-audit", + "--no-fund", + "--no-package-lock", + "--ignore-scripts", + resolution.spec, + ]); + + return path.join(installRoot, "node_modules", ".bin"); } function getCliExecutablePath(cliPath: string): string { @@ -292,21 +263,36 @@ function getCliExecutablePath(cliPath: string): string { return path.join(cliPath, "supabase"); } - const exePath = path.join(cliPath, "supabase.exe"); - if (existsSync(exePath)) { - return exePath; - } - const cmdPath = path.join(cliPath, "supabase.cmd"); if (existsSync(cmdPath)) { return cmdPath; } + const exePath = path.join(cliPath, "supabase.exe"); + if (existsSync(exePath)) { + return exePath; + } + return path.join(cliPath, "supabase"); } export async function determineInstalledVersion(cliPath: string): Promise { - const version = (await $`${getCliExecutablePath(cliPath)} --version`.text()).trim(); + const executable = getCliExecutablePath(cliPath); + const proc = Bun.spawn([executable, "--version"], { + stderr: "pipe", + stdout: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + if (exitCode !== 0) { + throw new Error(stderr.trim() || "Could not determine installed Supabase CLI version"); + } + + const version = stdout.trim(); if (!version) { throw new Error("Could not determine installed Supabase CLI version"); } @@ -314,21 +300,24 @@ export async function determineInstalledVersion(cliPath: string): Promise= 0; +} + export async function run(): Promise { try { - const version = resolveVersion(core.getInput("version")); - const archive = await getDownloadArchive(version); - const archivePath = await tc.downloadTool(archive.url); - const extractedPath = - archive.format === "zip" - ? await tc.extractZip(archivePath) - : await tc.extractTar(archivePath); - const cliPath = getCliPath(extractedPath, archive.format); + const resolution = resolvePackage(core.getInput("version")); + const cliPath = await installCli(resolution); const installedVersion = await determineInstalledVersion(cliPath); core.setOutput("version", installedVersion); core.addPath(cliPath); - if (version.toLowerCase() === "latest" || semver.order(version, REGISTRY_VERSION) >= 0) { + if (shouldUseGhcrRegistry(resolution.version, installedVersion)) { core.exportVariable(CLI_CONFIG_REGISTRY, "ghcr.io"); } } catch (error) {