mirror of
https://github.com/supabase/setup-cli.git
synced 2026-06-28 01:46:58 +00:00
fix: handle Supabase CLI v2.99 archives (#425)
## Summary Supabase CLI v2.99.0 changed the release archive layout. The `latest` release no longer exposes assets like `supabase_linux_amd64.tar.gz`; the downloadable tarballs are now versioned, for example `supabase_2.99.0_linux_amd64.tar.gz`. Windows archives also switched to `.zip` for v2.99.0+. This updates the setup action to: - Resolve `latest` to the actual Supabase CLI release tag before building the download URL. - Keep the existing unversioned archive path for CLI versions before v2.99.0. - Use the new versioned archive path for v2.99.0 and later. - Extract Windows v2.99.0+ archives with `extractZip`; keep tar extraction for Linux and macOS. - Continue executing the main `supabase` binary even though the archive now also contains `supabase-go`. This should fix the `latest` download failure reported in supabase/cli#5257. ## Testing - `bun test` - `bun run ci` - Local smoke test with `INPUT_VERSION=latest bun src/main.ts`, which downloaded and executed `supabase_2.99.0_darwin_arm64.tar.gz` successfully.
This commit is contained in:
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
|
||||
100
src/main.ts
100
src/main.ts
@@ -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?: {
|
||||
@@ -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);
|
||||
|
||||
@@ -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`;
|
||||
|
||||
if (version.toLowerCase() === "latest") {
|
||||
return `https://github.com/supabase/cli/releases/latest/download/${filename}`;
|
||||
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}`);
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
return `https://github.com/supabase/cli/releases/download/v${version}/${filename}`;
|
||||
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 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");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user