mirror of
https://github.com/supabase/setup-cli.git
synced 2026-05-13 11:26:59 +00:00
chore: prepare for v2.0.0 (#405)
## Summary This PR prepares `supabase/setup-cli` for `v2.0.0`. The main goal of this release is to simplify the action and modernize the repo/tooling around a Bun-based implementation, while tightening workflows, tests, and documentation. ## What Changed ### Action runtime - switched the action from a Node/compiled `dist` runtime to a Bun-based composite action - removed the checked-in `dist/` output entirely - simplified the action source down to a single runtime file in `src/main.ts` - kept the public action interface the same: - `with.version` - `outputs.version` ### Tooling - switched package management and local tooling from npm to Bun - removed Rollup and the build step - replaced Jest with Bun’s native test runner - replaced Prettier with `oxfmt` - replaced ESLint with `oxlint` - enabled type-aware/type-check linting with `oxlint-tsgolint` - simplified TypeScript config to a single `tsconfig.json` extending `@tsconfig/bun` ### Tests - moved tests next to the runtime source - rewrote tests to focus on meaningful user-facing action behavior - added coverage for: - default entrypoint execution - latest version installs - legacy version installs - modern pinned version installs - failure when the installed CLI cannot report a version - action code coverage is now `100%` ### Workflows - renamed workflow files for clarity: - `test.yml` -> `ci.yml` - `start.yml` -> `e2e.yml` - updated workflow/job naming so required checks are clean and stable: - `CI` - `E2E` - `CodeQL` - `Licensed` - added aggregate PR-facing checks so branch protection does not need matrix legs - made CI and E2E skip heavy jobs on draft PRs - made E2E run automatically on ready PRs and new commits - simplified CodeQL config by removing the separate config file - updated action pins to current releases using commit SHAs - refined Dependabot for Bun-era updates and non-major auto-merge ### Docs - refreshed `README.md` and `docs/index.md` for the new v2 behavior - updated examples to use `@v2` - added a practical example for exporting local Supabase env vars after `supabase start` - removed stale references to old local/dev flows ## Breaking / Notable Changes - the action now runs as a Bun-based composite action instead of a prebuilt JavaScript action - no checked-in `dist/` artifacts anymore - self-hosted runners now need the prerequisites expected by the composite action path: - `bash` - network access to install Bun/dependencies and download the Supabase CLI ## Validation Verified locally with: - `bun run format:check` - `bun run lint` - `bun test` - `bun run ci` Also updated workflows and branch-protection-friendly check names so PR validation is cleaner going forward. ## Follow-up After merge, branch protection should require only: - `CI` - `E2E` - `CodeQL` - `Licensed` --------- Co-authored-by: licensed-ci <licensed-ci@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* The entrypoint for the action. This file simply imports and runs the action's
|
||||
* main logic.
|
||||
*/
|
||||
import { run } from './main.js'
|
||||
|
||||
/* istanbul ignore next */
|
||||
run()
|
||||
407
src/main.test.ts
Normal file
407
src/main.test.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
import { mkdirSync, mkdtempSync, 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 originalWorkspace = process.env.GITHUB_WORKSPACE;
|
||||
const tempDirs = new Set<string>();
|
||||
let mainModule: typeof import("./main.ts") | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
process.env.GITHUB_WORKSPACE = originalWorkspace;
|
||||
|
||||
for (const dir of tempDirs) {
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
}
|
||||
tempDirs.clear();
|
||||
});
|
||||
|
||||
function createFakeCli(versionOutput: string): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "setup-cli-"));
|
||||
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, string>): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "setup-cli-workspace-"));
|
||||
tempDirs.add(dir);
|
||||
|
||||
for (const [relativePath, content] of Object.entries(files)) {
|
||||
const filePath = path.join(dir, relativePath);
|
||||
mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, content);
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
function createBunLock(
|
||||
version: string,
|
||||
options: {
|
||||
includeDependency?: boolean;
|
||||
includePackageEntry?: boolean;
|
||||
useDevDependency?: boolean;
|
||||
} = {},
|
||||
): string {
|
||||
const includeDependency = options.includeDependency ?? true;
|
||||
const includePackageEntry = options.includePackageEntry ?? true;
|
||||
const dependencyKey = options.useDevDependency ? "devDependencies" : "dependencies";
|
||||
|
||||
return `{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "app",
|
||||
"${dependencyKey}": {
|
||||
${includeDependency ? ` "supabase": "^${version}"` : ""}
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages": {
|
||||
${
|
||||
includePackageEntry
|
||||
? ` "supabase": [
|
||||
"supabase@${version}",
|
||||
"",
|
||||
{},
|
||||
"sha512-test"
|
||||
]`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function createPnpmLock(
|
||||
version: string,
|
||||
options: { asString?: boolean; includeVersion?: boolean; useDevDependency?: boolean } = {},
|
||||
): string {
|
||||
const dependencyKey = options.useDevDependency ? "devDependencies" : "dependencies";
|
||||
|
||||
return `lockfileVersion: "9.0"
|
||||
importers:
|
||||
.:
|
||||
${dependencyKey}:
|
||||
${
|
||||
options.asString
|
||||
? ` supabase: ${version}`
|
||||
: ` supabase:
|
||||
specifier: ^${version}
|
||||
${options.includeVersion === false ? "" : ` version: ${version}`}`
|
||||
}
|
||||
packages:
|
||||
supabase@${version}:
|
||||
resolution:
|
||||
integrity: sha512-test
|
||||
`;
|
||||
}
|
||||
|
||||
function createPackageLock(version: string): string {
|
||||
return JSON.stringify(
|
||||
{
|
||||
name: "app",
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": {
|
||||
dependencies: {
|
||||
supabase: `^${version}`,
|
||||
},
|
||||
},
|
||||
"node_modules/supabase": {
|
||||
version,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
async function getMainModule(): Promise<typeof import("./main.ts")> {
|
||||
if (!mainModule) {
|
||||
mainModule = await import("./main.ts");
|
||||
}
|
||||
|
||||
return mainModule;
|
||||
}
|
||||
|
||||
async function waitForCalls(assertion: () => void): Promise<void> {
|
||||
let failure: Error | null = null;
|
||||
|
||||
for (let attempt = 0; attempt < 50; attempt += 1) {
|
||||
try {
|
||||
assertion();
|
||||
return;
|
||||
} catch (error) {
|
||||
failure = error instanceof Error ? error : new Error(String(error));
|
||||
await Bun.sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
throw failure ?? new Error("Timed out waiting for action side effects");
|
||||
}
|
||||
|
||||
test("runs the action entrypoint with omitted version and latest fallback", async () => {
|
||||
process.env.GITHUB_WORKSPACE = repo;
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/latest/download/");
|
||||
const originalArgv1 = process.argv[1];
|
||||
process.argv[1] = defaultEntrypoint;
|
||||
|
||||
try {
|
||||
await import(`./main.ts?entrypoint=${Date.now()}`);
|
||||
await waitForCalls(() => {
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2");
|
||||
expect(spies.addPath).toHaveBeenCalledWith(cliDir);
|
||||
});
|
||||
} finally {
|
||||
process.argv[1] = originalArgv1 ?? "";
|
||||
}
|
||||
|
||||
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 () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"bun.lock": createBunLock("2.41.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.41.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.41.0/supabase_");
|
||||
const { run } = 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();
|
||||
});
|
||||
|
||||
test("uses the root pnpm-lock.yaml version when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"pnpm-lock.yaml": createPnpmLock("2.42.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.42.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.42.0/supabase_");
|
||||
const { run } = 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();
|
||||
});
|
||||
|
||||
test("uses the root package-lock.json version when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"package-lock.json": createPackageLock("2.43.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.43.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.43.0/supabase_");
|
||||
const { run } = 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();
|
||||
});
|
||||
|
||||
test("falls through malformed lockfiles and uses the next supported root lockfile", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"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();
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/latest/download/");
|
||||
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 version is omitted and no workspace is available", async () => {
|
||||
delete process.env.GITHUB_WORKSPACE;
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/latest/download/");
|
||||
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("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();
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test("falls through bun.lock without supabase and uses a pnpm string dependency version", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"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();
|
||||
|
||||
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"),
|
||||
});
|
||||
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;
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/latest/download/");
|
||||
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 }),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/latest/download/");
|
||||
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("explicit version overrides detected root lockfiles", async () => {
|
||||
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 { run } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 1.0.0");
|
||||
expect(spies.exportVariable).not.toHaveBeenCalled();
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails when the installed CLI does not report a version", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"package-lock.json": createPackageLock("2.46.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.46.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setFailed).toHaveBeenCalledWith(
|
||||
"Could not determine installed Supabase CLI version",
|
||||
);
|
||||
expect(spies.setOutput).not.toHaveBeenCalled();
|
||||
expect(spies.addPath).not.toHaveBeenCalled();
|
||||
expect(spies.exportVariable).not.toHaveBeenCalled();
|
||||
});
|
||||
237
src/main.ts
237
src/main.ts
@@ -1,39 +1,208 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as tc from '@actions/tool-cache'
|
||||
import { gte } from 'semver'
|
||||
import { getDownloadUrl, determineInstalledVersion } from './utils.js'
|
||||
import { $, semver } from "bun";
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const CLI_CONFIG_REGISTRY = 'SUPABASE_INTERNAL_IMAGE_REGISTRY'
|
||||
export const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
|
||||
const REGISTRY_VERSION = "1.28.0";
|
||||
const DEFAULT_VERSION = "latest";
|
||||
|
||||
type BunLock = {
|
||||
workspaces?: {
|
||||
"": {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
};
|
||||
packages?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type PnpmDependency =
|
||||
| string
|
||||
| {
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type PnpmLock = {
|
||||
importers?: {
|
||||
".": {
|
||||
dependencies?: Record<string, PnpmDependency>;
|
||||
devDependencies?: Record<string, PnpmDependency>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type PackageLock = {
|
||||
packages?: Record<string, { version?: string }>;
|
||||
dependencies?: Record<string, { version?: string }>;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const match = raw.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/);
|
||||
return match?.[0] ?? null;
|
||||
}
|
||||
|
||||
function readWorkspaceLockfile(workspaceRoot: string, filename: string): string | null {
|
||||
const filePath = path.join(workspaceRoot, filename);
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function for the action.
|
||||
*
|
||||
* @returns Resolves when the action is complete.
|
||||
*/
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
// Get version of tool to be installed
|
||||
const version = core.getInput('version')
|
||||
|
||||
// Download the specific version of the tool, e.g. as a tarball/zipball
|
||||
const download = await getDownloadUrl(version)
|
||||
const pathToTarball = await tc.downloadTool(download)
|
||||
|
||||
// Extract the tarball/zipball onto host runner
|
||||
const pathToCLI = await tc.extractTar(pathToTarball)
|
||||
|
||||
// Expose the tool by adding it to the PATH
|
||||
core.addPath(pathToCLI)
|
||||
|
||||
// Expose installed tool version
|
||||
const determinedVersion = await determineInstalledVersion()
|
||||
core.setOutput('version', determinedVersion)
|
||||
|
||||
// Use GHCR mirror by default
|
||||
if (version.toLowerCase() === 'latest' || gte(version, '1.28.0')) {
|
||||
core.exportVariable(CLI_CONFIG_REGISTRY, 'ghcr.io')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) core.setFailed(error.message)
|
||||
return readFileSync(filePath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function detectVersionFromBunLock(workspaceRoot: string): string | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "bun.lock");
|
||||
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const lockfile = JSON.parse(text.replace(/,\s*([}\]])/g, "$1")) as BunLock;
|
||||
const rootWorkspace = lockfile.workspaces?.[""];
|
||||
const declaredVersion =
|
||||
rootWorkspace?.dependencies?.supabase ?? rootWorkspace?.devDependencies?.supabase;
|
||||
|
||||
if (!declaredVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedPackage = lockfile.packages?.supabase;
|
||||
if (Array.isArray(resolvedPackage) && typeof resolvedPackage[0] === "string") {
|
||||
return extractConcreteVersion(resolvedPackage[0]);
|
||||
}
|
||||
|
||||
return extractConcreteVersion(declaredVersion);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function detectVersionFromPnpmLock(workspaceRoot: string): string | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "pnpm-lock.yaml");
|
||||
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const lockfile = Bun.YAML.parse(text) as PnpmLock;
|
||||
const rootImporter = lockfile.importers?.["."];
|
||||
const dependency =
|
||||
rootImporter?.dependencies?.supabase ?? rootImporter?.devDependencies?.supabase;
|
||||
|
||||
if (typeof dependency === "string") {
|
||||
return extractConcreteVersion(dependency);
|
||||
}
|
||||
|
||||
return extractConcreteVersion(dependency?.version);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function detectVersionFromPackageLock(workspaceRoot: string): string | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "package-lock.json");
|
||||
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const lockfile = JSON.parse(text) as PackageLock;
|
||||
|
||||
return (
|
||||
extractConcreteVersion(lockfile.packages?.["node_modules/supabase"]?.version) ??
|
||||
extractConcreteVersion(lockfile.dependencies?.supabase?.version)
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersion(inputVersion: string): string {
|
||||
const requestedVersion = inputVersion.trim();
|
||||
|
||||
if (requestedVersion) {
|
||||
return requestedVersion;
|
||||
}
|
||||
|
||||
const workspaceRoot = process.env.GITHUB_WORKSPACE?.trim();
|
||||
|
||||
if (!workspaceRoot) {
|
||||
return DEFAULT_VERSION;
|
||||
}
|
||||
|
||||
return (
|
||||
detectVersionFromBunLock(workspaceRoot) ??
|
||||
detectVersionFromPnpmLock(workspaceRoot) ??
|
||||
detectVersionFromPackageLock(workspaceRoot) ??
|
||||
DEFAULT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
export function getDownloadUrl(version: string): string {
|
||||
const platform = getArchivePlatform(process.platform);
|
||||
const arch = getArchiveArch(process.arch);
|
||||
const filename = `supabase_${platform}_${arch}.tar.gz`;
|
||||
|
||||
if (version.toLowerCase() === "latest") {
|
||||
return `https://github.com/supabase/cli/releases/latest/download/${filename}`;
|
||||
}
|
||||
|
||||
if (semver.order(version, REGISTRY_VERSION) === -1) {
|
||||
return `https://github.com/supabase/cli/releases/download/v${version}/supabase_${version}_${platform}_${arch}.tar.gz`;
|
||||
}
|
||||
|
||||
return `https://github.com/supabase/cli/releases/download/v${version}/${filename}`;
|
||||
}
|
||||
|
||||
export async function determineInstalledVersion(cliPath: string): Promise<string> {
|
||||
const version = (await $`${path.join(cliPath, "supabase")} --version`.text()).trim();
|
||||
if (!version) {
|
||||
throw new Error("Could not determine installed Supabase CLI version");
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const version = resolveVersion(core.getInput("version"));
|
||||
const tarball = await tc.downloadTool(getDownloadUrl(version));
|
||||
const cliPath = await tc.extractTar(tarball);
|
||||
const installedVersion = await determineInstalledVersion(cliPath);
|
||||
core.setOutput("version", installedVersion);
|
||||
core.addPath(cliPath);
|
||||
|
||||
if (version.toLowerCase() === "latest" || semver.order(version, REGISTRY_VERSION) >= 0) {
|
||||
core.exportVariable(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
void run();
|
||||
}
|
||||
|
||||
48
src/utils.ts
48
src/utils.ts
@@ -1,48 +0,0 @@
|
||||
import { exec } from 'child_process'
|
||||
import os from 'os'
|
||||
import { lt } from 'semver'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const doExec = promisify(exec)
|
||||
|
||||
// arch in [arm, arm64, x64...] (https://nodejs.org/docs/latest-v16.x/api/os.html#osarch)
|
||||
// return value in [amd64, arm64, arm]
|
||||
const mapArch = (arch: string): string => {
|
||||
const mappings: Record<string, string> = {
|
||||
x64: 'amd64'
|
||||
}
|
||||
return mappings[arch] || arch
|
||||
}
|
||||
|
||||
// os in [darwin, linux, win32...] (https://nodejs.org/docs/latest-v16.x/api/os.html#osplatform)
|
||||
// return value in [darwin, linux, windows]
|
||||
const mapOS = (platform: string): string => {
|
||||
const mappings: Record<string, string> = {
|
||||
win32: 'windows'
|
||||
}
|
||||
return mappings[platform] || platform
|
||||
}
|
||||
|
||||
export const getDownloadUrl = async (version: string): Promise<string> => {
|
||||
const platform = mapOS(os.platform())
|
||||
const arch = mapArch(os.arch())
|
||||
const filename = `supabase_${platform}_${arch}.tar.gz`
|
||||
if (version.toLowerCase() === 'latest') {
|
||||
return `https://github.com/supabase/cli/releases/latest/download/${filename}`
|
||||
}
|
||||
if (lt(version, '1.28.0')) {
|
||||
return `https://github.com/supabase/cli/releases/download/v${version}/supabase_${version}_${platform}_${arch}.tar.gz`
|
||||
}
|
||||
return `https://github.com/supabase/cli/releases/download/v${version}/${filename}`
|
||||
}
|
||||
|
||||
export const determineInstalledVersion = async (): Promise<string> => {
|
||||
const { stdout } = await doExec('supabase --version')
|
||||
|
||||
const version = stdout.trim()
|
||||
if (!version) {
|
||||
throw new Error('Could not determine installed Supabase CLI version')
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
Reference in New Issue
Block a user