Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Goux
afb1b15109 fix: allow registry fallback in supported CLI versions
The action currently pins every recent CLI to GHCR, which disables the
CLI's registry fallback and leaves image pulls exposed to GHCR
throttling.

Use the installed CLI version to keep the GHCR default for versions
before v2.108.0 and allow fallback for newer versions. Preserve a
caller's explicit registry choice.
2026-09-24 00:20:56 +02:00
5 changed files with 98 additions and 139 deletions

View File

@@ -47,6 +47,8 @@ jobs:
exclude:
- version: 1.178.2
pg_major: 17
- version: latest
pg_major: 14
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:

View File

@@ -40,17 +40,6 @@ steps:
version: 2.84.2
```
To always track the latest beta prerelease, set `version` to `beta`. This is
useful for surfacing (and fixing) breakages early:
```yaml
steps:
- uses: supabase/setup-cli@v2
with:
version: beta
github-token: ${{ github.token }}
```
Run `supabase db start` to execute all migrations on a fresh database:
```yaml
@@ -70,10 +59,10 @@ on Windows and macOS runners.
The action supports the following inputs:
| Name | Type | Description | Default | Required |
| -------------- | ------ | --------------------------------------------------------------------------------- | --------------------------------- | -------- |
| `version` | String | Supabase CLI version (or `latest`, or `beta` for the latest beta release) | Root lockfile version or `latest` | false |
| `github-token` | String | GitHub token used to resolve `latest`/`beta` without unauthenticated API limiting | | false |
| Name | Type | Description | Default | Required |
| -------------- | ------ | -------------------------------------------------------------------------- | --------------------------------- | -------- |
| `version` | String | Supabase CLI version (or `latest`) | Root lockfile version or `latest` | false |
| `github-token` | String | GitHub token used to resolve `latest` without unauthenticated API limiting | | false |
## Advanced Usage

View File

@@ -3,7 +3,7 @@ description: Setup Supabase CLI, supabase, on GitHub Actions runners
author: Supabase
inputs:
version:
description: Version of Supabase CLI to install. Accepts a specific version, "latest", or "beta" (latest beta prerelease). If omitted, detect from the root lockfile and otherwise use latest.
description: Version of Supabase CLI to install. If omitted, detect from the root lockfile and otherwise use latest.
required: false
github-token:
description: GitHub token used to resolve the latest Supabase CLI release without hitting unauthenticated API limits.

View File

