mirror of
https://github.com/supabase/setup-cli.git
synced 2026-06-29 02:16:58 +00:00
Compare commits
1 Commits
v2.1.0
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b235d82dcd |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -52,7 +52,6 @@ jobs:
|
|||||||
- uses: ./
|
- uses: ./
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.version }}
|
version: ${{ matrix.version }}
|
||||||
github-token: ${{ github.token }}
|
|
||||||
- run: supabase -h
|
- run: supabase -h
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ steps:
|
|||||||
- uses: supabase/setup-cli@v2
|
- uses: supabase/setup-cli@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
github-token: ${{ github.token }}
|
|
||||||
- run: supabase init
|
- run: supabase init
|
||||||
- run: supabase db start
|
- run: supabase db start
|
||||||
```
|
```
|
||||||
@@ -60,9 +59,8 @@ on Windows and macOS runners.
|
|||||||
The action supports the following inputs:
|
The action supports the following inputs:
|
||||||
|
|
||||||
| Name | Type | Description | Default | Required |
|
| Name | Type | Description | Default | Required |
|
||||||
| -------------- | ------ | -------------------------------------------------------------------------- | --------------------------------- | -------- |
|
| --------- | ------ | ---------------------------------- | --------------------------------- | -------- |
|
||||||
| `version` | String | Supabase CLI version (or `latest`) | Root lockfile version or `latest` | false |
|
| `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
|
## Advanced Usage
|
||||||
|
|
||||||
@@ -164,7 +162,6 @@ steps:
|
|||||||
- uses: ./
|
- uses: ./
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
github-token: ${{ github.token }}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The CI workflow provides fast smoke coverage across GitHub-hosted runners, and
|
The CI workflow provides fast smoke coverage across GitHub-hosted runners, and
|
||||||
|
|||||||
44
action.yml
44
action.yml
@@ -5,9 +5,6 @@ inputs:
|
|||||||
version:
|
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. If omitted, detect from the root lockfile and otherwise use latest.
|
||||||
required: false
|
required: false
|
||||||
github-token:
|
|
||||||
description: GitHub token used to resolve the latest Supabase CLI release without hitting unauthenticated API limits.
|
|
||||||
required: false
|
|
||||||
outputs:
|
outputs:
|
||||||
version:
|
version:
|
||||||
description: Version of installed Supabase CLI
|
description: Version of installed Supabase CLI
|
||||||
@@ -15,57 +12,20 @@ outputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
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
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||||
with:
|
with:
|
||||||
bun-version-file: ${{ github.action_path }}/.bun-version
|
bun-version-file: ${{ github.action_path }}/.bun-version
|
||||||
bun-download-url: ${{ steps.bun-download.outputs.url }}
|
|
||||||
|
|
||||||
- name: Install Action Dependencies
|
- name: Install Action Dependencies
|
||||||
shell: sh
|
shell: bash
|
||||||
working-directory: ${{ github.action_path }}
|
working-directory: ${{ github.action_path }}
|
||||||
run: bun install --frozen-lockfile --production
|
run: bun install --frozen-lockfile --production
|
||||||
|
|
||||||
- id: setup-cli
|
- id: setup-cli
|
||||||
name: Setup Supabase CLI
|
name: Setup Supabase CLI
|
||||||
shell: sh
|
shell: bash
|
||||||
working-directory: ${{ github.action_path }}
|
working-directory: ${{ github.action_path }}
|
||||||
env:
|
env:
|
||||||
INPUT_VERSION: ${{ inputs.version }}
|
INPUT_VERSION: ${{ inputs.version }}
|
||||||
SUPABASE_CLI_GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
||||||
run: bun src/main.ts
|
run: bun src/main.ts
|
||||||
|
|||||||
180
src/main.test.ts
180
src/main.test.ts
@@ -10,21 +10,13 @@ import * as tc from "@actions/tool-cache";
|
|||||||
const repo = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
const repo = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||||
const defaultEntrypoint = fileURLToPath(new URL("./main.ts", import.meta.url));
|
const defaultEntrypoint = fileURLToPath(new URL("./main.ts", import.meta.url));
|
||||||
const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
|
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 originalWorkspace = process.env.GITHUB_WORKSPACE;
|
||||||
const originalGithubToken = process.env[GITHUB_TOKEN_ENV];
|
|
||||||
const tempDirs = new Set<string>();
|
const tempDirs = new Set<string>();
|
||||||
let mainModule: typeof import("./main.ts") | null = null;
|
let mainModule: typeof import("./main.ts") | null = null;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
process.env.GITHUB_WORKSPACE = originalWorkspace;
|
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) {
|
for (const dir of tempDirs) {
|
||||||
rmSync(dir, { force: true, recursive: true });
|
rmSync(dir, { force: true, recursive: true });
|
||||||
@@ -164,16 +156,16 @@ function createActionSpies(inputVersion: string, cliDir: string, expectedUrlFrag
|
|||||||
return path.join(os.tmpdir(), "supabase-cli.tar.gz");
|
return path.join(os.tmpdir(), "supabase-cli.tar.gz");
|
||||||
}),
|
}),
|
||||||
extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir),
|
extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir),
|
||||||
extractZip: spyOn(tc, "extractZip").mockImplementation(async () => cliDir),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockLatestRelease(version = "v2.99.0") {
|
function mockLatestRelease(version: string) {
|
||||||
return spyOn(globalThis, "fetch").mockResolvedValue(
|
const response = new Response(null, {
|
||||||
new Response(JSON.stringify({ tag_name: version }), {
|
status: 302,
|
||||||
status: 200,
|
headers: { location: `https://github.com/supabase/cli/releases/tag/v${version}` },
|
||||||
statusText: "OK",
|
});
|
||||||
}),
|
return spyOn(globalThis, "fetch").mockImplementation(
|
||||||
|
(async () => response) as unknown as typeof fetch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,101 +177,10 @@ async function getMainModule(): Promise<typeof import("./main.ts")> {
|
|||||||
return mainModule;
|
return mainModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
test("uses versioned tar archives for Supabase CLI v2.99.0 and later", async () => {
|
|
||||||
const { getDownloadArchive } = await getMainModule();
|
|
||||||
|
|
||||||
const archive = await getDownloadArchive("2.99.0", "linux", "x64");
|
|
||||||
|
|
||||||
expect(archive).toEqual({
|
|
||||||
url: "https://github.com/supabase/cli/releases/download/v2.99.0/supabase_2.99.0_linux_amd64.tar.gz",
|
|
||||||
format: "tar",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
const archive = await getDownloadArchive("2.98.2", "linux", "x64");
|
|
||||||
|
|
||||||
expect(archive).toEqual({
|
|
||||||
url: "https://github.com/supabase/cli/releases/download/v2.98.2/supabase_linux_amd64.tar.gz",
|
|
||||||
format: "tar",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses versioned zip archives for Windows Supabase CLI v2.99.0 and later", async () => {
|
|
||||||
const { getDownloadArchive } = await getMainModule();
|
|
||||||
|
|
||||||
const archive = await getDownloadArchive("2.99.0", "win32", "x64");
|
|
||||||
|
|
||||||
expect(archive).toEqual({
|
|
||||||
url: "https://github.com/supabase/cli/releases/download/v2.99.0/supabase_2.99.0_windows_amd64.zip",
|
|
||||||
format: "zip",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("resolves latest before choosing a versioned Supabase CLI archive", async () => {
|
|
||||||
mockLatestRelease("v2.99.0");
|
|
||||||
const { getDownloadArchive } = await getMainModule();
|
|
||||||
|
|
||||||
const archive = await getDownloadArchive("latest", "darwin", "arm64");
|
|
||||||
|
|
||||||
expect(archive).toEqual({
|
|
||||||
url: "https://github.com/supabase/cli/releases/download/v2.99.0/supabase_2.99.0_darwin_arm64.tar.gz",
|
|
||||||
format: "tar",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 () => {
|
test("awaits the action entrypoint with omitted version and latest fallback", async () => {
|
||||||
process.env.GITHUB_WORKSPACE = repo;
|
process.env.GITHUB_WORKSPACE = repo;
|
||||||
mockLatestRelease();
|
|
||||||
const cliDir = createFakeCli("supabase 2.84.2");
|
const cliDir = createFakeCli("supabase 2.84.2");
|
||||||
|
mockLatestRelease("2.84.2");
|
||||||
let startDownload!: () => void;
|
let startDownload!: () => void;
|
||||||
let finishDownload!: () => void;
|
let finishDownload!: () => void;
|
||||||
const downloadStarted = new Promise<void>((resolve) => {
|
const downloadStarted = new Promise<void>((resolve) => {
|
||||||
@@ -295,12 +196,11 @@ test("awaits the action entrypoint with omitted version and latest fallback", as
|
|||||||
exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}),
|
exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}),
|
||||||
setFailed: spyOn(core, "setFailed").mockImplementation(() => {}),
|
setFailed: spyOn(core, "setFailed").mockImplementation(() => {}),
|
||||||
downloadTool: spyOn(tc, "downloadTool").mockImplementation(async (url: string) => {
|
downloadTool: spyOn(tc, "downloadTool").mockImplementation(async (url: string) => {
|
||||||
expect(url).toContain("/download/v2.99.0/supabase_2.99.0_");
|
expect(url).toContain("/download/v2.84.2/supabase_");
|
||||||
startDownload();
|
startDownload();
|
||||||
return downloadFinished;
|
return downloadFinished;
|
||||||
}),
|
}),
|
||||||
extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir),
|
extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir),
|
||||||
extractZip: spyOn(tc, "extractZip").mockImplementation(async () => cliDir),
|
|
||||||
};
|
};
|
||||||
const originalArgv1 = process.argv[1];
|
const originalArgv1 = process.argv[1];
|
||||||
process.argv[1] = defaultEntrypoint;
|
process.argv[1] = defaultEntrypoint;
|
||||||
@@ -394,9 +294,9 @@ test("falls back to latest when version is omitted and no supported root lockfil
|
|||||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||||
"README.md": "# app\n",
|
"README.md": "# app\n",
|
||||||
});
|
});
|
||||||
mockLatestRelease();
|
|
||||||
const cliDir = createFakeCli("supabase 2.84.2");
|
const cliDir = createFakeCli("supabase 2.84.2");
|
||||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
mockLatestRelease("2.84.2");
|
||||||
|
const spies = createActionSpies("", cliDir, "/download/v2.84.2/supabase_");
|
||||||
const { run } = await getMainModule();
|
const { run } = await getMainModule();
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
@@ -408,9 +308,9 @@ test("falls back to latest when version is omitted and no supported root lockfil
|
|||||||
|
|
||||||
test("falls back to latest when version is omitted and no workspace is available", async () => {
|
test("falls back to latest when version is omitted and no workspace is available", async () => {
|
||||||
delete process.env.GITHUB_WORKSPACE;
|
delete process.env.GITHUB_WORKSPACE;
|
||||||
mockLatestRelease();
|
|
||||||
const cliDir = createFakeCli("supabase 2.84.2");
|
const cliDir = createFakeCli("supabase 2.84.2");
|
||||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
mockLatestRelease("2.84.2");
|
||||||
|
const spies = createActionSpies("", cliDir, "/download/v2.84.2/supabase_");
|
||||||
const { run } = await getMainModule();
|
const { run } = await getMainModule();
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
@@ -473,9 +373,9 @@ test("falls through unreadable bun.lock paths and malformed package-lock files t
|
|||||||
});
|
});
|
||||||
mkdirSync(path.join(workspace, "bun.lock"), { recursive: true });
|
mkdirSync(path.join(workspace, "bun.lock"), { recursive: true });
|
||||||
process.env.GITHUB_WORKSPACE = workspace;
|
process.env.GITHUB_WORKSPACE = workspace;
|
||||||
mockLatestRelease();
|
|
||||||
const cliDir = createFakeCli("supabase 2.84.2");
|
const cliDir = createFakeCli("supabase 2.84.2");
|
||||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
mockLatestRelease("2.84.2");
|
||||||
|
const spies = createActionSpies("", cliDir, "/download/v2.84.2/supabase_");
|
||||||
const { run } = await getMainModule();
|
const { run } = await getMainModule();
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
@@ -489,9 +389,9 @@ test("falls back to latest when a pnpm dependency entry has no concrete version"
|
|||||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||||
"pnpm-lock.yaml": createPnpmLock("2.49.0", { includeVersion: false }),
|
"pnpm-lock.yaml": createPnpmLock("2.49.0", { includeVersion: false }),
|
||||||
});
|
});
|
||||||
mockLatestRelease();
|
|
||||||
const cliDir = createFakeCli("supabase 2.84.2");
|
const cliDir = createFakeCli("supabase 2.84.2");
|
||||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
mockLatestRelease("2.84.2");
|
||||||
|
const spies = createActionSpies("", cliDir, "/download/v2.84.2/supabase_");
|
||||||
const { run } = await getMainModule();
|
const { run } = await getMainModule();
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
@@ -516,6 +416,52 @@ test("explicit version overrides detected root lockfiles", async () => {
|
|||||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("downloads the version-prefixed tarball for releases >= 2.99.0", async () => {
|
||||||
|
// Regression for supabase/cli#5257: from v2.99.0 onward the CLI only
|
||||||
|
// publishes supabase_<version>_<platform>_<arch>.tar.gz; the unversioned
|
||||||
|
// alias is gone.
|
||||||
|
delete process.env.GITHUB_WORKSPACE;
|
||||||
|
const cliDir = createFakeCli("supabase 2.99.0");
|
||||||
|
mockLatestRelease("2.99.0");
|
||||||
|
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
||||||
|
const { run } = await getMainModule();
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.99.0");
|
||||||
|
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||||
|
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("downloads the version-prefixed tarball when explicit version >= 2.99.0 is requested", async () => {
|
||||||
|
delete process.env.GITHUB_WORKSPACE;
|
||||||
|
const cliDir = createFakeCli("supabase 2.99.0");
|
||||||
|
const fetchSpy = spyOn(globalThis, "fetch");
|
||||||
|
const spies = createActionSpies("2.99.0", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
||||||
|
const { run } = await getMainModule();
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.99.0");
|
||||||
|
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||||
|
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when the latest release redirect does not include a version tag", async () => {
|
||||||
|
delete process.env.GITHUB_WORKSPACE;
|
||||||
|
const cliDir = createFakeCli("supabase 2.99.0");
|
||||||
|
const response = new Response(null, { status: 302, headers: { location: "/releases" } });
|
||||||
|
spyOn(globalThis, "fetch").mockImplementation((async () => response) as unknown as typeof fetch);
|
||||||
|
const spies = createActionSpies("", cliDir, "/download/");
|
||||||
|
const { run } = await getMainModule();
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(spies.downloadTool).not.toHaveBeenCalled();
|
||||||
|
expect(spies.setFailed).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test("fails when the installed CLI does not report a version", async () => {
|
test("fails when the installed CLI does not report a version", async () => {
|
||||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||||
"package-lock.json": createPackageLock("2.46.0"),
|
"package-lock.json": createPackageLock("2.46.0"),
|
||||||
|
|||||||
170
src/main.ts
170
src/main.ts
@@ -7,17 +7,12 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
export const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
|
export const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
|
||||||
const REGISTRY_VERSION = "1.28.0";
|
const REGISTRY_VERSION = "1.28.0";
|
||||||
|
// Starting with this release, the CLI publishes only version-prefixed tarballs
|
||||||
|
// (e.g. supabase_2.99.0_linux_amd64.tar.gz); the unversioned aliases that used
|
||||||
|
// to live alongside them are no longer uploaded. See supabase/cli#5257.
|
||||||
const VERSIONED_ARCHIVE_VERSION = "2.99.0";
|
const VERSIONED_ARCHIVE_VERSION = "2.99.0";
|
||||||
const DEFAULT_VERSION = "latest";
|
const DEFAULT_VERSION = "latest";
|
||||||
const GITHUB_RELEASES_API = "https://api.github.com/repos/supabase/cli/releases/latest";
|
const LATEST_RELEASE_URL = "https://github.com/supabase/cli/releases/latest";
|
||||||
const GITHUB_TOKEN_ENV = "SUPABASE_CLI_GITHUB_TOKEN";
|
|
||||||
|
|
||||||
type ArchiveFormat = "apk" | "tar" | "zip";
|
|
||||||
|
|
||||||
type DownloadArchive = {
|
|
||||||
url: string;
|
|
||||||
format: ArchiveFormat;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BunLock = {
|
type BunLock = {
|
||||||
workspaces?: {
|
workspaces?: {
|
||||||
@@ -66,10 +61,6 @@ function extractConcreteVersion(raw: string | undefined): string | null {
|
|||||||
return match?.[0] ?? null;
|
return match?.[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeVersion(version: string): string {
|
|
||||||
return version.replace(/^v/i, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function readWorkspaceLockfile(workspaceRoot: string, filename: string): string | null {
|
function readWorkspaceLockfile(workspaceRoot: string, filename: string): string | null {
|
||||||
const filePath = path.join(workspaceRoot, filename);
|
const filePath = path.join(workspaceRoot, filename);
|
||||||
|
|
||||||
@@ -175,138 +166,40 @@ function resolveVersion(inputVersion: string): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveLatestVersion(): Promise<string> {
|
export async function resolveLatestVersion(): Promise<string> {
|
||||||
const headers: Record<string, string> = {
|
const response = await fetch(LATEST_RELEASE_URL, { method: "HEAD", redirect: "manual" });
|
||||||
Accept: "application/vnd.github+json",
|
const location = response.headers.get("location");
|
||||||
"X-GitHub-Api-Version": "2022-11-28",
|
const version = extractConcreteVersion(location ?? undefined);
|
||||||
};
|
|
||||||
const githubToken = process.env[GITHUB_TOKEN_ENV]?.trim();
|
|
||||||
|
|
||||||
if (githubToken) {
|
if (!version) {
|
||||||
headers.Authorization = `Bearer ${githubToken}`;
|
throw new Error(
|
||||||
|
`Could not resolve latest Supabase CLI version (status ${response.status}, location ${location ?? "<none>"})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(GITHUB_RELEASES_API, { headers });
|
return version;
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to resolve latest Supabase CLI release: ${response.statusText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const release = (await response.json()) as { tag_name?: unknown };
|
export function getDownloadUrl(version: string): string {
|
||||||
if (typeof release.tag_name !== "string") {
|
const platform = getArchivePlatform(process.platform);
|
||||||
throw new Error("Failed to resolve latest Supabase CLI release: missing tag name");
|
const arch = getArchiveArch(process.arch);
|
||||||
}
|
const versionedFilename = `supabase_${version}_${platform}_${arch}.tar.gz`;
|
||||||
|
const unversionedFilename = `supabase_${platform}_${arch}.tar.gz`;
|
||||||
|
|
||||||
return normalizeVersion(release.tag_name);
|
// v2.99.0+ and the earliest releases (pre-v1.28.0) only publish version-prefixed
|
||||||
}
|
// tarballs; the intermediate releases publish unversioned aliases.
|
||||||
|
|
||||||
function getArchiveFormat(
|
|
||||||
version: string,
|
|
||||||
platform: NodeJS.Platform,
|
|
||||||
isMuslLinux: boolean,
|
|
||||||
): ArchiveFormat {
|
|
||||||
if (
|
if (
|
||||||
platform === "linux" &&
|
semver.order(version, REGISTRY_VERSION) === -1 ||
|
||||||
isMuslLinux &&
|
|
||||||
semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0
|
semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0
|
||||||
) {
|
) {
|
||||||
return "apk";
|
return `https://github.com/supabase/cli/releases/download/v${version}/${versionedFilename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform === "win32" && semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0) {
|
return `https://github.com/supabase/cli/releases/download/v${version}/${unversionedFilename}`;
|
||||||
return "zip";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "tar";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArchiveFilename(
|
|
||||||
version: string,
|
|
||||||
platform: NodeJS.Platform,
|
|
||||||
arch: NodeJS.Architecture,
|
|
||||||
archiveFormat: ArchiveFormat,
|
|
||||||
): string {
|
|
||||||
const archivePlatform = getArchivePlatform(platform);
|
|
||||||
const archiveArch = getArchiveArch(arch);
|
|
||||||
|
|
||||||
if (semver.order(version, REGISTRY_VERSION) === -1) {
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `supabase_${archivePlatform}_${archiveArch}.tar.gz`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDownloadArchive(
|
|
||||||
version: string,
|
|
||||||
platform = process.platform,
|
|
||||||
arch = process.arch,
|
|
||||||
isMuslLinux?: boolean,
|
|
||||||
): Promise<DownloadArchive> {
|
|
||||||
const resolvedVersion =
|
|
||||||
version.toLowerCase() === "latest" ? await resolveLatestVersion() : normalizeVersion(version);
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function detectMuslLinux(platform = process.platform): Promise<boolean> {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
const exePath = path.join(cliPath, "supabase.exe");
|
|
||||||
if (existsSync(exePath)) {
|
|
||||||
return exePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmdPath = path.join(cliPath, "supabase.cmd");
|
|
||||||
if (existsSync(cmdPath)) {
|
|
||||||
return cmdPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.join(cliPath, "supabase");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function determineInstalledVersion(cliPath: string): Promise<string> {
|
export async function determineInstalledVersion(cliPath: string): Promise<string> {
|
||||||
const version = (await $`${getCliExecutablePath(cliPath)} --version`.text()).trim();
|
const version = (await $`${path.join(cliPath, "supabase")} --version`.text()).trim();
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw new Error("Could not determine installed Supabase CLI version");
|
throw new Error("Could not determine installed Supabase CLI version");
|
||||||
}
|
}
|
||||||
@@ -316,19 +209,16 @@ export async function determineInstalledVersion(cliPath: string): Promise<string
|
|||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const version = resolveVersion(core.getInput("version"));
|
const requestedVersion = resolveVersion(core.getInput("version"));
|
||||||
const archive = await getDownloadArchive(version);
|
const version =
|
||||||
const archivePath = await tc.downloadTool(archive.url);
|
requestedVersion.toLowerCase() === "latest" ? await resolveLatestVersion() : requestedVersion;
|
||||||
const extractedPath =
|
const tarball = await tc.downloadTool(getDownloadUrl(version));
|
||||||
archive.format === "zip"
|
const cliPath = await tc.extractTar(tarball);
|
||||||
? await tc.extractZip(archivePath)
|
|
||||||
: await tc.extractTar(archivePath);
|
|
||||||
const cliPath = getCliPath(extractedPath, archive.format);
|
|
||||||
const installedVersion = await determineInstalledVersion(cliPath);
|
const installedVersion = await determineInstalledVersion(cliPath);
|
||||||
core.setOutput("version", installedVersion);
|
core.setOutput("version", installedVersion);
|
||||||
core.addPath(cliPath);
|
core.addPath(cliPath);
|
||||||
|
|
||||||
if (version.toLowerCase() === "latest" || semver.order(version, REGISTRY_VERSION) >= 0) {
|
if (semver.order(version, REGISTRY_VERSION) >= 0) {
|
||||||
core.exportVariable(CLI_CONFIG_REGISTRY, "ghcr.io");
|
core.exportVariable(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user