Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 66 additions & 5 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,19 @@ function createActionSpies(inputVersion: string, cliDir: string, expectedUrlFrag
return path.join(os.tmpdir(), "supabase-cli.tar.gz");
}),
extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir),
extractZip: spyOn(tc, "extractZip").mockImplementation(async () => cliDir),
};
}

function mockLatestRelease(version = "v2.99.0") {
return spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({ tag_name: version }), {
status: 200,
statusText: "OK",
}),
);
}

async function getMainModule(): Promise<typeof import("./main.ts")> {
if (!mainModule) {
mainModule = await import("./main.ts");
Expand All @@ -167,8 +177,54 @@ async function getMainModule(): Promise<typeof import("./main.ts")> {
return mainModule;
}

test("uses versioned tar archives for Supabase CLI v2.99.0 and later", async () => {
const { getDownloadArchive } = 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",
});
});

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("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;
Expand All @@ -185,11 +241,12 @@ test("awaits the action entrypoint with omitted version and latest fallback", as
exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}),
setFailed: spyOn(core, "setFailed").mockImplementation(() => {}),
downloadTool: spyOn(tc, "downloadTool").mockImplementation(async (url: string) => {
expect(url).toContain("/latest/download/");
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;
Expand Down Expand Up @@ -283,8 +340,9 @@ test("falls back to latest when version is omitted and no supported root lockfil
process.env.GITHUB_WORKSPACE = createWorkspace({
"README.md": "# app\n",
});
mockLatestRelease();
const cliDir = createFakeCli("supabase 2.84.2");
const spies = createActionSpies("", cliDir, "/latest/download/");
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
const { run } = await getMainModule();

await run();
Expand All @@ -296,8 +354,9 @@ test("falls back to latest when version is omitted and no supported root lockfil

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, "/latest/download/");
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
const { run } = await getMainModule();

await run();
Expand Down Expand Up @@ -360,8 +419,9 @@ test("falls through unreadable bun.lock paths and malformed package-lock files t
});
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, "/latest/download/");
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
const { run } = await getMainModule();

await run();
Expand All @@ -375,8 +435,9 @@ test("falls back to latest when a pnpm dependency entry has no concrete version"
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, "/latest/download/");
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
const { run } = await getMainModule();

await run();
Expand Down
98 changes: 87 additions & 11 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ 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";

type ArchiveFormat = "tar" | "zip";

type DownloadArchive = {
url: string;
format: ArchiveFormat;
};

type BunLock = {
workspaces?: {
Expand Down Expand Up @@ -56,6 +65,10 @@ function extractConcreteVersion(raw: string | undefined): string | null {
return match?.[0] ?? null;
}

function normalizeVersion(version: string): string {
return version.replace(/^v/i, "");
}

function readWorkspaceLockfile(workspaceRoot: string, filename: string): string | null {
const filePath = path.join(workspaceRoot, filename);

Expand Down Expand Up @@ -161,24 +174,83 @@ function resolveVersion(inputVersion: string): string {
);
}

export function getDownloadUrl(version: string): string {
const platform = getArchivePlatform(process.platform);
const arch = getArchiveArch(process.arch);
const filename = `supabase_${platform}_${arch}.tar.gz`;
async function resolveLatestVersion(): Promise<string> {
const response = await fetch(GITHUB_RELEASES_API);
if (!response.ok) {
throw new Error(`Failed to resolve latest Supabase CLI release: ${response.statusText}`);
}

if (version.toLowerCase() === "latest") {
return `https://github.com/supabase/cli/releases/latest/download/${filename}`;
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): ArchiveFormat {
if (platform === "win32" && semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0) {
return "zip";
}

return "tar";
}

function getArchiveFilename(
version: string,
platform: NodeJS.Platform,
arch: NodeJS.Architecture,
): string {
const archivePlatform = getArchivePlatform(platform);
const archiveArch = getArchiveArch(arch);

if (semver.order(version, REGISTRY_VERSION) === -1) {
return `https://github.com/supabase/cli/releases/download/v${version}/supabase_${version}_${platform}_${arch}.tar.gz`;
return `supabase_${version}_${archivePlatform}_${archiveArch}.tar.gz`;
}

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,
): Promise<DownloadArchive> {
const resolvedVersion =
version.toLowerCase() === "latest" ? await resolveLatestVersion() : normalizeVersion(version);
const filename = getArchiveFilename(resolvedVersion, platform, arch);

return {
url: `https://github.com/supabase/cli/releases/download/v${resolvedVersion}/${filename}`,
format: getArchiveFormat(resolvedVersion, platform),
};
}

function getCliExecutablePath(cliPath: string): string {
if (process.platform !== "win32") {
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;
}

return `https://github.com/supabase/cli/releases/download/v${version}/${filename}`;
return path.join(cliPath, "supabase");
}

export async function determineInstalledVersion(cliPath: string): Promise<string> {
const version = (await $`${path.join(cliPath, "supabase")} --version`.text()).trim();
const version = (await $`${getCliExecutablePath(cliPath)} --version`.text()).trim();
if (!version) {
throw new Error("Could not determine installed Supabase CLI version");
}
Expand All @@ -189,8 +261,12 @@ export async function determineInstalledVersion(cliPath: string): Promise<string
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 archive = await getDownloadArchive(version);
const archivePath = await tc.downloadTool(archive.url);
const cliPath =
archive.format === "zip"
? await tc.extractZip(archivePath)
: await tc.extractTar(archivePath);
const installedVersion = await determineInstalledVersion(cliPath);
core.setOutput("version", installedVersion);
core.addPath(cliPath);
Expand Down