@@ -11,8 +11,8 @@ const repo = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const defaultEntrypoint = fileURLToPath(new URL("./main.ts", import.meta.url));
const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
const GITHUB_RELEASES_API = "https://api.github.com/repos/supabase/cli/releases/latest";
const GITHUB_RELEASES_LIST_API = "https://api.github.com/repos/supabase/cli/releases";
const GITHUB_TOKEN_ENV = "SUPABASE_CLI_GITHUB_TOKEN";
const originalCliConfigRegistry = process.env[CLI_CONFIG_REGISTRY];
const originalWorkspace = process.env.GITHUB_WORKSPACE;
const originalGithubToken = process.env[GITHUB_TOKEN_ENV];
const tempDirs = new Set<string>();
@@ -26,6 +26,11 @@ afterEach(() => {
} else {
process.env[GITHUB_TOKEN_ENV] = originalGithubToken;
}
if (originalCliConfigRegistry === undefined) {
delete process.env[CLI_CONFIG_REGISTRY];
} else {
process.env[CLI_CONFIG_REGISTRY] = originalCliConfigRegistry;
}
for (const dir of tempDirs) {
rmSync(dir, { force: true, recursive: true });
@@ -178,21 +183,6 @@ function mockLatestRelease(version = "v2.99.0") {
);
}
function mockBetaReleases(
releases: Array<{ tag_name: string; prerelease: boolean }> = [
{ tag_name: "v2.100.0-beta.2", prerelease: true },
{ tag_name: "v2.100.0-beta.1", prerelease: true },
{ tag_name: "v2.99.0", prerelease: false },
],
) {
return spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify(releases), {
status: 200,
statusText: "OK",
}),
);
}
async function getMainModule(): Promise<typeof import("./main.ts")> {
if (!mainModule) {
mainModule = await import("./main.ts");
@@ -292,66 +282,6 @@ test("authenticates latest release lookup when a GitHub token is provided", asyn
});
});
test("resolves the latest beta prerelease for the beta channel", async () => {
mockBetaReleases();
const { getDownloadArchive } = await getMainModule();
const archive = await getDownloadArchive("beta", "darwin", "arm64");
expect(archive).toEqual({
url: "https://github.com/supabase/cli/releases/download/v2.100.0-beta.2/supabase_2.100.0-beta.2_darwin_arm64.tar.gz",
format: "tar",
});
});
test("treats the beta channel case-insensitively and skips stable releases", async () => {
mockBetaReleases([
{ tag_name: "v2.99.0", prerelease: false },
{ tag_name: "v2.100.0-beta.5", prerelease: true },
]);
const { getDownloadArchive } = await getMainModule();
const archive = await getDownloadArchive("BETA", "linux", "x64");
expect(archive.url).toContain("/download/v2.100.0-beta.5/supabase_2.100.0-beta.5_linux_amd64");
});
test("fails when no beta prerelease is available", async () => {
mockBetaReleases([{ tag_name: "v2.99.0", prerelease: false }]);
const { getDownloadArchive } = await getMainModule();
expect(getDownloadArchive("beta", "linux", "x64")).rejects.toThrow(
"Failed to resolve latest Supabase CLI beta release: no beta release found",
);
});
test("queries the releases list when resolving the beta channel", async () => {
const fetch = mockBetaReleases();
const { getDownloadArchive } = await getMainModule();
await getDownloadArchive("beta", "darwin", "arm64");
expect(fetch).toHaveBeenCalledWith(GITHUB_RELEASES_LIST_API, {
headers: expect.objectContaining({
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}),
});
});
test("exports the internal registry when installing the beta channel", async () => {
mockBetaReleases();
const cliDir = createFakeCli("supabase 2.100.0-beta.2");
const spies = createActionSpies("beta", cliDir, "/download/v2.100.0-beta.2/supabase_");
const { run } = await getMainModule();
await run();
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.100.0-beta.2");
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
expect(spies.setFailed).not.toHaveBeenCalled();
});
test("awaits the action entrypoint with omitted version and latest fallback", async () => {
process.env.GITHUB_WORKSPACE = repo;
mockLatestRelease();
@@ -592,6 +522,75 @@ test("explicit version overrides detected root lockfiles", async () => {
expect(spies.setFailed).not.toHaveBeenCalled();
});
test("keeps the GHCR registry pin through Supabase CLI v2.107.x", async () => {
const cliDir = createFakeCli("supabase 2.107.9");
const spies = createActionSpies("2.107.9", cliDir, "/download/v2.107.9/supabase_");
const { run } = await getMainModule();
await run();
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
expect(spies.setFailed).not.toHaveBeenCalled();
});
test("keeps the GHCR registry pin starting with Supabase CLI v1.28.0", async () => {
const cliDir = createFakeCli("supabase 1.28.0");
const spies = createActionSpies("1.28.0", cliDir, "/download/v1.28.0/supabase_");
const { run } = await getMainModule();
await run();
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
expect(spies.setFailed).not.toHaveBeenCalled();
});
test("uses the CLI built-in registry fallback starting with Supabase CLI v2.108.0", async () => {
const cliDir = createFakeCli("supabase 2.108.0");
const spies = createActionSpies("2.108.0", cliDir, "/download/v2.108.0/supabase_");
const { run } = await getMainModule();
await run();
expect(spies.exportVariable).not.toHaveBeenCalled();
expect(spies.setFailed).not.toHaveBeenCalled();
});
test("preserves an explicitly configured internal image registry", async () => {
process.env[CLI_CONFIG_REGISTRY] = "registry.example.test";
const cliDir = createFakeCli("supabase 2.108.0");
const spies = createActionSpies("2.108.0", cliDir, "/download/v2.108.0/supabase_");
const { run } = await getMainModule();
await run();
expect(process.env[CLI_CONFIG_REGISTRY]).toBe("registry.example.test");
expect(spies.exportVariable).not.toHaveBeenCalled();
});
test("preserves a whitespace-only internal image registry", async () => {
process.env[CLI_CONFIG_REGISTRY] = " ";
const cliDir = createFakeCli("supabase 2.108.0");
const spies = createActionSpies("2.108.0", cliDir, "/download/v2.108.0/supabase_");
const { run } = await getMainModule();
await run();
expect(process.env[CLI_CONFIG_REGISTRY]).toBe(" ");
expect(spies.exportVariable).not.toHaveBeenCalled();
});
test("uses the installed version to select the registry for latest", async () => {
mockLatestRelease("v2.108.0");
const cliDir = createFakeCli("supabase 2.108.0");
const spies = createActionSpies("latest", cliDir, "/download/v2.108.0/supabase_");
const { run } = await getMainModule();
await run();
expect(spies.exportVariable).not.toHaveBeenCalled();
expect(spies.setFailed).not.toHaveBeenCalled();
});
test("fails when the installed CLI does not report a version", async () => {
process.env.GITHUB_WORKSPACE = createWorkspace({
"package-lock.json": createPackageLock("2.46.0"),

View File

@@ -7,12 +7,10 @@ import { fileURLToPath } from "node:url";
export const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
const REGISTRY_VERSION = "1.28.0";
const DEFAULT_REGISTRY_FALLBACK_VERSION = "2.108.0";
const VERSIONED_ARCHIVE_VERSION = "2.99.0";
const DEFAULT_VERSION = "latest";
const LATEST_VERSION = "latest";
const BETA_VERSION = "beta";
const GITHUB_RELEASES_API = "https://api.github.com/repos/supabase/cli/releases/latest";
const GITHUB_RELEASES_LIST_API = "https://api.github.com/repos/supabase/cli/releases";
const GITHUB_TOKEN_ENV = "SUPABASE_CLI_GITHUB_TOKEN";
type ArchiveFormat = "apk" | "tar" | "zip";
@@ -178,7 +176,7 @@ function resolveVersion(inputVersion: string): string {
);
}
function buildGithubHeaders(): Record<string, string> {
async function resolveLatestVersion(): Promise<string> {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
@@ -189,11 +187,7 @@ function buildGithubHeaders(): Record<string, string> {
headers.Authorization = `Bearer ${githubToken}`;
}
return headers;
}
async function resolveLatestVersion(): Promise<string> {
const response = await fetch(GITHUB_RELEASES_API, { headers: buildGithubHeaders() });
const response = await fetch(GITHUB_RELEASES_API, { headers });
if (!response.ok) {
throw new Error(`Failed to resolve latest Supabase CLI release: ${response.statusText}`);
}
@@ -206,31 +200,6 @@ async function resolveLatestVersion(): Promise<string> {
return normalizeVersion(release.tag_name);
}
async function resolveLatestBetaVersion(): Promise<string> {
// The /releases/latest endpoint never returns prereleases, so list all
// releases (sorted newest-first) and pick the most recent beta prerelease.
const response = await fetch(GITHUB_RELEASES_LIST_API, { headers: buildGithubHeaders() });
if (!response.ok) {
throw new Error(`Failed to resolve latest Supabase CLI beta release: ${response.statusText}`);
}
const releases = (await response.json()) as Array<{ tag_name?: unknown; prerelease?: unknown }>;
const beta = Array.isArray(releases)
? releases.find(
(release) =>
release.prerelease === true &&
typeof release.tag_name === "string" &&
/-beta/i.test(release.tag_name),
)
: undefined;
if (!beta || typeof beta.tag_name !== "string") {
throw new Error("Failed to resolve latest Supabase CLI beta release: no beta release found");
}
return normalizeVersion(beta.tag_name);
}
function getArchiveFormat(
version: string,
platform: NodeJS.Platform,
@@ -282,15 +251,8 @@ export async function getDownloadArchive(
arch = process.arch,
isMuslLinux?: boolean,
): Promise<DownloadArchive> {
const channel = version.toLowerCase();
let resolvedVersion: string;
if (channel === LATEST_VERSION) {
resolvedVersion = await resolveLatestVersion();
} else if (channel === BETA_VERSION) {
resolvedVersion = await resolveLatestBetaVersion();
} else {
resolvedVersion = normalizeVersion(version);
}
const resolvedVersion =
version.toLowerCase() === "latest" ? await resolveLatestVersion() : normalizeVersion(version);
const format = getArchiveFormat(
resolvedVersion,
platform,
@@ -367,11 +329,18 @@ export async function run(): Promise<void> {
core.setOutput("version", installedVersion);
core.addPath(cliPath);
const channel = version.toLowerCase();
if (process.env[CLI_CONFIG_REGISTRY]) {
return;
}
const installedVersionNumber = extractConcreteVersion(installedVersion);
if (!installedVersionNumber) {
throw new Error("Could not determine installed Supabase CLI version");
}
if (
channel === LATEST_VERSION ||
channel === BETA_VERSION ||
semver.order(version, REGISTRY_VERSION) >= 0
semver.order(installedVersionNumber, REGISTRY_VERSION) >= 0 &&
semver.order(installedVersionNumber, DEFAULT_REGISTRY_FALLBACK_VERSION) === -1
) {
core.exportVariable(CLI_CONFIG_REGISTRY, "ghcr.io");
}