From 32b9432460c48c8b8231667f55d0d4133aefebb5 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 20 May 2026 12:56:11 +0200 Subject: [PATCH] fix alpine cli setup --- action.yml | 40 ++++++++++++++++++++++++++++++++-- src/main.test.ts | 30 ++++++++++++++++++++++++++ src/main.ts | 56 +++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/action.yml b/action.yml index cbf4c66..dd76e82 100644 --- a/action.yml +++ b/action.yml @@ -12,19 +12,55 @@ outputs: runs: using: composite steps: + - id: bun-download + name: Resolve Bun Download URL + shell: sh + working-directory: ${{ github.action_path }} + run: | + set -eu + + if [ "${RUNNER_OS}" != "Linux" ]; then + exit 0 + fi + + # setup-bun does not detect Linux musl yet, so Alpine-like containers need the musl asset explicitly. + is_musl=false + if [ -f /etc/alpine-release ]; then + is_musl=true + elif command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then + is_musl=true + fi + + if [ "${is_musl}" != "true" ]; then + exit 0 + fi + + version="$(cat .bun-version)" + case "$(uname -m)" in + x86_64) arch="x64" ;; + aarch64|arm64) arch="aarch64" ;; + *) + echo "Unsupported Linux musl architecture: $(uname -m)" >&2 + exit 1 + ;; + esac + + echo "url=https://github.com/oven-sh/bun/releases/download/bun-v${version}/bun-linux-${arch}-musl.zip" >> "$GITHUB_OUTPUT" + - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: ${{ github.action_path }}/.bun-version + bun-download-url: ${{ steps.bun-download.outputs.url }} - name: Install Action Dependencies - shell: bash + shell: sh working-directory: ${{ github.action_path }} run: bun install --frozen-lockfile --production - id: setup-cli name: Setup Supabase CLI - shell: bash + shell: sh working-directory: ${{ github.action_path }} env: INPUT_VERSION: ${{ inputs.version }} diff --git a/src/main.test.ts b/src/main.test.ts index 46627c6..6a3becf 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -188,6 +188,36 @@ test("uses versioned tar archives for Supabase CLI v2.99.0 and later", async () }); }); +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(); diff --git a/src/main.ts b/src/main.ts index 68ca9ba..5665c74 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,7 @@ 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 ArchiveFormat = "apk" | "tar" | "zip"; type DownloadArchive = { url: string; @@ -188,7 +188,19 @@ async function resolveLatestVersion(): Promise { return normalizeVersion(release.tag_name); } -function getArchiveFormat(version: string, platform: NodeJS.Platform): ArchiveFormat { +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"; } @@ -200,6 +212,7 @@ function getArchiveFilename( version: string, platform: NodeJS.Platform, arch: NodeJS.Architecture, + archiveFormat: ArchiveFormat, ): string { const archivePlatform = getArchivePlatform(platform); const archiveArch = getArchiveArch(arch); @@ -208,6 +221,10 @@ function getArchiveFilename( 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}`; @@ -220,17 +237,45 @@ 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 filename = getArchiveFilename(resolvedVersion, platform, arch); + 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: getArchiveFormat(resolvedVersion, platform), + 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"); + } +} + +export function getCliPath(extractedPath: string, archiveFormat: ArchiveFormat): string { + return archiveFormat === "apk" ? path.join(extractedPath, "usr", "bin") : extractedPath; +} + function getCliExecutablePath(cliPath: string): string { if (process.platform !== "win32") { return path.join(cliPath, "supabase"); @@ -263,10 +308,11 @@ export async function run(): Promise { const version = resolveVersion(core.getInput("version")); const archive = await getDownloadArchive(version); const archivePath = await tc.downloadTool(archive.url); - const cliPath = + const extractedPath = archive.format === "zip" ? await tc.extractZip(archivePath) : await tc.extractTar(archivePath); + const cliPath = getCliPath(extractedPath, archive.format); const installedVersion = await determineInstalledVersion(cliPath); core.setOutput("version", installedVersion); core.addPath(cliPath);