fix: authenticate latest release lookup (#430)

## Summary

- Add an optional `github-token` input to authenticate the GitHub
release lookup used by `version: latest`.
- Pass the token through the composite action as
`SUPABASE_CLI_GITHUB_TOKEN` and use it as a bearer token for the
`/repos/supabase/cli/releases/latest` request.
- Update this repository's CI smoke test and README examples to pass
`${{ github.token }}` when testing or using `latest`.

## Root Cause

CI failed in `test (macos-latest, latest)` because the action resolved
`latest` through an unauthenticated GitHub REST API request and hit the
low unauthenticated rate limit. The dependency bump in #429 was not the
cause; the validate job passed and the failure happened inside the
release lookup path.

## Impact

Pinned versions continue to work without a token. For `version: latest`,
callers can now pass `${{ github.token }}` to avoid unauthenticated API
rate limiting while keeping the input optional for backward
compatibility.

## Validation

- `bun run ci`
This commit is contained in:
Julien Goux
2026-05-20 15:06:17 +02:00
committed by GitHub
parent a4d563a017
commit 3095b000b6
5 changed files with 47 additions and 4 deletions

View File

@@ -52,6 +52,7 @@ jobs:
- uses: ./
with:
version: ${{ matrix.version }}
github-token: ${{ github.token }}
- run: supabase -h
ci:

View File

@@ -47,6 +47,7 @@ steps:
- uses: supabase/setup-cli@v2
with:
version: latest
github-token: ${{ github.token }}
- run: supabase init
- run: supabase db start
```
@@ -58,9 +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`) | Root lockfile version or `latest` | 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
@@ -162,6 +164,7 @@ steps:
- uses: ./
with:
version: latest
github-token: ${{ github.token }}
```
The CI workflow provides fast smoke coverage across GitHub-hosted runners, and

View File

@@ -5,6 +5,9 @@ inputs:
version:
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.
required: false
outputs:
version:
description: Version of installed Supabase CLI
@@ -28,4 +31,5 @@ runs:
working-directory: ${{ github.action_path }}
env:
INPUT_VERSION: ${{ inputs.version }}
SUPABASE_CLI_GITHUB_TOKEN: ${{ inputs.github-token }}
run: bun src/main.ts

View File

@@ -10,13 +10,21 @@ import * as tc from "@actions/tool-cache";
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_TOKEN_ENV = "SUPABASE_CLI_GITHUB_TOKEN";
const originalWorkspace = process.env.GITHUB_WORKSPACE;
const originalGithubToken = process.env[GITHUB_TOKEN_ENV];
const tempDirs = new Set<string>();
let mainModule: typeof import("./main.ts") | null = null;
afterEach(() => {
mock.restore();
process.env.GITHUB_WORKSPACE = originalWorkspace;
if (originalGithubToken === undefined) {
delete process.env[GITHUB_TOKEN_ENV];
} else {
process.env[GITHUB_TOKEN_ENV] = originalGithubToken;
}
for (const dir of tempDirs) {
rmSync(dir, { force: true, recursive: true });
@@ -222,6 +230,22 @@ test("resolves latest before choosing a versioned Supabase CLI archive", async (
});
});
test("authenticates latest release lookup when a GitHub token is provided", async () => {
process.env[GITHUB_TOKEN_ENV] = "ghs_test-token";
const fetch = mockLatestRelease("v2.99.0");
const { getDownloadArchive } = await getMainModule();
await getDownloadArchive("latest", "darwin", "arm64");
expect(fetch).toHaveBeenCalledWith(GITHUB_RELEASES_API, {
headers: expect.objectContaining({
Accept: "application/vnd.github+json",
Authorization: "Bearer ghs_test-token",
"X-GitHub-Api-Version": "2022-11-28",
}),
});
});
test("awaits the action entrypoint with omitted version and latest fallback", async () => {
process.env.GITHUB_WORKSPACE = repo;
mockLatestRelease();

View File

@@ -10,6 +10,7 @@ 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";
const GITHUB_TOKEN_ENV = "SUPABASE_CLI_GITHUB_TOKEN";
type ArchiveFormat = "tar" | "zip";
@@ -175,7 +176,17 @@ function resolveVersion(inputVersion: string): string {
}
async function resolveLatestVersion(): Promise<string> {
const response = await fetch(GITHUB_RELEASES_API);
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
const githubToken = process.env[GITHUB_TOKEN_ENV]?.trim();
if (githubToken) {
headers.Authorization = `Bearer ${githubToken}`;
}
const response = await fetch(GITHUB_RELEASES_API, { headers });
if (!response.ok) {
throw new Error(`Failed to resolve latest Supabase CLI release: ${response.statusText}`);
}