mirror of
https://github.com/supabase/setup-cli.git
synced 2026-06-27 17:36:57 +00:00
feat: support installing the latest beta release via version: beta
The `latest` channel resolves through the GitHub `/releases/latest` endpoint, which never returns prereleases, so there was no way to track the CLI beta channel — any other value was treated as an exact version. Add a `beta` channel that lists releases and installs the most recent beta prerelease. This lets consumers surface (and fix) breakages early. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APKXLPMGXsnWUSU3i16Lfv
This commit is contained in:
19
README.md
19
README.md
@@ -40,6 +40,17 @@ 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
|
||||
@@ -59,10 +70,10 @@ on Windows and macOS runners.
|
||||
|
||||
The action supports the following inputs:
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ description: Setup Supabase CLI, supabase, on GitHub Actions runners
|
||||
author: Supabase
|
||||
inputs:
|
||||
version:
|
||||
description: Version of Supabase CLI to install. If omitted, detect from the root lockfile and otherwise use latest.
|
||||
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.
|
||||
required: false
|
||||
github-token:
|
||||
description: GitHub token used to resolve the latest Supabase CLI release without hitting unauthenticated API limits.
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 originalWorkspace = process.env.GITHUB_WORKSPACE;
|
||||
const originalGithubToken = process.env[GITHUB_TOKEN_ENV];
|
||||
@@ -177,6 +178,21 @@ 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");
|
||||
@@ -276,6 +292,66 @@ 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();
|
||||
|
||||
54
src/main.ts
54
src/main.ts
@@ -9,7 +9,10 @@ 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 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";
|
||||
@@ -175,7 +178,7 @@ function resolveVersion(inputVersion: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveLatestVersion(): Promise<string> {
|
||||
function buildGithubHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
@@ -186,7 +189,11 @@ async function resolveLatestVersion(): Promise<string> {
|
||||
headers.Authorization = `Bearer ${githubToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(GITHUB_RELEASES_API, { headers });
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function resolveLatestVersion(): Promise<string> {
|
||||
const response = await fetch(GITHUB_RELEASES_API, { headers: buildGithubHeaders() });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to resolve latest Supabase CLI release: ${response.statusText}`);
|
||||
}
|
||||
@@ -199,6 +206,31 @@ 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,
|
||||
@@ -250,8 +282,15 @@ export async function getDownloadArchive(
|
||||
arch = process.arch,
|
||||
isMuslLinux?: boolean,
|
||||
): Promise<DownloadArchive> {
|
||||
const resolvedVersion =
|
||||
version.toLowerCase() === "latest" ? await resolveLatestVersion() : normalizeVersion(version);
|
||||
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 format = getArchiveFormat(
|
||||
resolvedVersion,
|
||||
platform,
|
||||
@@ -328,7 +367,12 @@ export async function run(): Promise<void> {
|
||||
core.setOutput("version", installedVersion);
|
||||
core.addPath(cliPath);
|
||||
|
||||
if (version.toLowerCase() === "latest" || semver.order(version, REGISTRY_VERSION) >= 0) {
|
||||
const channel = version.toLowerCase();
|
||||
if (
|
||||
channel === LATEST_VERSION ||
|
||||
channel === BETA_VERSION ||
|
||||
semver.order(version, REGISTRY_VERSION) >= 0
|
||||
) {
|
||||
core.exportVariable(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user