fix: v1 setup on Linux musl (#432)

## Summary

- Detect Linux musl runners in the v1 action and download the Supabase
CLI `.apk` asset for CLI versions `>= 2.99.0`.
- Add the extracted `usr/bin` directory to `PATH` for `.apk` archives.
- Backport the optional `github-token` input for authenticated `latest`
release lookup, because the test matrix hit unauthenticated GitHub API
rate limits.
- Rebuild `dist/index.js` for the Node action.

## Validation

- `npm run format:check`
- `npm run lint`
- `npm test`
- `npm run package`
- Local Docker smoke test in `node:20-alpine` with
`INPUT_VERSION=2.100.1`
- setup-cli-testing workflow:
https://github.com/jgoux/setup-cli-testing/actions/runs/26165593808

The external workflow passed Alpine `2.100.1`, Alpine `latest`, and
Ubuntu/macOS/Windows with both `2.100.1` and `latest`.
This commit is contained in:
Julien Goux
2026-05-20 16:22:46 +02:00
committed by GitHub
parent cd9b0fd6c9
commit ad077b4817
22 changed files with 43754 additions and 15606 deletions

View File

@@ -1,7 +1,11 @@
import * as core from '@actions/core'
import * as tc from '@actions/tool-cache'
import { gte } from 'semver'
import { getDownloadArchive, determineInstalledVersion } from './utils.js'
import {
getDownloadArchive,
determineInstalledVersion,
getCliPath
} from './utils.js'
export const CLI_CONFIG_REGISTRY = 'SUPABASE_INTERNAL_IMAGE_REGISTRY'
@@ -14,16 +18,24 @@ export async function run(): Promise<void> {
try {
// Get version of tool to be installed
const version = core.getInput('version')
const githubToken = core.getInput('github-token')
// Download the specific version of the tool, e.g. as a tarball/zipball
const download = await getDownloadArchive(version)
const download = await getDownloadArchive(
version,
undefined,
undefined,
undefined,
githubToken
)
const pathToArchive = await tc.downloadTool(download.url)
// Extract the tarball/zipball onto host runner
const pathToCLI =
const extractedPath =
download.format === 'zip'
? await tc.extractZip(pathToArchive)
: await tc.extractTar(pathToArchive)
const pathToCLI = getCliPath(extractedPath, download.format)
// Expose the tool by adding it to the PATH
core.addPath(pathToCLI)

View File

@@ -1,4 +1,5 @@
import { exec } from 'child_process'
import { existsSync } from 'fs'
import os from 'os'
import { gte, lt } from 'semver'
import { promisify } from 'util'
@@ -8,7 +9,7 @@ const VERSIONED_ARCHIVE_VERSION = '2.99.0'
const LATEST_RELEASE_URL =
'https://api.github.com/repos/supabase/cli/releases/latest'
export type ArchiveFormat = 'tar' | 'zip'
export type ArchiveFormat = 'apk' | 'tar' | 'zip'
export type DownloadArchive = {
url: string
@@ -35,8 +36,18 @@ const mapOS = (platform: string): string => {
const normalizeVersion = (version: string): string => version.replace(/^v/i, '')
const resolveLatestVersion = async (): Promise<string> => {
const response = await fetch(LATEST_RELEASE_URL)
const resolveLatestVersion = async (githubToken?: string): Promise<string> => {
const headers: Record<string, string> = {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
const token = githubToken?.trim()
if (token) {
headers.Authorization = `Bearer ${token}`
}
const response = await fetch(LATEST_RELEASE_URL, { headers })
if (!response.ok) {
throw new Error(
`Failed to resolve latest Supabase CLI release: ${response.statusText}`
@@ -53,7 +64,37 @@ const resolveLatestVersion = async (): Promise<string> => {
return normalizeVersion(release.tag_name)
}
const getArchiveFormat = (version: string, platform: string): ArchiveFormat => {
const detectMuslLinux = async (platform = os.platform()): Promise<boolean> => {
if (platform !== 'linux') {
return false
}
if (existsSync('/etc/alpine-release')) {
return true
}
try {
const { stdout, stderr } = await doExec('ldd --version')
return `${stdout}\n${stderr}`.toLowerCase().includes('musl')
} catch (error) {
const output = error instanceof Error ? error.message : String(error)
return output.toLowerCase().includes('musl')
}
}
const getArchiveFormat = (
version: string,
platform: string,
isMuslLinux: boolean
): ArchiveFormat => {
if (
platform === 'linux' &&
isMuslLinux &&
gte(version, VERSIONED_ARCHIVE_VERSION)
) {
return 'apk'
}
if (platform === 'win32' && gte(version, VERSIONED_ARCHIVE_VERSION)) {
return 'zip'
}
@@ -64,7 +105,8 @@ const getArchiveFormat = (version: string, platform: string): ArchiveFormat => {
const getArchiveFilename = (
version: string,
platform: string,
arch: string
arch: string,
format: ArchiveFormat
): string => {
const archivePlatform = mapOS(platform)
const archiveArch = mapArch(arch)
@@ -72,6 +114,10 @@ const getArchiveFilename = (
return `supabase_${version}_${archivePlatform}_${archiveArch}.tar.gz`
}
if (platform === 'linux' && format === 'apk') {
return `supabase_${version}_${archivePlatform}_${archiveArch}.apk`
}
if (gte(version, VERSIONED_ARCHIVE_VERSION)) {
const extension = platform === 'win32' ? 'zip' : 'tar.gz'
return `supabase_${version}_${archivePlatform}_${archiveArch}.${extension}`
@@ -83,22 +129,45 @@ const getArchiveFilename = (
export const getDownloadArchive = async (
version: string,
platform = os.platform(),
arch = os.arch()
arch = os.arch(),
isMuslLinux?: boolean,
githubToken?: string
): Promise<DownloadArchive> => {
const resolvedVersion =
version.toLowerCase() === 'latest'
? await resolveLatestVersion()
? await resolveLatestVersion(githubToken)
: 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
}
}
export const getDownloadUrl = async (version: string): Promise<string> => {
const archive = await getDownloadArchive(version)
export const getCliPath = (
extractedPath: string,
archiveFormat: ArchiveFormat
): string => {
return archiveFormat === 'apk' ? `${extractedPath}/usr/bin` : extractedPath
}
export const getDownloadUrl = async (
version: string,
githubToken?: string
): Promise<string> => {
const archive = await getDownloadArchive(
version,
os.platform(),
os.arch(),
undefined,
githubToken
)
return archive.url
}