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:
Julien Goux
2026-05-18 13:09:18 +02:00
committed by GitHub
parent 0abc813ee3
commit a4d563a017
2 changed files with 154 additions and 17 deletions

View File

@@ -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);