mirror of
https://github.com/supabase/setup-cli.git
synced 2026-06-28 01:46:58 +00:00
Compare commits
1 Commits
julien/cli
...
feat/add-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dacd93e8f |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -46,12 +46,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
version: [1.178.2, latest, beta]
|
||||
version: [1.0.0, latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
github-token: ${{ github.token }}
|
||||
- run: supabase -h
|
||||
|
||||
ci:
|
||||
|
||||
20
.licenses/npm/@actions/tool-cache.dep.yml
Normal file
20
.licenses/npm/@actions/tool-cache.dep.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "@actions/tool-cache"
|
||||
version: 4.0.0
|
||||
type: npm
|
||||
summary: Actions tool-cache lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |-
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2019 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
26
.licenses/npm/semver.dep.yml
Normal file
26
.licenses/npm/semver.dep.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: semver
|
||||
version: 7.7.4
|
||||
type: npm
|
||||
summary: The semantic version parser used by npm.
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
||||
42
README.md
42
README.md
@@ -24,43 +24,41 @@ Setup the `supabase` CLI:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: supabase/setup-cli@v3
|
||||
- uses: supabase/setup-cli@v2
|
||||
```
|
||||
|
||||
If `version` is omitted, the action checks the repository root for `bun.lock`,
|
||||
`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`.
|
||||
`pnpm-lock.yaml`, or `package-lock.json` and uses the declared `supabase`
|
||||
version. If no supported lockfile is present, it falls back to `latest`.
|
||||
|
||||
The action provisions Node.js and npm internally, so runners do not need npm
|
||||
preinstalled. Runners must be able to reach the npm registry to install the CLI
|
||||
package.
|
||||
|
||||
A fixed npm-published version, `latest`, or `beta` of the `supabase` CLI can be
|
||||
installed:
|
||||
A specific version of the `supabase` CLI can be installed:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: supabase/setup-cli@v3
|
||||
- uses: supabase/setup-cli@v2
|
||||
with:
|
||||
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@v3
|
||||
- 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
|
||||
steps:
|
||||
- uses: supabase/setup-cli@v3
|
||||
- uses: supabase/setup-cli@v2
|
||||
with:
|
||||
version: latest
|
||||
github-token: ${{ github.token }}
|
||||
- run: supabase init
|
||||
- run: supabase db start
|
||||
```
|
||||
@@ -72,9 +70,10 @@ on Windows and macOS runners.
|
||||
|
||||
The action supports the following inputs:
|
||||
|
||||
| Name | Type | Description | Default | Required |
|
||||
| --------- | ------ | ---------------------------------------------------------------- | --------------------------------- | -------- |
|
||||
| `version` | String | Supabase CLI `latest`, `beta`, or fixed version published to npm | Root lockfile version or `latest` | 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
|
||||
|
||||
@@ -82,7 +81,7 @@ Check generated TypeScript types are up-to-date with Postgres schema:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: supabase/setup-cli@v3
|
||||
- uses: supabase/setup-cli@v2
|
||||
- run: supabase init
|
||||
- run: supabase db start
|
||||
- name: Verify generated types match Postgres schema
|
||||
@@ -105,7 +104,7 @@ env:
|
||||
PROJECT_ID: <project-id>
|
||||
|
||||
steps:
|
||||
- uses: supabase/setup-cli@v3
|
||||
- uses: supabase/setup-cli@v2
|
||||
- run: supabase link --project-ref $PROJECT_ID
|
||||
- run: supabase db push
|
||||
```
|
||||
@@ -114,7 +113,7 @@ Export local Supabase env vars for app tests:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- uses: supabase/setup-cli@v3
|
||||
- uses: supabase/setup-cli@v2
|
||||
- run: supabase init
|
||||
- run: supabase start
|
||||
- name: Export local Supabase env vars
|
||||
@@ -159,7 +158,7 @@ need to perform a few setup steps before you can work on the action.
|
||||
## Publish
|
||||
|
||||
1. Create a new GitHub release
|
||||
2. Rebase `v3` branch on `main`
|
||||
2. Rebase `v2` branch on `main`
|
||||
|
||||
Your action is now published! :rocket:
|
||||
|
||||
@@ -176,6 +175,7 @@ steps:
|
||||
- uses: ./
|
||||
with:
|
||||
version: latest
|
||||
github-token: ${{ github.token }}
|
||||
```
|
||||
|
||||
The CI workflow provides fast smoke coverage across GitHub-hosted runners, and
|
||||
|
||||
11
action.yml
11
action.yml
@@ -3,7 +3,10 @@ description: Setup Supabase CLI, supabase, on GitHub Actions runners
|
||||
author: Supabase
|
||||
inputs:
|
||||
version:
|
||||
description: Supabase CLI version to install. Supports latest, beta, or a fixed version published to npm. 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.
|
||||
required: false
|
||||
outputs:
|
||||
version:
|
||||
@@ -92,11 +95,6 @@ runs:
|
||||
echo "::error::Linux musl containers need libstdc++ and libgcc to run Supabase CLI. Install them before supabase/setup-cli."
|
||||
exit 1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
@@ -114,4 +112,5 @@ 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,6 +6,7 @@
|
||||
"name": "setup-cli",
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/tool-cache": "^4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "^1.0.10",
|
||||
@@ -26,6 +27,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -144,6 +147,8 @@
|
||||
|
||||
"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=="],
|
||||
@@ -151,5 +156,7 @@
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@ The action supports `ubuntu-latest`, `windows-latest`, and `macos-latest`, and
|
||||
adds the requested `supabase` version to `PATH` for the rest of the job.
|
||||
|
||||
If `version` is omitted, the action checks the repository root for `bun.lock`,
|
||||
`pnpm-lock.yaml`, or `package-lock.json` and otherwise falls back to npm
|
||||
`latest`.
|
||||
|
||||
The action provisions Node.js and npm internally; runners only need network
|
||||
access to the npm registry.
|
||||
`pnpm-lock.yaml`, or `package-lock.json` and otherwise falls back to `latest`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -28,27 +24,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: supabase/setup-cli@v3
|
||||
- uses: supabase/setup-cli@v2
|
||||
- run: supabase init
|
||||
- run: supabase db start
|
||||
```
|
||||
|
||||
To pin a fixed npm-published CLI version:
|
||||
To pin a specific CLI version:
|
||||
|
||||
```yaml
|
||||
- uses: supabase/setup-cli@v3
|
||||
- uses: supabase/setup-cli@v2
|
||||
with:
|
||||
version: 2.84.2
|
||||
```
|
||||
|
||||
To test the current beta release:
|
||||
|
||||
```yaml
|
||||
- uses: supabase/setup-cli@v3
|
||||
with:
|
||||
version: beta
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **Source Code**: <https://github.com/supabase/setup-cli>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "setup-cli",
|
||||
"version": "3.0.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "Supabase CLI GitHub Action",
|
||||
"keywords": [
|
||||
@@ -24,7 +24,8 @@
|
||||
"typecheck": "bun x tsgo -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.1"
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/tool-cache": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "^1.0.10",
|
||||
|
||||
745
src/main.test.ts
745
src/main.test.ts
@@ -1,29 +1,31 @@
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, 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 originalPath = process.env.PATH;
|
||||
const originalRunnerTemp = process.env.RUNNER_TEMP;
|
||||
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];
|
||||
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;
|
||||
delete process.env.FAKE_CLI_VERSION;
|
||||
delete process.env.FAKE_NPM_BIN;
|
||||
delete process.env.FAKE_NPM_INTEGRITY;
|
||||
delete process.env.FAKE_NPM_LOG;
|
||||
delete process.env.FAKE_NPM_PACKAGE_VERSION;
|
||||
delete process.env.FAKE_NPM_POSTINSTALL;
|
||||
delete process.env.SUPABASE_SETUP_CLI_NPM;
|
||||
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 });
|
||||
@@ -31,14 +33,32 @@ afterEach(() => {
|
||||
tempDirs.clear();
|
||||
});
|
||||
|
||||
function createTempDir(prefix: string): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
function createFakeCli(versionOutput: string): string {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "setup-cli-"));
|
||||
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 = createTempDir("setup-cli-workspace-");
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "setup-cli-workspace-"));
|
||||
tempDirs.add(dir);
|
||||
|
||||
for (const [relativePath, content] of Object.entries(files)) {
|
||||
const filePath = path.join(dir, relativePath);
|
||||
@@ -54,7 +74,6 @@ function createBunLock(
|
||||
options: {
|
||||
includeDependency?: boolean;
|
||||
includePackageEntry?: boolean;
|
||||
integrity?: string;
|
||||
useDevDependency?: boolean;
|
||||
} = {},
|
||||
): string {
|
||||
@@ -80,7 +99,7 @@ ${
|
||||
"supabase@${version}",
|
||||
"",
|
||||
{},
|
||||
"${options.integrity ?? "sha512-bun"}"
|
||||
"sha512-test"
|
||||
]`
|
||||
: ""
|
||||
}
|
||||
@@ -91,12 +110,7 @@ ${
|
||||
|
||||
function createPnpmLock(
|
||||
version: string,
|
||||
options: {
|
||||
asString?: boolean;
|
||||
includeVersion?: boolean;
|
||||
integrity?: string;
|
||||
useDevDependency?: boolean;
|
||||
} = {},
|
||||
options: { asString?: boolean; includeVersion?: boolean; useDevDependency?: boolean } = {},
|
||||
): string {
|
||||
const dependencyKey = options.useDevDependency ? "devDependencies" : "dependencies";
|
||||
|
||||
@@ -114,11 +128,11 @@ ${options.includeVersion === false ? "" : ` version: ${version}`}`
|
||||
packages:
|
||||
supabase@${version}:
|
||||
resolution:
|
||||
integrity: ${options.integrity ?? "sha512-pnpm"}
|
||||
integrity: sha512-test
|
||||
`;
|
||||
}
|
||||
|
||||
function createPackageLock(version: string, integrity = "sha512-package-lock"): string {
|
||||
function createPackageLock(version: string): string {
|
||||
return JSON.stringify(
|
||||
{
|
||||
name: "app",
|
||||
@@ -130,7 +144,6 @@ function createPackageLock(version: string, integrity = "sha512-package-lock"):
|
||||
},
|
||||
},
|
||||
"node_modules/supabase": {
|
||||
integrity,
|
||||
version,
|
||||
},
|
||||
},
|
||||
@@ -140,142 +153,46 @@ function createPackageLock(version: string, integrity = "sha512-package-lock"):
|
||||
);
|
||||
}
|
||||
|
||||
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") {
|
||||
const bin =
|
||||
process.env.FAKE_NPM_BIN === "missing"
|
||||
? undefined
|
||||
: { supabase: process.env.FAKE_NPM_BIN ?? "dist/supabase.js" };
|
||||
const scripts = process.env.FAKE_NPM_POSTINSTALL
|
||||
? { postinstall: process.env.FAKE_NPM_POSTINSTALL }
|
||||
: {};
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
version: process.env.FAKE_NPM_PACKAGE_VERSION ?? "2.101.0",
|
||||
bin,
|
||||
scripts,
|
||||
"dist.integrity": process.env.FAKE_NPM_INTEGRITY ?? "sha512-test",
|
||||
}),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
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",
|
||||
options: {
|
||||
bin?: string;
|
||||
integrity?: string;
|
||||
packageVersion?: string;
|
||||
postinstall?: string;
|
||||
} = {},
|
||||
): 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_BIN = options.bin ?? "dist/supabase.js";
|
||||
process.env.FAKE_NPM_INTEGRITY = options.integrity ?? "sha512-test";
|
||||
process.env.FAKE_NPM_LOG = logPath;
|
||||
process.env.FAKE_NPM_PACKAGE_VERSION =
|
||||
options.packageVersion ??
|
||||
versionOutput.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/)?.[0] ??
|
||||
"2.101.0";
|
||||
if (options.postinstall) {
|
||||
process.env.FAKE_NPM_POSTINSTALL = options.postinstall;
|
||||
}
|
||||
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 viewMetadataCall(spec: string): string[] {
|
||||
return ["view", spec, "version", "bin", "scripts", "dist.integrity", "--json"];
|
||||
}
|
||||
|
||||
function createActionSpies(inputVersion: 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(() => {}),
|
||||
getInput: spyOn(core, "getInput").mockReturnValue(inputVersion),
|
||||
setFailed: spyOn(core, "setFailed").mockImplementation(() => {}),
|
||||
setOutput: spyOn(core, "setOutput").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 mockLatestRelease(version = "v2.99.0") {
|
||||
return spyOn(globalThis, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ tag_name: version }), {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -284,69 +201,253 @@ async function getMainModule(): Promise<typeof import("./main.ts")> {
|
||||
return mainModule;
|
||||
}
|
||||
|
||||
test("uses an explicit npm package version when provided", async () => {
|
||||
const { resolvePackage } = await getMainModule();
|
||||
test("uses versioned tar archives for Supabase CLI v2.99.0 and later", async () => {
|
||||
const { getDownloadArchive } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("v2.101.0")).toEqual({
|
||||
spec: "supabase@2.101.0",
|
||||
version: "2.101.0",
|
||||
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 an explicit npm dist-tag when provided", async () => {
|
||||
const { resolvePackage } = await getMainModule();
|
||||
test("uses apk archives for Supabase CLI v2.99.0 and later on Linux musl", async () => {
|
||||
const { getDownloadArchive } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("beta")).toEqual({
|
||||
spec: "supabase@beta",
|
||||
version: "beta",
|
||||
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("rejects unsupported npm package selectors", async () => {
|
||||
const { resolvePackage } = await getMainModule();
|
||||
test("keeps tar archives before Supabase CLI v2.99.0 on Linux musl", async () => {
|
||||
const { getDownloadArchive } = await getMainModule();
|
||||
|
||||
expect(() => resolvePackage("hotfix")).toThrow(
|
||||
'Unsupported Supabase CLI version "hotfix". Use latest, beta, or a fixed npm package version like 2.101.0.',
|
||||
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("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("uses the root bun.lock resolution when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"bun.lock": createBunLock("2.41.0", { integrity: "sha512-bun-lock" }),
|
||||
});
|
||||
const { resolvePackage } = await getMainModule();
|
||||
test("queries the releases list when resolving the beta channel", async () => {
|
||||
const fetch = mockBetaReleases();
|
||||
const { getDownloadArchive } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-bun-lock",
|
||||
spec: "supabase@2.41.0",
|
||||
version: "2.41.0",
|
||||
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("uses the root pnpm-lock.yaml resolution when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"pnpm-lock.yaml": createPnpmLock("2.42.0", { integrity: "sha512-pnpm-lock" }),
|
||||
});
|
||||
const { resolvePackage } = await getMainModule();
|
||||
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();
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-pnpm-lock",
|
||||
spec: "supabase@2.42.0",
|
||||
version: "2.42.0",
|
||||
});
|
||||
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("uses the root package-lock.json resolution when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"package-lock.json": createPackageLock("2.43.0", "sha512-package-lock"),
|
||||
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 { resolvePackage } = await getMainModule();
|
||||
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;
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-package-lock",
|
||||
spec: "supabase@2.43.0",
|
||||
version: "2.43.0",
|
||||
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 () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"bun.lock": createBunLock("2.41.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.41.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.41.0/supabase_");
|
||||
const { run } = 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();
|
||||
});
|
||||
|
||||
test("uses the root pnpm-lock.yaml version when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"pnpm-lock.yaml": createPnpmLock("2.42.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.42.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.42.0/supabase_");
|
||||
const { run } = 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();
|
||||
});
|
||||
|
||||
test("uses the root package-lock.json version when version is omitted", async () => {
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"package-lock.json": createPackageLock("2.43.0"),
|
||||
});
|
||||
const cliDir = createFakeCli("supabase 2.43.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.43.0/supabase_");
|
||||
const { run } = 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();
|
||||
});
|
||||
|
||||
test("falls through malformed lockfiles and uses the next supported root lockfile", async () => {
|
||||
@@ -354,47 +455,60 @@ test("falls through malformed lockfiles and uses the next supported root lockfil
|
||||
"bun.lock": "{ not valid",
|
||||
"package-lock.json": createPackageLock("2.44.0"),
|
||||
});
|
||||
const { resolvePackage } = await getMainModule();
|
||||
const cliDir = createFakeCli("supabase 2.44.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.44.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-package-lock",
|
||||
spec: "supabase@2.44.0",
|
||||
version: "2.44.0",
|
||||
});
|
||||
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();
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
const { resolvePackage } = await getMainModule();
|
||||
mockLatestRelease();
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
spec: "supabase@latest",
|
||||
version: "latest",
|
||||
});
|
||||
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 version is omitted and no workspace is available", async () => {
|
||||
delete process.env.GITHUB_WORKSPACE;
|
||||
const { resolvePackage } = await getMainModule();
|
||||
mockLatestRelease();
|
||||
const cliDir = createFakeCli("supabase 2.84.2");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.99.0/supabase_2.99.0_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
spec: "supabase@latest",
|
||||
version: "latest",
|
||||
});
|
||||
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("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 { resolvePackage } = await getMainModule();
|
||||
const cliDir = createFakeCli("supabase 2.44.1");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.44.1/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
spec: "supabase@2.44.1",
|
||||
version: "2.44.1",
|
||||
});
|
||||
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();
|
||||
});
|
||||
|
||||
test("falls through bun.lock without supabase and uses a pnpm string dependency version", async () => {
|
||||
@@ -402,183 +516,88 @@ 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 { resolvePackage } = await getMainModule();
|
||||
const cliDir = createFakeCli("supabase 2.47.0");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.47.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
expect(resolvePackage("")).toEqual({
|
||||
integrity: "sha512-pnpm",
|
||||
spec: "supabase@2.47.0",
|
||||
version: "2.47.0",
|
||||
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"),
|
||||
});
|
||||
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 }),
|
||||
});
|
||||
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([
|
||||
viewMetadataCall("supabase@2.101.0"),
|
||||
[
|
||||
"install",
|
||||
"--prefix",
|
||||
expect.any(String),
|
||||
"--omit=dev",
|
||||
"--include=optional",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--no-package-lock",
|
||||
"--ignore-scripts=true",
|
||||
"supabase@2.101.0",
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test("allows install scripts for legacy npm packages that declare a postinstall", async () => {
|
||||
const logPath = installFakeNpm("supabase 1.178.2", {
|
||||
bin: "bin/supabase",
|
||||
postinstall: "node scripts/postinstall.js",
|
||||
});
|
||||
const { installCli } = await getMainModule();
|
||||
|
||||
await installCli({
|
||||
spec: "supabase@1.178.2",
|
||||
version: "1.178.2",
|
||||
});
|
||||
|
||||
expect(readNpmCalls(logPath)).toEqual([
|
||||
viewMetadataCall("supabase@1.178.2"),
|
||||
[
|
||||
"install",
|
||||
"--prefix",
|
||||
expect.any(String),
|
||||
"--omit=dev",
|
||||
"--include=optional",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--no-package-lock",
|
||||
"--ignore-scripts=false",
|
||||
"supabase@1.178.2",
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test("verifies lockfile integrity before installing", async () => {
|
||||
const logPath = installFakeNpm("supabase 2.101.0", { integrity: "sha512-lock" });
|
||||
const { installCli } = await getMainModule();
|
||||
|
||||
await installCli({
|
||||
integrity: "sha512-lock",
|
||||
spec: "supabase@2.101.0",
|
||||
version: "2.101.0",
|
||||
});
|
||||
|
||||
expect(readNpmCalls(logPath)).toEqual([
|
||||
viewMetadataCall("supabase@2.101.0"),
|
||||
[
|
||||
"install",
|
||||
"--prefix",
|
||||
expect.any(String),
|
||||
"--omit=dev",
|
||||
"--include=optional",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--no-package-lock",
|
||||
"--ignore-scripts=true",
|
||||
"supabase@2.101.0",
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test("fails when lockfile integrity does not match the registry", async () => {
|
||||
installFakeNpm("supabase 2.101.0", { integrity: "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("fails when the npm package does not expose a Supabase CLI executable", async () => {
|
||||
installFakeNpm("supabase 2.101.0", { bin: "missing" });
|
||||
const { installCli } = await getMainModule();
|
||||
|
||||
try {
|
||||
await installCli({
|
||||
spec: "supabase@2.101.0",
|
||||
version: "2.101.0",
|
||||
});
|
||||
throw new Error("Expected installCli to reject");
|
||||
} catch (error) {
|
||||
expect(error).toEqual(
|
||||
new Error("The npm package supabase@2.101.0 does not expose a supabase executable"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("runs the action with a package-lock resolution", async () => {
|
||||
const logPath = installFakeNpm("supabase 2.43.0", { integrity: "sha512-package-lock" });
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"package-lock.json": createPackageLock("2.43.0", "sha512-package-lock"),
|
||||
});
|
||||
const spies = createActionSpies("");
|
||||
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(readNpmCalls(logPath)[0]).toEqual(viewMetadataCall("supabase@2.43.0"));
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.43.0");
|
||||
expect(spies.addPath).toHaveBeenCalledWith(expect.stringContaining("node_modules"));
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 2.84.2");
|
||||
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.1.6");
|
||||
process.env.GITHUB_WORKSPACE = createWorkspace({
|
||||
"bun.lock": createBunLock("2.45.0"),
|
||||
});
|
||||
const spies = createActionSpies("1.1.6");
|
||||
const cliDir = createFakeCli("supabase 1.0.0");
|
||||
const spies = createActionSpies("1.0.0", cliDir, "/download/v1.0.0/supabase_1.0.0_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 1.1.6");
|
||||
expect(spies.setOutput).toHaveBeenCalledWith("version", "supabase 1.0.0");
|
||||
expect(spies.exportVariable).not.toHaveBeenCalled();
|
||||
expect(spies.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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", "sha512-test"),
|
||||
"package-lock.json": createPackageLock("2.46.0"),
|
||||
});
|
||||
const spies = createActionSpies("");
|
||||
const cliDir = createFakeCli("");
|
||||
const spies = createActionSpies("", cliDir, "/download/v2.46.0/supabase_");
|
||||
const { run } = await getMainModule();
|
||||
|
||||
await run();
|
||||
|
||||
424
src/main.ts
424
src/main.ts
@@ -1,30 +1,25 @@
|
||||
import { semver } from "bun";
|
||||
import { $, semver } from "bun";
|
||||
import * as core from "@actions/core";
|
||||
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
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 NPM_PACKAGE = "supabase";
|
||||
const NPM_EXECUTABLE_ENV = "SUPABASE_SETUP_CLI_NPM";
|
||||
const SUPPORTED_DIST_TAGS = new Set([DEFAULT_VERSION, "beta"]);
|
||||
const CONCRETE_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
const CONCRETE_VERSION_EXTRACT_PATTERN = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/;
|
||||
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 PackageResolution = {
|
||||
spec: string;
|
||||
version: string;
|
||||
integrity?: string;
|
||||
};
|
||||
type ArchiveFormat = "apk" | "tar" | "zip";
|
||||
|
||||
type PackageMetadata = {
|
||||
version?: unknown;
|
||||
bin?: unknown;
|
||||
scripts?: unknown;
|
||||
"dist.integrity"?: unknown;
|
||||
type DownloadArchive = {
|
||||
url: string;
|
||||
format: ArchiveFormat;
|
||||
};
|
||||
|
||||
type BunLock = {
|
||||
@@ -43,12 +38,6 @@ type PnpmDependency =
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type PnpmPackage = {
|
||||
resolution?: {
|
||||
integrity?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type PnpmLock = {
|
||||
importers?: {
|
||||
".": {
|
||||
@@ -56,16 +45,19 @@ type PnpmLock = {
|
||||
devDependencies?: Record<string, PnpmDependency>;
|
||||
};
|
||||
};
|
||||
packages?: Record<string, PnpmPackage>;
|
||||
};
|
||||
|
||||
type PackageLock = {
|
||||
packages?: Record<string, { integrity?: string; version?: string }>;
|
||||
dependencies?: Record<string, { integrity?: string; version?: string }>;
|
||||
packages?: Record<string, { version?: string }>;
|
||||
dependencies?: Record<string, { version?: string }>;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
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 {
|
||||
@@ -73,7 +65,7 @@ function extractConcreteVersion(raw: string | undefined): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = raw.match(CONCRETE_VERSION_EXTRACT_PATTERN);
|
||||
const match = raw.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/);
|
||||
return match?.[0] ?? null;
|
||||
}
|
||||
|
||||
@@ -81,33 +73,6 @@ function normalizeVersion(version: string): string {
|
||||
return version.replace(/^v/i, "");
|
||||
}
|
||||
|
||||
function normalizeSupportedVersion(version: string): string {
|
||||
const normalizedVersion = normalizeVersion(version.trim());
|
||||
const distTag = normalizedVersion.toLowerCase();
|
||||
|
||||
if (SUPPORTED_DIST_TAGS.has(distTag)) {
|
||||
return distTag;
|
||||
}
|
||||
|
||||
if (CONCRETE_VERSION_PATTERN.test(normalizedVersion)) {
|
||||
return normalizedVersion;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported Supabase CLI version "${version}". Use latest, beta, or a fixed npm package version like 2.101.0.`,
|
||||
);
|
||||
}
|
||||
|
||||
function toPackageResolution(version: string, integrity?: string): PackageResolution {
|
||||
const normalizedVersion = normalizeSupportedVersion(version);
|
||||
|
||||
return {
|
||||
spec: `${NPM_PACKAGE}@${normalizedVersion}`,
|
||||
version: normalizedVersion,
|
||||
integrity,
|
||||
};
|
||||
}
|
||||
|
||||
function readWorkspaceLockfile(workspaceRoot: string, filename: string): string | null {
|
||||
const filePath = path.join(workspaceRoot, filename);
|
||||
|
||||
@@ -122,7 +87,7 @@ function readWorkspaceLockfile(workspaceRoot: string, filename: string): string
|
||||
}
|
||||
}
|
||||
|
||||
function detectResolutionFromBunLock(workspaceRoot: string): PackageResolution | null {
|
||||
function detectVersionFromBunLock(workspaceRoot: string): string | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "bun.lock");
|
||||
|
||||
if (!text) {
|
||||
@@ -133,28 +98,24 @@ function detectResolutionFromBunLock(workspaceRoot: string): PackageResolution |
|
||||
const lockfile = JSON.parse(text.replace(/,\s*([}\]])/g, "$1")) as BunLock;
|
||||
const rootWorkspace = lockfile.workspaces?.[""];
|
||||
const declaredVersion =
|
||||
rootWorkspace?.dependencies?.[NPM_PACKAGE] ?? rootWorkspace?.devDependencies?.[NPM_PACKAGE];
|
||||
rootWorkspace?.dependencies?.supabase ?? rootWorkspace?.devDependencies?.supabase;
|
||||
|
||||
if (!declaredVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedPackage = lockfile.packages?.[NPM_PACKAGE];
|
||||
const resolvedPackage = lockfile.packages?.supabase;
|
||||
if (Array.isArray(resolvedPackage) && typeof resolvedPackage[0] === "string") {
|
||||
const version = extractConcreteVersion(resolvedPackage[0]);
|
||||
const integrity = typeof resolvedPackage[3] === "string" ? resolvedPackage[3] : undefined;
|
||||
|
||||
return version ? toPackageResolution(version, integrity) : null;
|
||||
return extractConcreteVersion(resolvedPackage[0]);
|
||||
}
|
||||
|
||||
const version = extractConcreteVersion(declaredVersion);
|
||||
return version ? toPackageResolution(version) : null;
|
||||
return extractConcreteVersion(declaredVersion);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function detectResolutionFromPnpmLock(workspaceRoot: string): PackageResolution | null {
|
||||
function detectVersionFromPnpmLock(workspaceRoot: string): string | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "pnpm-lock.yaml");
|
||||
|
||||
if (!text) {
|
||||
@@ -165,29 +126,19 @@ function detectResolutionFromPnpmLock(workspaceRoot: string): PackageResolution
|
||||
const lockfile = Bun.YAML.parse(text) as PnpmLock;
|
||||
const rootImporter = lockfile.importers?.["."];
|
||||
const dependency =
|
||||
rootImporter?.dependencies?.[NPM_PACKAGE] ?? rootImporter?.devDependencies?.[NPM_PACKAGE];
|
||||
const version =
|
||||
typeof dependency === "string"
|
||||
? extractConcreteVersion(dependency)
|
||||
: extractConcreteVersion(dependency?.version);
|
||||
rootImporter?.dependencies?.supabase ?? rootImporter?.devDependencies?.supabase;
|
||||
|
||||
if (!version) {
|
||||
return null;
|
||||
if (typeof dependency === "string") {
|
||||
return extractConcreteVersion(dependency);
|
||||
}
|
||||
|
||||
const integrity = Object.entries(lockfile.packages ?? {}).find(
|
||||
([packageKey]) =>
|
||||
packageKey === `${NPM_PACKAGE}@${version}` ||
|
||||
packageKey.startsWith(`/${NPM_PACKAGE}@${version}`),
|
||||
)?.[1].resolution?.integrity;
|
||||
|
||||
return toPackageResolution(version, integrity);
|
||||
return extractConcreteVersion(dependency?.version);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function detectResolutionFromPackageLock(workspaceRoot: string): PackageResolution | null {
|
||||
function detectVersionFromPackageLock(workspaceRoot: string): string | null {
|
||||
const text = readWorkspaceLockfile(workspaceRoot, "package-lock.json");
|
||||
|
||||
if (!text) {
|
||||
@@ -196,135 +147,183 @@ function detectResolutionFromPackageLock(workspaceRoot: string): PackageResoluti
|
||||
|
||||
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 version
|
||||
? toPackageResolution(version, packageEntry?.integrity ?? dependencyEntry?.integrity)
|
||||
: null;
|
||||
return (
|
||||
extractConcreteVersion(lockfile.packages?.["node_modules/supabase"]?.version) ??
|
||||
extractConcreteVersion(lockfile.dependencies?.supabase?.version)
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePackage(inputVersion: string): PackageResolution {
|
||||
function resolveVersion(inputVersion: string): string {
|
||||
const requestedVersion = inputVersion.trim();
|
||||
|
||||
if (requestedVersion) {
|
||||
return toPackageResolution(requestedVersion);
|
||||
return requestedVersion;
|
||||
}
|
||||
|
||||
const workspaceRoot = process.env.GITHUB_WORKSPACE?.trim();
|
||||
|
||||
if (!workspaceRoot) {
|
||||
return toPackageResolution(DEFAULT_VERSION);
|
||||
return DEFAULT_VERSION;
|
||||
}
|
||||
|
||||
return (
|
||||
detectResolutionFromBunLock(workspaceRoot) ??
|
||||
detectResolutionFromPnpmLock(workspaceRoot) ??
|
||||
detectResolutionFromPackageLock(workspaceRoot) ??
|
||||
toPackageResolution(DEFAULT_VERSION)
|
||||
detectVersionFromBunLock(workspaceRoot) ??
|
||||
detectVersionFromPnpmLock(workspaceRoot) ??
|
||||
detectVersionFromPackageLock(workspaceRoot) ??
|
||||
DEFAULT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
function verifyPackageIntegrity(resolution: PackageResolution, metadata: PackageMetadata): void {
|
||||
if (!resolution.integrity) {
|
||||
return;
|
||||
function buildGithubHeaders(): Record<string, 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}`;
|
||||
}
|
||||
|
||||
const registryIntegrity = metadata["dist.integrity"];
|
||||
if (registryIntegrity !== resolution.integrity) {
|
||||
throw new Error(`Lockfile integrity for ${resolution.spec} does not match the npm registry`);
|
||||
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}`);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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,
|
||||
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 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,
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
async function getPackageMetadata(resolution: PackageResolution): Promise<PackageMetadata> {
|
||||
const output = await runNpm([
|
||||
"view",
|
||||
resolution.spec,
|
||||
"version",
|
||||
"bin",
|
||||
"scripts",
|
||||
"dist.integrity",
|
||||
"--json",
|
||||
]);
|
||||
const metadata = JSON.parse(output) as unknown;
|
||||
|
||||
if (!isRecord(metadata)) {
|
||||
throw new Error(`Could not read npm metadata for ${resolution.spec}`);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function verifyPackageMetadata(resolution: PackageResolution, metadata: PackageMetadata): void {
|
||||
if (typeof metadata.version !== "string" || !CONCRETE_VERSION_PATTERN.test(metadata.version)) {
|
||||
throw new Error(`Could not resolve a fixed npm version for ${resolution.spec}`);
|
||||
}
|
||||
|
||||
const bin = metadata.bin;
|
||||
const hasSupabaseBin =
|
||||
typeof bin === "string" || (isRecord(bin) && typeof bin[NPM_PACKAGE] === "string");
|
||||
|
||||
if (!hasSupabaseBin) {
|
||||
throw new Error(`The npm package ${resolution.spec} does not expose a supabase executable`);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldIgnoreInstallScripts(metadata: PackageMetadata): boolean {
|
||||
return !(isRecord(metadata.scripts) && typeof metadata.scripts.postinstall === "string");
|
||||
}
|
||||
|
||||
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> {
|
||||
const metadata = await getPackageMetadata(resolution);
|
||||
verifyPackageMetadata(resolution, metadata);
|
||||
verifyPackageIntegrity(resolution, metadata);
|
||||
|
||||
const installRoot = createInstallRoot();
|
||||
|
||||
await runNpm([
|
||||
"install",
|
||||
"--prefix",
|
||||
installRoot,
|
||||
"--omit=dev",
|
||||
"--include=optional",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--no-package-lock",
|
||||
`--ignore-scripts=${shouldIgnoreInstallScripts(metadata)}`,
|
||||
resolution.spec,
|
||||
]);
|
||||
|
||||
return path.join(installRoot, "node_modules", ".bin");
|
||||
export function getCliPath(extractedPath: string, archiveFormat: ArchiveFormat): string {
|
||||
return archiveFormat === "apk" ? path.join(extractedPath, "usr", "bin") : extractedPath;
|
||||
}
|
||||
|
||||
function getCliExecutablePath(cliPath: string): string {
|
||||
@@ -332,36 +331,21 @@ function getCliExecutablePath(cliPath: string): string {
|
||||
return path.join(cliPath, "supabase");
|
||||
}
|
||||
|
||||
const cmdPath = path.join(cliPath, "supabase.cmd");
|
||||
if (existsSync(cmdPath)) {
|
||||
return cmdPath;
|
||||
}
|
||||
|
||||
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> {
|
||||
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();
|
||||
const version = (await $`${getCliExecutablePath(cliPath)} --version`.text()).trim();
|
||||
if (!version) {
|
||||
throw new Error("Could not determine installed Supabase CLI version");
|
||||
}
|
||||
@@ -369,24 +353,26 @@ 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 resolution = resolvePackage(core.getInput("version"));
|
||||
const cliPath = await installCli(resolution);
|
||||
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 installedVersion = await determineInstalledVersion(cliPath);
|
||||
core.setOutput("version", installedVersion);
|
||||
core.addPath(cliPath);
|
||||
|
||||
if (shouldUseGhcrRegistry(resolution.version, installedVersion)) {
|
||||
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