mirror of
https://github.com/supabase/setup-cli.git
synced 2026-06-28 09:57:03 +00:00
Compare commits
1 Commits
codex/read
...
codex/npm-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ad437f39 |
16
README.md
16
README.md
@@ -28,13 +28,10 @@ steps:
|
||||
```
|
||||
|
||||
If `version` is omitted, the action checks the repository root for `bun.lock`,
|
||||
`pnpm-lock.yaml`, or `package-lock.json` and uses the declared `supabase`
|
||||
version. If no supported lockfile is present, it falls back to `latest`.
|
||||
|
||||
When the action resolves `latest`, it queries the GitHub releases API. In CI,
|
||||
pass `github-token: ${{ github.token }}` to avoid unauthenticated API rate
|
||||
limits. Pinning `version` to a specific Supabase CLI release avoids that lookup
|
||||
entirely.
|
||||
`pnpm-lock.yaml`, or `package-lock.json` and installs the declared `supabase`
|
||||
package version through npm. If the lockfile includes package integrity
|
||||
metadata, the action verifies it against the npm registry before installing. If
|
||||
no supported lockfile is present, it falls back to `latest`.
|
||||
|
||||
A specific version of the `supabase` CLI can be installed:
|
||||
|
||||
@@ -52,7 +49,6 @@ steps:
|
||||
- uses: supabase/setup-cli@v2
|
||||
with:
|
||||
version: latest
|
||||
github-token: ${{ github.token }}
|
||||
- run: supabase init
|
||||
- run: supabase db start
|
||||
```
|
||||
@@ -65,9 +61,9 @@ 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 |
|
||||
| `github-token` | String | Deprecated; no longer used now that installs resolve through npm | | false |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ inputs:
|
||||
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.
|
||||
description: Deprecated. The action now installs through npm and does not use GitHub release API requests.
|
||||
required: false
|
||||
outputs:
|
||||
version:
|
||||
@@ -112,5 +112,4 @@ runs:
|
||||
working-directory: ${{ github.action_path }}
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
SUPABASE_CLI_GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||
run: bun src/main.ts
|
||||
|
||||
7
bun.lock
7
bun.lock
@@ -6,7 +6,6 @@
|
||||
"name": "setup-cli",
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/tool-cache": "^4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "^1.0.10",
|
||||
@@ -27,8 +26,6 @@
|
||||
|
||||
"@actions/io": ["@actions/io@3.0.2", "", {}, "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw=="],
|
||||
|
||||
"@actions/tool-cache": ["@actions/tool-cache@4.0.0", "", { "dependencies": { "@actions/core": "^3.0.0", "@actions/exec": "^3.0.0", "@actions/http-client": "^4.0.0", "@actions/io": "^3.0.0", "semver": "^7.7.3" } }, "sha512-L8P9HbXvpvqjZDveb/fdsa55IVC0trfPgQ4ZwGo6r5af6YDVdM9vMGPZ7rgY2fAT9gGj4PSYd6bYlg3p3jD78A=="],
|
||||
|
||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.49.0", "", { "os": "android", "cpu": "arm" }, "sha512-HbifJ84prIh9+55CTPAU35JdRQrwg47y16cGerCC+iejSKOuHXYo2WDql6l7cQlzrYVtc3f4UWY+dBj2lRmOeA=="],
|
||||
|
||||
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.49.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ef7SKJqAaH2d7E6eXZZa2OffIShbhFMxnGK0zd93p4qiyTJr75B0qf7lrPD+qQOwcf04BrjYJ0JUxq8d5+yZwg=="],
|
||||
@@ -147,8 +144,6 @@
|
||||
|
||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.22.1", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.22.1", "@oxlint-tsgolint/darwin-x64": "0.22.1", "@oxlint-tsgolint/linux-arm64": "0.22.1", "@oxlint-tsgolint/linux-x64": "0.22.1", "@oxlint-tsgolint/win32-arm64": "0.22.1", "@oxlint-tsgolint/win32-x64": "0.22.1" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-YUSGSLUnoolsu8gxISEDio3q1rtsCozwfOzASUn3DT2mR2EeQ93uEEnen7s+6LpF+lyTQFln1pQfqwBh/fsVEg=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
||||
|
||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||
@@ -156,7 +151,5 @@
|
||||
"undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"@actions/tool-cache/@actions/core": ["@actions/core@3.0.0", "", { "dependencies": { "@actions/exec": "^3.0.0", "@actions/http-client": "^4.0.0" } }, "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
"typecheck": "bun x tsgo -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/tool-cache": "^4.0.0"
|
||||
"@actions/core": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "^1.0.10",
|
||||
|
||||
577
src/main.test.ts
577
src/main.test.ts
@@ -1,30 +1,26 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, expect, mock, spyOn, test } from "bun:test";
|
||||
import * as core from "@actions/core";
|
||||
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 originalPath = process.env.PATH;
|
||||
const originalRunnerTemp = process.env.RUNNER_TEMP;
|
||||
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.PATH = originalPath;
|
||||
process.env.RUNNER_TEMP = originalRunnerTemp;
|
||||
process.env.GITHUB_WORKSPACE = originalWorkspace;
|
||||
if (originalGithubToken === undefined) {
|
||||
delete process.env[GITHUB_TOKEN_ENV];
|
||||
} else {
|
||||
process.env[GITHUB_TOKEN_ENV] = originalGithubToken;
|
||||
}
|
||||
delete process.env.FAKE_CLI_VERSION;
|
||||
delete process.env.FAKE_NPM_INTEGRITY;
|
||||
delete process.env.FAKE_NPM_LOG;
|
||||
delete process.env.SUPABASE_SETUP_CLI_NPM;
|
||||
|
||||
for (const dir of tempDirs) {
|
||||
rmSync(dir, { force: true, recursive: true });
|
||||
@@ -32,32 +28,14 @@ afterEach(() => {
|
||||
tempDirs.clear();
|
||||
});
|
||||
|
||||
function createFakeCli(versionOutput: string): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "setup-cli-"));
|
||||
function createTempDir(prefix: string): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
tempDirs.add(dir);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
writeFileSync(
|
||||
path.join(dir, "supabase.cmd"),
|
||||
versionOutput ? `@echo off\r\necho ${versionOutput}\r\n` : "@echo off\r\n",
|
||||
);
|
||||
return dir;
|
||||
}
|
||||
|
||||
const escapedOutput = versionOutput.replaceAll("'", "'\"'\"'");
|
||||
writeFileSync(
|
||||
path.join(dir, "supabase"),
|
||||
versionOutput
|
||||
? `#!/usr/bin/env bash\nprintf '%s\\n' '${escapedOutput}'\n`
|
||||
: "#!/usr/bin/env bash\n",
|
||||
);
|
||||
Bun.spawnSync(["chmod", "+x", path.join(dir, "supabase")]);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function createWorkspace(files: Record<string, string>): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "setup-cli-workspace-"));
|
||||
tempDirs.add(dir);
|
||||
const dir = createTempDir("setup-cli-workspace-");
|
||||
|
||||
for (const [relativePath, content] of Object.entries(files)) {
|
||||
const filePath = path.join(dir, relativePath);
|
||||
@@ -73,6 +51,7 @@ function createBunLock(
|
||||
options: {
|
||||
includeDependency?: boolean;
|
||||
includePackageEntry?: boolean;
|
||||
integrity?: string;
|
||||
useDevDependency?: boolean;
|
||||
} = {},
|
||||
): string {
|
||||
@@ -98,7 +77,7 @@ ${
|
||||
"supabase@${version}",
|
||||
"",
|
||||
{},
|
||||
"sha512-test"
|
||||
"${options.integrity ?? "sha512-bun"}"
|
||||
]`
|
||||
: ""
|
||||
}
|
||||
@@ -109,7 +88,12 @@ ${
|
||||
|
||||
function createPnpmLock(
|
||||
version: string,
|
||||
options: { asString?: boolean; includeVersion?: boolean; useDevDependency?: boolean } = {},
|
||||
options: {
|
||||
asString?: boolean;
|
||||
includeVersion?: boolean;
|
||||
integrity?: string;
|
||||
useDevDependency?: boolean;
|
||||
} = {},
|
||||
): string {
|
||||
const dependencyKey = options.useDevDependency ? "devDependencies" : "dependencies";
|
||||
|
||||
@@ -127,11 +111,11 @@ ${options.includeVersion === false ? "" : ` version: ${version}`}`
|
||||
packages:
|
||||
supabase@${version}:
|
||||
resolution:
|
||||
integrity: sha512-test
|
||||
integrity: ${options.integrity ?? "sha512-pnpm"}
|
||||
`;
|
||||
}
|
||||
|
||||
function createPackageLock(version: string): string {
|
||||
function createPackageLock(version: string, integrity = "sha512-package-lock"): string {
|
||||
return JSON.stringify(
|
||||
{
|
||||
name: "app",
|
||||
@@ -143,6 +127,7 @@ function createPackageLock(version: string): string {
|
||||
},
|
||||
},
|
||||
"node_modules/supabase": {
|
||||
integrity,
|
||||
version,
|
||||
},
|
||||
},
|
||||
@@ -152,29 +137,105 @@ function createPackageLock(version: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
function createActionSpies(inputVersion: string, cliDir: string, expectedUrlFragment: string) {
|
||||
return {
|
||||
getInput: spyOn(core, "getInput").mockReturnValue(inputVersion),
|
||||
setOutput: spyOn(core, "setOutput").mockImplementation(() => {}),
|
||||
addPath: spyOn(core, "addPath").mockImplementation(() => {}),
|
||||
exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}),
|
||||
setFailed: spyOn(core, "setFailed").mockImplementation(() => {}),
|
||||
downloadTool: spyOn(tc, "downloadTool").mockImplementation(async (url: string) => {
|
||||
expect(url).toContain(expectedUrlFragment);
|
||||
return path.join(os.tmpdir(), "supabase-cli.tar.gz");
|
||||
}),
|
||||
extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir),
|
||||
extractZip: spyOn(tc, "extractZip").mockImplementation(async () => cliDir),
|
||||
};
|
||||
function createFakeNpm(): string {
|
||||
const root = createTempDir("setup-cli-fake-npm-");
|
||||
const binDir = path.join(root, "bin");
|
||||
const scriptPath = path.join(root, "fake-npm.js");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
writeFileSync(
|
||||
scriptPath,
|
||||
`import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
appendFileSync(process.env.FAKE_NPM_LOG, JSON.stringify(args) + "\\n");
|
||||
|
||||
if (args[0] === "view") {
|
||||
console.log(JSON.stringify(process.env.FAKE_NPM_INTEGRITY ?? "sha512-test"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function mockLatestRelease(version = "v2.99.0") {
|
||||
return spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ tag_name: version }), {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
}),
|
||||
if (args[0] !== "install") {
|
||||
console.error("Unexpected npm command: " + args.join(" "));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const prefixIndex = args.indexOf("--prefix");
|
||||
const prefix = prefixIndex === -1 ? undefined : args[prefixIndex + 1];
|
||||
if (!prefix) {
|
||||
console.error("Missing --prefix");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const binDir = path.join(prefix, "node_modules", ".bin");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
|
||||
if (process.platform === "win32") {
|
||||
writeFileSync(
|
||||
path.join(binDir, "supabase.cmd"),
|
||||
process.env.FAKE_CLI_VERSION ? "@echo off\\r\\necho " + process.env.FAKE_CLI_VERSION + "\\r\\n" : "@echo off\\r\\n",
|
||||
);
|
||||
} else {
|
||||
writeFileSync(
|
||||
path.join(binDir, "supabase"),
|
||||
process.env.FAKE_CLI_VERSION
|
||||
? "#!/usr/bin/env bash\\nprintf '%s\\\\n' '" + process.env.FAKE_CLI_VERSION.replaceAll("'", "'\\\\''") + "'\\n"
|
||||
: "#!/usr/bin/env bash\\n",
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
writeFileSync(
|
||||
path.join(binDir, "npm.cmd"),
|
||||
`@echo off\r\n"${process.execPath}" "${scriptPath}" %*\r\n`,
|
||||
);
|
||||
} else {
|
||||
writeFileSync(
|
||||
path.join(binDir, "npm"),
|
||||
`#!/usr/bin/env bash\nexec "${process.execPath}" "${scriptPath}" "$@"\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
}
|
||||
|
||||
return binDir;
|
||||
}
|
||||
|
||||
function installFakeNpm(versionOutput = "supabase 2.101.0", integrity = "sha512-test"): string {
|
||||
const binDir = createFakeNpm();
|
||||
const logPath = path.join(createTempDir("setup-cli-fake-npm-log-"), "npm.log");
|
||||
writeFileSync(logPath, "");
|
||||
process.env.FAKE_CLI_VERSION = versionOutput;
|
||||
process.env.FAKE_NPM_INTEGRITY = integrity;
|
||||
process.env.FAKE_NPM_LOG = logPath;
|
||||
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ""}`;
|
||||
process.env.RUNNER_TEMP = createTempDir("setup-cli-runner-temp-");
|
||||
process.env.SUPABASE_SETUP_CLI_NPM = path.join(
|
||||
binDir,
|
||||
process.platform === "win32" ? "npm.cmd" : "npm",
|
||||
);
|
||||
|
||||
return logPath;
|
||||
}
|
||||
|
||||
function readNpmCalls(logPath: string): string[][] {
|
||||
return readFileSync(logPath, "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line) as string[]);
|
||||
}
|
||||
|
||||
function createActionSpies(inputVersion: string) {
|
||||
return {
|
||||
addPath: spyOn(core, "addPath").mockImplementation(() => {}),
|
||||
exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}),
|
||||
getInput: spyOn(core, "getInput").mockReturnValue(inputVersion),
|
||||
setFailed: spyOn(core, "setFailed").mockImplementation(() => {}),
|
||||
setOutput: spyOn(core, "setOutput").mockImplementation(() => {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function getMainModule(): Promise<typeof import("./main.ts")> {
|
||||
@@ -185,193 +246,52 @@ async function getMainModule(): Promise<typeof import("./main.ts")> {
|
||||
return mainModule;
|
||||
}
|
||||
|
||||
test("uses versioned tar archives for Supabase CLI v2.99.0 and later", async () => {
|
||||
const { getDownloadArchive } = await getMainModule();
|
||||
test("uses an explicit npm package version when provided", async () => {
|
||||
const { resolvePackage } = 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",
|
||||
expect(resolvePackage("v2.101.0")).toEqual({
|
||||
spec: "supabase@2.101.0",
|
||||
version: "2.101.0",
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
process.env.GITHUB_WORKSPACE = repo;
|
||||
mockLatestRelease();
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
let startDownload!: () => void;
|
||||
let finishDownload!: () => void;
|
||||
const downloadStarted = new Promise<void>((resolve) => {
|
||||
startDownload = resolve;
|
||||
});
|
||||
const downloadFinished = new Promise<string>((resolve) => {
|
||||
finishDownload = () => resolve(path.join(os.tmpdir(), "supabase-cli.tar.gz"));
|
||||
});
|
||||
const spies = {
|
||||
getInput: spyOn(core, "getInput").mockReturnValue(""),
|
||||
setOutput: spyOn(core, "setOutput").mockImplementation(() => {}),
|
||||
addPath: spyOn(core, "addPath").mockImplementation(() => {}),
|
||||
exportVariable: spyOn(core, "exportVariable").mockImplementation(() => {}),
|
||||
setFailed: spyOn(core, "setFailed").mockImplementation(() => {}),
|
||||
downloadTool: spyOn(tc, "downloadTool").mockImplementation(async (url: string) => {
|
||||
expect(url).toContain("/download/v2.99.0/supabase_2.99.0_");
|
||||
startDownload();
|
||||
return downloadFinished;
|
||||
}),
|
||||
extractTar: spyOn(tc, "extractTar").mockImplementation(async () => cliDir),
|
||||
extractZip: spyOn(tc, "extractZip").mockImplementation(async () => cliDir),
|
||||
};
|
||||
const originalArgv1 = process.argv[1];
|
||||
process.argv[1] = defaultEntrypoint;
|
||||
|
||||
try {
|
||||
let importSettled = false;
|
||||
const entrypoint = import(`./main.ts?entrypoint=${Date.now()}`).finally(() => {
|
||||
importSettled = true;
|
||||
});
|
||||
|
||||
await downloadStarted;
|
||||
await Bun.sleep(0);
|
||||
|
||||
expect(importSettled).toBe(false);
|
||||
|
||||
finishDownload();
|
||||
await entrypoint;
|
||||
} finally {
|
||||
process.argv[1] = originalArgv1 ?? "";
|
||||
}
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2");
|
||||
expect(spies.addPath).toHaveBeenCalledWith(cliDir);
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("uses the root bun.lock version when version is omitted", async () => {
|
||||
test("uses the root bun.lock resolution when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"bun.lock": createBunLock("2.41.0"),
|
||||
"bun.lock": createBunLock("2.41.0", { integrity: "sha512-bun-lock" }),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.41.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.41.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.downloadTool).not.toHaveBeenCalledWith(expect.stringContaining("/latest/download/"));
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.41.0");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-bun-lock",
|
||||
spec: "supabase@2.41.0",
|
||||
version: "2.41.0",
|
||||
});
|
||||
});
|
||||
|
||||
test("uses the root pnpm-lock.yaml version when version is omitted", async () => {
|
||||
test("uses the root pnpm-lock.yaml resolution when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"pnpm-lock.yaml": createPnpmLock("2.42.0"),
|
||||
"pnpm-lock.yaml": createPnpmLock("2.42.0", { integrity: "sha512-pnpm-lock" }),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.42.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.42.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.42.0");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-pnpm-lock",
|
||||
spec: "supabase@2.42.0",
|
||||
version: "2.42.0",
|
||||
});
|
||||
});
|
||||
|
||||
test("uses the root package-lock.json version when version is omitted", async () => {
|
||||
test("uses the root package-lock.json resolution when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"package-lock.json": createPackageLock("2.43.0"),
|
||||
"package-lock.json": createPackageLock("2.43.0", "sha512-package-lock"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.43.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.43.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.43.0");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-package-lock",
|
||||
spec: "supabase@2.43.0",
|
||||
version: "2.43.0",
|
||||
});
|
||||
});
|
||||
|
||||
test("falls through malformed lockfiles and uses the next supported root lockfile", async () => {
|
||||
@@ -379,60 +299,47 @@ test("falls through malformed lockfiles and uses the next supported root lockfil
|
||||
"bun.lock": "{ not valid",
|
||||
"package-lock.json": createPackageLock("2.44.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.44.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.44.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.44.0");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-package-lock",
|
||||
spec: "supabase@2.44.0",
|
||||
version: "2.44.0",
|
||||
});
|
||||
});
|
||||
|
||||
test("falls back to latest when version is omitted and no supported root lockfile is present", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"README.md": "# app\n",
|
||||
});
|
||||
mockLatestRelease();
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
||||
const { run } = await getMainModule();
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
expect(resolvePackage("")).toEqual({
|
||||
spec: "supabase@latest",
|
||||
version: "latest",
|
||||
});
|
||||
});
|
||||
|
||||
test("falls back to latest when version is omitted and no workspace is available", async () => {
|
||||
delete process.env.GITHUB_WORKSPACE;
|
||||
mockLatestRelease();
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
||||
const { run } = await getMainModule();
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
expect(resolvePackage("")).toEqual({
|
||||
spec: "supabase@latest",
|
||||
version: "latest",
|
||||
});
|
||||
});
|
||||
|
||||
test("uses the declared bun.lock version when the resolved package entry is missing", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"bun.lock": createBunLock("2.44.1", { includePackageEntry: false, useDevDependency: true }),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.44.1");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.44.1/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.44.1");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
expect(resolvePackage("")).toEqual({
|
||||
spec: "supabase@2.44.1",
|
||||
version: "2.44.1",
|
||||
});
|
||||
});
|
||||
|
||||
test("falls through bun.lock without supabase and uses a pnpm string dependency version", async () => {
|
||||
@@ -440,73 +347,119 @@ test("falls through bun.lock without supabase and uses a pnpm string dependency
|
||||
"bun.lock": createBunLock("2.47.0", { includeDependency: false }),
|
||||
"pnpm-lock.yaml": createPnpmLock("2.47.0", { asString: true }),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.47.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.47.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.47.0");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("falls through malformed pnpm lockfiles and uses the next supported root lockfile", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"pnpm-lock.yaml": "not: [valid",
|
||||
"package-lock.json": createPackageLock("2.48.0"),
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-pnpm",
|
||||
spec: "supabase@2.47.0",
|
||||
version: "2.47.0",
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.48.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.48.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.48.0");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("falls through unreadable bun.lock paths and malformed package-lock files to latest", async () => {
|
||||
const workspace = createWorkspace({
|
||||
"package-lock.json": "{ invalid",
|
||||
});
|
||||
mkdirSync(path.join(workspace, "bun.lock"), { recursive: true });
|
||||
process.env.GITHUB_WORKSPACE = workspace;
|
||||
mockLatestRelease();
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
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.84.2");
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("falls back to latest when a pnpm dependency entry has no concrete version", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"pnpm-lock.yaml": createPnpmLock("2.49.0", { includeVersion: false }),
|
||||
});
|
||||
mockLatestRelease();
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
||||
const { resolvePackage } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
spec: "supabase@latest",
|
||||
version: "latest",
|
||||
});
|
||||
});
|
||||
|
||||
test("installs the CLI with npm into an isolated prefix", async () => {
|
||||
const logPath = installFakeNpm();
|
||||
const { installCli } = await getMainModule();
|
||||
|
||||
const cliPath = await installCli({
|
||||
spec: "supabase@2.101.0",
|
||||
version: "2.101.0",
|
||||
});
|
||||
|
||||
expect(cliPath).toContain(`${path.sep}node_modules${path.sep}.bin`);
|
||||
expect(readNpmCalls(logPath)).toEqual([
|
||||
[
|
||||
"install",
|
||||
"--prefix",
|
||||
expect.any(String),
|
||||
"--omit=dev",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--no-package-lock",
|
||||
"--ignore-scripts",
|
||||
"supabase@2.101.0",
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test("verifies lockfile integrity before installing", async () => {
|
||||
const logPath = installFakeNpm("supabase 2.101.0", "sha512-lock");
|
||||
const { installCli } = await getMainModule();
|
||||
|
||||
await installCli({
|
||||
integrity: "sha512-lock",
|
||||
spec: "supabase@2.101.0",
|
||||
version: "2.101.0",
|
||||
});
|
||||
|
||||
expect(readNpmCalls(logPath)).toEqual([
|
||||
["view", "supabase@2.101.0", "dist.integrity", "--json"],
|
||||
[
|
||||
"install",
|
||||
"--prefix",
|
||||
expect.any(String),
|
||||
"--omit=dev",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--no-package-lock",
|
||||
"--ignore-scripts",
|
||||
"supabase@2.101.0",
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test("fails when lockfile integrity does not match the registry", async () => {
|
||||
installFakeNpm("supabase 2.101.0", "sha512-registry");
|
||||
const { installCli } = await getMainModule();
|
||||
|
||||
try {
|
||||
await installCli({
|
||||
integrity: "sha512-lock",
|
||||
spec: "supabase@2.101.0",
|
||||
version: "2.101.0",
|
||||
});
|
||||
throw new Error("Expected installCli to reject");
|
||||
} catch (error) {
|
||||
expect(error).toEqual(
|
||||
new Error("Lockfile integrity for supabase@2.101.0 does not match the npm registry"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("runs the action with a package-lock resolution", async () => {
|
||||
const logPath = installFakeNpm("supabase 2.43.0", "sha512-package-lock");
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"package-lock.json": createPackageLock("2.43.0", "sha512-package-lock"),
|
||||
});
|
||||
const spies = createActionSpies("");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2");
|
||||
expect(readNpmCalls(logPath)[0]).toEqual(["view", "supabase@2.43.0", "dist.integrity", "--json"]);
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.43.0");
|
||||
expect(spies.addPath).toHaveBeenCalledWith(expect.stringContaining("node_modules"));
|
||||
expect(spies.exportVariable).toHaveBeenCalledWith(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("explicit version overrides detected root lockfiles", async () => {
|
||||
installFakeNpm("supabase 1.0.0");
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"bun.lock": createBunLock("2.45.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 1.0.0");
|
||||
const spies = createActionSpies("1.0.0", cliDir, "/download/v1.0.0/supabase_1.0.0_");
|
||||
const spies = createActionSpies("1.0.0");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
await run();
|
||||
@@ -517,11 +470,11 @@ test("explicit version overrides detected root lockfiles", async () => {
|
||||
});
|
||||
|
||||
test("fails when the installed CLI does not report a version", async () => {
|
||||
installFakeNpm("");
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"package-lock.json": createPackageLock("2.46.0"),
|
||||
"package-lock.json": createPackageLock("2.46.0", "sha512-test"),
|
||||
});
|
||||
const cliDir = createFakeCli("");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.46.0/supabase_");
|
||||
const spies = createActionSpies("");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
313
src/main.ts
313
src/main.ts
@@ -1,22 +1,20 @@
|
||||
import { $, semver } from "bun";
|
||||
import { semver } from "bun";
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
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 GITHUB_RELEASES_API = "https://api.github.com/repos/supabase/cli/releases/latest";
|
||||
const GITHUB_TOKEN_ENV = "SUPABASE_CLI_GITHUB_TOKEN";
|
||||
const NPM_PACKAGE = "supabase";
|
||||
const NPM_EXECUTABLE_ENV = "SUPABASE_SETUP_CLI_NPM";
|
||||
|
||||
type ArchiveFormat = "apk" | "tar" | "zip";
|
||||
|
||||
type DownloadArchive = {
|
||||
url: string;
|
||||
format: ArchiveFormat;
|
||||
type PackageResolution = {
|
||||
spec: string;
|
||||
version: string;
|
||||
integrity?: string;
|
||||
};
|
||||
|
||||
type BunLock = {
|
||||
@@ -35,6 +33,12 @@ type PnpmDependency =
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type PnpmPackage = {
|
||||
resolution?: {
|
||||
integrity?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type PnpmLock = {
|
||||
importers?: {
|
||||
".": {
|
||||
@@ -42,21 +46,14 @@ type PnpmLock = {
|
||||
devDependencies?: Record<string, PnpmDependency>;
|
||||
};
|
||||
};
|
||||
packages?: Record<string, PnpmPackage>;
|
||||
};
|
||||
|
||||
type PackageLock = {
|
||||
packages?: Record<string, { version?: string }>;
|
||||
dependencies?: Record<string, { version?: string }>;
|
||||
packages?: Record<string, { integrity?: string; version?: string }>;
|
||||
dependencies?: Record<string, { integrity?: string; version?: string }>;
|
||||
};
|
||||
|
||||
function getArchivePlatform(platform: NodeJS.Platform): string {
|
||||
return platform === "win32" ? "windows" : platform;
|
||||
}
|
||||
|
||||
function getArchiveArch(arch: NodeJS.Architecture): string {
|
||||
return arch === "x64" ? "amd64" : arch;
|
||||
}
|
||||
|
||||
function extractConcreteVersion(raw: string | undefined): string | null {
|
||||
if (!raw) {
|
||||
return null;
|
||||
@@ -70,6 +67,16 @@ function normalizeVersion(version: string): string {
|
||||
return version.replace(/^v/i, "");
|
||||
}
|
||||
|
||||
function toPackageResolution(version: string, integrity?: string): PackageResolution {
|
||||
const normalizedVersion = normalizeVersion(version);
|
||||
|
||||
return {
|
||||
spec: `${NPM_PACKAGE}@${normalizedVersion}`,
|
||||
version: normalizedVersion,
|
||||
integrity,
|
||||
};
|
||||
}
|
||||
|
||||
function readWorkspaceLockfile(workspaceRoot: string, filename: string): string | null {
|
||||
const filePath = path.join(workspaceRoot, filename);
|
||||
|
||||
@@ -84,7 +91,7 @@ function readWorkspaceLockfile(workspaceRoot: string, filename: string): string
|
||||
}
|
||||
}
|
||||
|
||||
function detectVersionFromBunLock(workspaceRoot: string): string | null {
|
||||
function detectResolutionFromBunLock(workspaceRoot: string): PackageResolution | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "bun.lock");
|
||||
|
||||
if (!text) {
|
||||
@@ -95,24 +102,28 @@ function detectVersionFromBunLock(workspaceRoot: string): string | null {
|
||||
const lockfile = JSON.parse(text.replace(/,\s*([}\]])/g, "$1")) as BunLock;
|
||||
const rootWorkspace = lockfile.workspaces?.[""];
|
||||
const declaredVersion =
|
||||
rootWorkspace?.dependencies?.supabase ?? rootWorkspace?.devDependencies?.supabase;
|
||||
rootWorkspace?.dependencies?.[NPM_PACKAGE] ?? rootWorkspace?.devDependencies?.[NPM_PACKAGE];
|
||||
|
||||
if (!declaredVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedPackage = lockfile.packages?.supabase;
|
||||
const resolvedPackage = lockfile.packages?.[NPM_PACKAGE];
|
||||
if (Array.isArray(resolvedPackage) && typeof resolvedPackage[0] === "string") {
|
||||
return extractConcreteVersion(resolvedPackage[0]);
|
||||
const version = extractConcreteVersion(resolvedPackage[0]);
|
||||
const integrity = typeof resolvedPackage[3] === "string" ? resolvedPackage[3] : undefined;
|
||||
|
||||
return version ? toPackageResolution(version, integrity) : null;
|
||||
}
|
||||
|
||||
return extractConcreteVersion(declaredVersion);
|
||||
const version = extractConcreteVersion(declaredVersion);
|
||||
return version ? toPackageResolution(version) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function detectVersionFromPnpmLock(workspaceRoot: string): string | null {
|
||||
function detectResolutionFromPnpmLock(workspaceRoot: string): PackageResolution | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "pnpm-lock.yaml");
|
||||
|
||||
if (!text) {
|
||||
@@ -123,19 +134,29 @@ function detectVersionFromPnpmLock(workspaceRoot: string): string | null {
|
||||
const lockfile = Bun.YAML.parse(text) as PnpmLock;
|
||||
const rootImporter = lockfile.importers?.["."];
|
||||
const dependency =
|
||||
rootImporter?.dependencies?.supabase ?? rootImporter?.devDependencies?.supabase;
|
||||
rootImporter?.dependencies?.[NPM_PACKAGE] ?? rootImporter?.devDependencies?.[NPM_PACKAGE];
|
||||
const version =
|
||||
typeof dependency === "string"
|
||||
? extractConcreteVersion(dependency)
|
||||
: extractConcreteVersion(dependency?.version);
|
||||
|
||||
if (typeof dependency === "string") {
|
||||
return extractConcreteVersion(dependency);
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return extractConcreteVersion(dependency?.version);
|
||||
const integrity = Object.entries(lockfile.packages ?? {}).find(
|
||||
([packageKey]) =>
|
||||
packageKey === `${NPM_PACKAGE}@${version}` ||
|
||||
packageKey.startsWith(`/${NPM_PACKAGE}@${version}`),
|
||||
)?.[1].resolution?.integrity;
|
||||
|
||||
return toPackageResolution(version, integrity);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function detectVersionFromPackageLock(workspaceRoot: string): string | null {
|
||||
function detectResolutionFromPackageLock(workspaceRoot: string): PackageResolution | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "package-lock.json");
|
||||
|
||||
if (!text) {
|
||||
@@ -144,147 +165,97 @@ function detectVersionFromPackageLock(workspaceRoot: string): string | null {
|
||||
|
||||
try {
|
||||
const lockfile = JSON.parse(text) as PackageLock;
|
||||
const packageEntry = lockfile.packages?.[`node_modules/${NPM_PACKAGE}`];
|
||||
const dependencyEntry = lockfile.dependencies?.[NPM_PACKAGE];
|
||||
const version =
|
||||
extractConcreteVersion(packageEntry?.version) ??
|
||||
extractConcreteVersion(dependencyEntry?.version);
|
||||
|
||||
return (
|
||||
extractConcreteVersion(lockfile.packages?.["node_modules/supabase"]?.version) ??
|
||||
extractConcreteVersion(lockfile.dependencies?.supabase?.version)
|
||||
);
|
||||
return version
|
||||
? toPackageResolution(version, packageEntry?.integrity ?? dependencyEntry?.integrity)
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersion(inputVersion: string): string {
|
||||
export function resolvePackage(inputVersion: string): PackageResolution {
|
||||
const requestedVersion = inputVersion.trim();
|
||||
|
||||
if (requestedVersion) {
|
||||
return requestedVersion;
|
||||
return toPackageResolution(requestedVersion);
|
||||
}
|
||||
|
||||
const workspaceRoot = process.env.GITHUB_WORKSPACE?.trim();
|
||||
|
||||
if (!workspaceRoot) {
|
||||
return DEFAULT_VERSION;
|
||||
return toPackageResolution(DEFAULT_VERSION);
|
||||
}
|
||||
|
||||
return (
|
||||
detectVersionFromBunLock(workspaceRoot) ??
|
||||
detectVersionFromPnpmLock(workspaceRoot) ??
|
||||
detectVersionFromPackageLock(workspaceRoot) ??
|
||||
DEFAULT_VERSION
|
||||
detectResolutionFromBunLock(workspaceRoot) ??
|
||||
detectResolutionFromPnpmLock(workspaceRoot) ??
|
||||
detectResolutionFromPackageLock(workspaceRoot) ??
|
||||
toPackageResolution(DEFAULT_VERSION)
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveLatestVersion(): Promise<string> {
|
||||
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}`;
|
||||
async function verifyExpectedIntegrity(resolution: PackageResolution): Promise<void> {
|
||||
if (!resolution.integrity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(GITHUB_RELEASES_API, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to resolve latest Supabase CLI release: ${response.statusText}`);
|
||||
}
|
||||
const output = await runNpm(["view", resolution.spec, "dist.integrity", "--json"]);
|
||||
const registryIntegrity = JSON.parse(output) as unknown;
|
||||
|
||||
const release = (await response.json()) as { tag_name?: unknown };
|
||||
if (typeof release.tag_name !== "string") {
|
||||
throw new Error("Failed to resolve latest Supabase CLI release: missing tag name");
|
||||
}
|
||||
|
||||
return normalizeVersion(release.tag_name);
|
||||
}
|
||||
|
||||
function getArchiveFormat(
|
||||
version: string,
|
||||
platform: NodeJS.Platform,
|
||||
isMuslLinux: boolean,
|
||||
): ArchiveFormat {
|
||||
if (
|
||||
platform === "linux" &&
|
||||
isMuslLinux &&
|
||||
semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0
|
||||
) {
|
||||
return "apk";
|
||||
}
|
||||
|
||||
if (platform === "win32" && semver.order(version, VERSIONED_ARCHIVE_VERSION) >= 0) {
|
||||
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");
|
||||
if (registryIntegrity !== resolution.integrity) {
|
||||
throw new Error(`Lockfile integrity for ${resolution.spec} does not match the npm registry`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCliPath(extractedPath: string, archiveFormat: ArchiveFormat): string {
|
||||
return archiveFormat === "apk" ? path.join(extractedPath, "usr", "bin") : extractedPath;
|
||||
function createInstallRoot(): string {
|
||||
const tempRoot = process.env.RUNNER_TEMP?.trim() || os.tmpdir();
|
||||
return mkdtempSync(path.join(tempRoot, "setup-cli-"));
|
||||
}
|
||||
|
||||
async function runNpm(args: string[]): Promise<string> {
|
||||
const executable = process.env[NPM_EXECUTABLE_ENV]?.trim() || "npm";
|
||||
const proc = Bun.spawn([executable, ...args], {
|
||||
env: process.env,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(stderr.trim() || `npm ${args.join(" ")} failed`);
|
||||
}
|
||||
|
||||
return stdout;
|
||||
}
|
||||
|
||||
export async function installCli(resolution: PackageResolution): Promise<string> {
|
||||
await verifyExpectedIntegrity(resolution);
|
||||
|
||||
const installRoot = createInstallRoot();
|
||||
|
||||
await runNpm([
|
||||
"install",
|
||||
"--prefix",
|
||||
installRoot,
|
||||
"--omit=dev",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--no-package-lock",
|
||||
"--ignore-scripts",
|
||||
resolution.spec,
|
||||
]);
|
||||
|
||||
return path.join(installRoot, "node_modules", ".bin");
|
||||
}
|
||||
|
||||
function getCliExecutablePath(cliPath: string): string {
|
||||
@@ -292,21 +263,36 @@ function getCliExecutablePath(cliPath: string): string {
|
||||
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;
|
||||
}
|
||||
|
||||
const exePath = path.join(cliPath, "supabase.exe");
|
||||
if (existsSync(exePath)) {
|
||||
return exePath;
|
||||
}
|
||||
|
||||
return path.join(cliPath, "supabase");
|
||||
}
|
||||
|
||||
export async function determineInstalledVersion(cliPath: string): Promise<string> {
|
||||
const version = (await $`${getCliExecutablePath(cliPath)} --version`.text()).trim();
|
||||
const executable = getCliExecutablePath(cliPath);
|
||||
const proc = Bun.spawn([executable, "--version"], {
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(stderr.trim() || "Could not determine installed Supabase CLI version");
|
||||
}
|
||||
|
||||
const version = stdout.trim();
|
||||
if (!version) {
|
||||
throw new Error("Could not determine installed Supabase CLI version");
|
||||
}
|
||||
@@ -314,21 +300,24 @@ export async function determineInstalledVersion(cliPath: string): Promise<string
|
||||
return version;
|
||||
}
|
||||
|
||||
function shouldUseGhcrRegistry(requestedVersion: string, installedVersion: string): boolean {
|
||||
if (requestedVersion.toLowerCase() === DEFAULT_VERSION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const concreteVersion = extractConcreteVersion(installedVersion);
|
||||
return concreteVersion !== null && semver.order(concreteVersion, REGISTRY_VERSION) >= 0;
|
||||
}
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const version = resolveVersion(core.getInput("version"));
|
||||
const archive = await getDownloadArchive(version);
|
||||
const archivePath = await tc.downloadTool(archive.url);
|
||||
const extractedPath =
|
||||
archive.format === "zip"
|
||||
? await tc.extractZip(archivePath)
|
||||
: await tc.extractTar(archivePath);
|
||||
const cliPath = getCliPath(extractedPath, archive.format);
|
||||
const resolution = resolvePackage(core.getInput("version"));
|
||||
const cliPath = await installCli(resolution);
|
||||
const installedVersion = await determineInstalledVersion(cliPath);
|
||||
core.setOutput("version", installedVersion);
|
||||
core.addPath(cliPath);
|
||||
|
||||
if (version.toLowerCase() === "latest" || semver.order(version, REGISTRY_VERSION) >= 0) {
|
||||
if (shouldUseGhcrRegistry(resolution.version, installedVersion)) {
|
||||
core.exportVariable(CLI_CONFIG_REGISTRY, "ghcr.io");
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user