supabase-worker-previews 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -34
- package/dist/cli.js +8 -4
- package/dist/commands/branches.d.ts +1 -1
- package/dist/commands/branches.js +2 -2
- package/dist/commands/doctor.js +27 -15
- package/dist/commands/init.js +7 -7
- package/dist/commands/pr.js +4 -4
- package/dist/config.d.ts +10 -4
- package/dist/config.js +25 -13
- package/dist/feedback.d.ts +1 -1
- package/dist/feedback.js +1 -1
- package/dist/run.d.ts +1 -1
- package/dist/run.js +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/supabase.js +1 -1
- package/dist/usage.js +37 -22
- package/package.json +1 -2
- package/templates/supabase-previews-action.yml +1 -1
package/README.md
CHANGED
|
@@ -37,18 +37,18 @@ flowchart LR
|
|
|
37
37
|
preview -- "previews.vars" --> shareddb[("Shared Preview database<br/>persistent branch 'preview'")]
|
|
38
38
|
pr["PR changes supabase/<br/>or has label isolated-db"] --> integ["Supabase GitHub integration"]
|
|
39
39
|
integ --> prdb[("The PR's own database")]
|
|
40
|
-
pr --> action["GitHub Actions:
|
|
40
|
+
pr --> action["GitHub Actions: pr command"]
|
|
41
41
|
action -- "SUPABASE_OVERRIDE secret" --> preview
|
|
42
42
|
preview -. "PRs with their own database" .-> prdb
|
|
43
|
-
action -- "
|
|
43
|
+
action -- "check reads<br/>/.well-known/supabase-preview" --> preview
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
It adds no infrastructure. Cloudflare builds the Previews and Supabase makes and migrates the databases; this package connects the two:
|
|
47
47
|
|
|
48
48
|
1. **Every Preview starts on the shared Preview database**, a persistent Supabase branch that tracks your trunk. Its public URL and key live in your wrangler config's `previews.vars`.
|
|
49
|
-
2. **A PR that changes the schema gets its own database.** The Supabase GitHub integration creates it; `
|
|
49
|
+
2. **A PR that changes the schema gets its own database.** The Supabase GitHub integration creates it; the `pr` command points the PR's Preview at it with one secret, `SUPABASE_OVERRIDE`.
|
|
50
50
|
3. **The Worker reads its Supabase settings per request.** `withSupabasePreviews()` applies the override and injects the public config into each HTML page, so no build-time `VITE_SUPABASE_URL` pins a database into the bundle.
|
|
51
|
-
4. **`
|
|
51
|
+
4. **`check` asks the Preview which database it serves** and fails the PR unless it is the right one.
|
|
52
52
|
|
|
53
53
|
New to Worker Previews or Supabase branching? [Concepts](docs/concepts.md) explains both in five minutes.
|
|
54
54
|
|
|
@@ -61,7 +61,7 @@ npm install --save-dev supabase-worker-previews
|
|
|
61
61
|
npx supabase-worker-previews init --project-ref <production project ref>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
`init` writes `
|
|
64
|
+
`init` writes `supabase-worker-previews.json`, a grants migration and the PR workflow, and prints the rest. Then:
|
|
65
65
|
|
|
66
66
|
1. Connect the Supabase GitHub integration (automatic branching on, deploy to production off).
|
|
67
67
|
2. Add a `previews` block to your wrangler config.
|
|
@@ -83,43 +83,43 @@ The **[full quickstart](docs/quickstart.md)** walks through every step with the
|
|
|
83
83
|
|
|
84
84
|
## Commands
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
| -------------- | ------------ | ------------------------------------------------------------------------------------ |
|
|
88
|
-
| **Set up** | `swp init` | Scaffold `swp.config.json`, the grants migration and the workflow (never overwrites) |
|
|
89
|
-
| | `swp doctor` | Check the whole setup, offline and (with a token) against Supabase |
|
|
90
|
-
| | `swp shared` | Create or repair the shared Preview database; prints its `previews.vars` |
|
|
91
|
-
| **Per branch** | `swp up` | Give a branch's Preview its own database |
|
|
92
|
-
| | `swp check` | Fail unless the Preview serves the right database |
|
|
93
|
-
| | `swp down` | Delete a branch's Preview and its own database |
|
|
94
|
-
| **In CI** | `swp pr` | All of the above for a `pull_request` workflow, plus the PR comment and deployment |
|
|
95
|
-
| | `swp prune` | List leftovers of deleted branches and closed PRs; delete them with `--yes` |
|
|
86
|
+
Run each one as `npx supabase-worker-previews <command>`, or from an npm script.
|
|
96
87
|
|
|
97
|
-
|
|
88
|
+
| Group | Command | Does |
|
|
89
|
+
| -------------- | -------- | -------------------------------------------------------------------------------------------------- |
|
|
90
|
+
| **Set up** | `init` | Scaffold `supabase-worker-previews.json`, the grants migration and the workflow (never overwrites) |
|
|
91
|
+
| | `doctor` | Check the whole setup, offline and (with a token) against Supabase |
|
|
92
|
+
| | `shared` | Create or repair the shared Preview database; prints its `previews.vars` |
|
|
93
|
+
| **Per branch** | `up` | Give a branch's Preview its own database |
|
|
94
|
+
| | `check` | Fail unless the Preview serves the right database |
|
|
95
|
+
| | `down` | Delete a branch's Preview and its own database |
|
|
96
|
+
| **In CI** | `pr` | All of the above for a `pull_request` workflow, plus the PR comment and deployment |
|
|
97
|
+
| | `prune` | List leftovers of deleted branches and closed PRs; delete them with `--yes` |
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
`<command> --help` shows one command's flags and examples, and `--version` prints the version. Flags, environment variables, every `supabase-worker-previews.json` field and the GitHub Action inputs are in the [configuration reference](docs/configuration.md).
|
|
100
100
|
|
|
101
101
|
## Safety
|
|
102
102
|
|
|
103
|
-
-
|
|
104
|
-
- `
|
|
105
|
-
- `
|
|
106
|
-
- `
|
|
103
|
+
- It never writes to, repoints or deletes the production project, and never deletes a persistent branch.
|
|
104
|
+
- `check` fails the first time a Preview serves production, with no retry.
|
|
105
|
+
- `doctor` fails if `previews.vars` names production or holds a secret.
|
|
106
|
+
- `prune` only lists until you pass `--yes`, and every command that changes something takes `--dry-run`.
|
|
107
107
|
|
|
108
|
-
[Security](docs/security.md) covers what
|
|
108
|
+
[Security](docs/security.md) covers what the CLI can touch and what each token can reach.
|
|
109
109
|
|
|
110
110
|
## Docs
|
|
111
111
|
|
|
112
|
-
| Page | For
|
|
113
|
-
| ------------------------------------------ |
|
|
114
|
-
| [Concepts](docs/concepts.md) | Worker Previews, Supabase branches, and how
|
|
115
|
-
| [Quickstart](docs/quickstart.md) | From an existing Worker to the first working PR Preview
|
|
116
|
-
| [Frameworks](docs/README.md#frameworks) | TanStack Start, Hono, React Router v7, Astro
|
|
117
|
-
| [Troubleshooting](docs/troubleshooting.md) | Symptoms, causes and fixes
|
|
118
|
-
| [How it works](docs/how-it-works.md) | Every moving part and why it is built that way
|
|
119
|
-
| [Configuration](docs/configuration.md) | Commands, flags, `
|
|
120
|
-
| [Tokens](docs/tokens.md) | Least-privilege Cloudflare, Supabase and GitHub credentials
|
|
121
|
-
| [Security](docs/security.md) | What
|
|
122
|
-
| [Compared with Vercel](docs/vs-vercel.md) | When Vercel and its Supabase integration are the better fit
|
|
112
|
+
| Page | For |
|
|
113
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
114
|
+
| [Concepts](docs/concepts.md) | Worker Previews, Supabase branches, and how this package joins them |
|
|
115
|
+
| [Quickstart](docs/quickstart.md) | From an existing Worker to the first working PR Preview |
|
|
116
|
+
| [Frameworks](docs/README.md#frameworks) | TanStack Start, Hono, React Router v7, Astro |
|
|
117
|
+
| [Troubleshooting](docs/troubleshooting.md) | Symptoms, causes and fixes |
|
|
118
|
+
| [How it works](docs/how-it-works.md) | Every moving part and why it is built that way |
|
|
119
|
+
| [Configuration](docs/configuration.md) | Commands, flags, `supabase-worker-previews.json`, the GitHub Action, the runtime |
|
|
120
|
+
| [Tokens](docs/tokens.md) | Least-privilege Cloudflare, Supabase and GitHub credentials |
|
|
121
|
+
| [Security](docs/security.md) | What the CLI can touch, what is public, what is secret |
|
|
122
|
+
| [Compared with Vercel](docs/vs-vercel.md) | When Vercel and its Supabase integration are the better fit |
|
|
123
123
|
|
|
124
124
|
The full index is [docs/README.md](docs/README.md).
|
|
125
125
|
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { CloudflareApi } from "./cloudflare.js";
|
|
5
|
-
import { loadConfig, migrationFiles } from "./config.js";
|
|
5
|
+
import { CONFIG_FILE, configFileIn, DOTENV_FILE, LEGACY_CONFIG_FILE, LEGACY_DOTENV_FILE, loadConfig, migrationFiles, } from "./config.js";
|
|
6
6
|
import { check, down, shared, up } from "./commands/branches.js";
|
|
7
7
|
import { doctor, formatReport } from "./commands/doctor.js";
|
|
8
8
|
import { init } from "./commands/init.js";
|
|
@@ -64,12 +64,14 @@ async function main(argv) {
|
|
|
64
64
|
return 0;
|
|
65
65
|
}
|
|
66
66
|
if (invocation.kind === "error") {
|
|
67
|
-
console.error(`${errStyle.red("
|
|
67
|
+
console.error(`${errStyle.red("supabase-worker-previews:")} ${invocation.message}`);
|
|
68
68
|
return 2;
|
|
69
69
|
}
|
|
70
70
|
const { command, flags } = invocation;
|
|
71
71
|
const dryRun = flags["dry-run"] === true;
|
|
72
|
-
const envFile = str(flags.dotenv) ?? (
|
|
72
|
+
const envFile = str(flags.dotenv) ?? [DOTENV_FILE, LEGACY_DOTENV_FILE].find((f) => existsSync(f));
|
|
73
|
+
if (envFile === LEGACY_DOTENV_FILE)
|
|
74
|
+
console.error(errStyle.yellow(`Loading ${LEGACY_DOTENV_FILE}; rename it to ${DOTENV_FILE}.`));
|
|
73
75
|
if (envFile) {
|
|
74
76
|
if (!existsSync(envFile))
|
|
75
77
|
throw new Error(`--dotenv ${envFile}: no such file`);
|
|
@@ -98,6 +100,8 @@ async function main(argv) {
|
|
|
98
100
|
console.log(report.text);
|
|
99
101
|
return report.pass ? 0 : 1;
|
|
100
102
|
}
|
|
103
|
+
if (configFileIn() === LEGACY_CONFIG_FILE)
|
|
104
|
+
console.error(errStyle.yellow(`Reading ${LEGACY_CONFIG_FILE}; rename it to ${CONFIG_FILE}.`));
|
|
101
105
|
const config = loadConfig(overrides);
|
|
102
106
|
const prNum = prNumber(flags.pr);
|
|
103
107
|
const event = command === "pr" ? readEvent() : undefined;
|
|
@@ -147,6 +151,6 @@ async function main(argv) {
|
|
|
147
151
|
return 0;
|
|
148
152
|
}
|
|
149
153
|
main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
|
|
150
|
-
console.error(`${errStyle.red("
|
|
154
|
+
console.error(`${errStyle.red("supabase-worker-previews:")} ${formatError(err)}`);
|
|
151
155
|
process.exit(1);
|
|
152
156
|
});
|
|
@@ -17,7 +17,7 @@ export type Deps = {
|
|
|
17
17
|
export declare function branchFor(gitBranch: string, deps: Deps): Promise<Branch | undefined>;
|
|
18
18
|
/** The Preview a git branch deploys to: named after the branch, or `pr-<number>` when configured. */
|
|
19
19
|
export declare function previewKey(gitBranch: string, config: Config, pr?: number): string;
|
|
20
|
-
/** A branch `
|
|
20
|
+
/** A branch `supabase-worker-previews` may delete: never production, the shared database, the trunk's or a persistent one. */
|
|
21
21
|
export declare function isDisposable(branch: Branch, config: Config): boolean;
|
|
22
22
|
/** Never repoint, write to or delete the production project through a branch record. */
|
|
23
23
|
export declare function assertIsolated(branch: Branch, parent: string): void;
|
|
@@ -19,7 +19,7 @@ export function previewKey(gitBranch, config, pr) {
|
|
|
19
19
|
throw new Error(`previewName is "pr", so pass the PR number (--pr <n>)`);
|
|
20
20
|
return `pr-${pr}`;
|
|
21
21
|
}
|
|
22
|
-
/** A branch `
|
|
22
|
+
/** A branch `supabase-worker-previews` may delete: never production, the shared database, the trunk's or a persistent one. */
|
|
23
23
|
export function isDisposable(branch, config) {
|
|
24
24
|
return (!branch.is_default &&
|
|
25
25
|
!branch.persistent &&
|
|
@@ -59,7 +59,7 @@ export async function shared(deps) {
|
|
|
59
59
|
}
|
|
60
60
|
if (!branches.some((b) => b.is_default))
|
|
61
61
|
throw new Error(`Branching is not enabled on ${parent}. Connect the repo in the Supabase dashboard ` +
|
|
62
|
-
"(project settings, Integrations, GitHub, automatic branching on), then run
|
|
62
|
+
"(project settings, Integrations, GitHub, automatic branching on), then run supabase-worker-previews shared again.");
|
|
63
63
|
let branch = branches.find((b) => b.name === config.sharedBranch);
|
|
64
64
|
if (!branch) {
|
|
65
65
|
branch = await supabase.createBranch(parent, {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { CONFIG_DEFAULTS, defined, loadConfig, migrationFiles, migrationsDir, readWranglerConfig, } from "../config.js";
|
|
3
|
+
import { CONFIG_DEFAULTS, CONFIG_FILE, configFileIn, defined, DOTENV_FILE, LEGACY_CONFIG_FILE, LEGACY_DOTENV_FILE, loadConfig, migrationFiles, migrationsDir, readWranglerConfig, } from "../config.js";
|
|
4
4
|
import { TOKENS_URL } from "../http.js";
|
|
5
5
|
import { projectRefOf } from "../runtime.js";
|
|
6
6
|
import { makeStyle } from "../style.js";
|
|
@@ -37,7 +37,7 @@ export function checkLocal(config, cwd = process.cwd()) {
|
|
|
37
37
|
const ok = (message) => out.push({ level: "ok", message });
|
|
38
38
|
const warn = (message, fix) => out.push({ level: "warn", message, fix });
|
|
39
39
|
const error = (message, fix) => out.push({ level: "error", message, fix });
|
|
40
|
-
const shared = "Run `
|
|
40
|
+
const shared = "Run `supabase-worker-previews shared` and paste the values it prints into previews.vars.";
|
|
41
41
|
const version = installedWranglerVersion(cwd);
|
|
42
42
|
if (!version)
|
|
43
43
|
warn("wrangler is not installed in this project", "npm install --save-dev wrangler@latest (4.135.0 or later)");
|
|
@@ -47,11 +47,11 @@ export function checkLocal(config, cwd = process.cwd()) {
|
|
|
47
47
|
ok(`wrangler ${version}`);
|
|
48
48
|
const wrangler = readWranglerConfig(cwd);
|
|
49
49
|
if (!wrangler)
|
|
50
|
-
error("no wrangler.jsonc, wrangler.json or wrangler.toml here", "Run
|
|
50
|
+
error("no wrangler.jsonc, wrangler.json or wrangler.toml here", "Run supabase-worker-previews doctor in the Worker's directory.");
|
|
51
51
|
else {
|
|
52
52
|
const previews = wrangler.json.previews;
|
|
53
53
|
if (!previews)
|
|
54
|
-
error(`${wrangler.file} has no "previews" block, so Previews get no Supabase settings`, 'Add "previews": { "vars": {} }, redeclaring every binding the Worker uses, then run `
|
|
54
|
+
error(`${wrangler.file} has no "previews" block, so Previews get no Supabase settings`, 'Add "previews": { "vars": {} }, redeclaring every binding the Worker uses, then run `supabase-worker-previews shared` for the vars.');
|
|
55
55
|
else {
|
|
56
56
|
const vars = (previews.vars ?? {});
|
|
57
57
|
const url = typeof vars.SUPABASE_URL === "string" ? vars.SUPABASE_URL : undefined;
|
|
@@ -63,7 +63,7 @@ export function checkLocal(config, cwd = process.cwd()) {
|
|
|
63
63
|
ok(`previews.vars uses ${projectRefOf(url) ?? url}`);
|
|
64
64
|
for (const name of SECRET_NAMES)
|
|
65
65
|
if (name in vars)
|
|
66
|
-
error(`previews.vars holds ${name}; secrets belong in the Preview base config`, `Remove ${name} from previews.vars (
|
|
66
|
+
error(`previews.vars holds ${name}; secrets belong in the Preview base config`, `Remove ${name} from previews.vars (supabase-worker-previews stores it as a secret), and rotate the key if it was committed.`);
|
|
67
67
|
for (const key of BINDING_KEYS) {
|
|
68
68
|
if (!(key in wrangler.json) || key in previews)
|
|
69
69
|
continue;
|
|
@@ -81,13 +81,13 @@ export function checkLocal(config, cwd = process.cwd()) {
|
|
|
81
81
|
}
|
|
82
82
|
const migrations = migrationFiles(config, cwd);
|
|
83
83
|
if (!migrations.length)
|
|
84
|
-
warn(`no migrations in ${migrationsDir(config)}`, 'Commit your migrations there, or set "supabaseDir" in
|
|
84
|
+
warn(`no migrations in ${migrationsDir(config)}`, 'Commit your migrations there, or set "supabaseDir" in supabase-worker-previews.json.');
|
|
85
85
|
else {
|
|
86
86
|
const first = readFileSync(join(cwd, migrationsDir(config), migrations[0]), "utf8");
|
|
87
87
|
if (hasDefaultPrivileges(first))
|
|
88
88
|
ok(`${migrations[0]} grants the API roles default privileges`);
|
|
89
89
|
else
|
|
90
|
-
error(`the first migration (${migrations[0]}) does not grant default privileges; branch databases will 403`, "Run `
|
|
90
|
+
error(`the first migration (${migrations[0]}) does not grant default privileges; branch databases will 403`, "Run `supabase-worker-previews init` to add the grants migration before it.");
|
|
91
91
|
}
|
|
92
92
|
const toml = join(cwd, config.supabaseDir, "config.toml");
|
|
93
93
|
if (existsSync(toml)) {
|
|
@@ -110,7 +110,7 @@ export async function checkRemote(config, supabase, cwd = process.cwd()) {
|
|
|
110
110
|
level: "error",
|
|
111
111
|
message: `cannot read project ${parent}: ${err.message}`,
|
|
112
112
|
...(status === 404 && {
|
|
113
|
-
fix: `Check "supabaseProjectRef" in
|
|
113
|
+
fix: `Check "supabaseProjectRef" in supabase-worker-previews.json, and that SUPABASE_ACCESS_TOKEN can reach that project.`,
|
|
114
114
|
}),
|
|
115
115
|
},
|
|
116
116
|
];
|
|
@@ -142,7 +142,7 @@ export async function checkRemote(config, supabase, cwd = process.cwd()) {
|
|
|
142
142
|
out.push({
|
|
143
143
|
level: "error",
|
|
144
144
|
message: `no "${config.sharedBranch}" branch, the shared Preview database`,
|
|
145
|
-
fix: "Run `
|
|
145
|
+
fix: "Run `supabase-worker-previews shared` to create it.",
|
|
146
146
|
});
|
|
147
147
|
return out;
|
|
148
148
|
}
|
|
@@ -150,19 +150,19 @@ export async function checkRemote(config, supabase, cwd = process.cwd()) {
|
|
|
150
150
|
out.push({
|
|
151
151
|
level: "error",
|
|
152
152
|
message: `"${config.sharedBranch}" is the production project itself`,
|
|
153
|
-
fix: 'Set "sharedBranch" in
|
|
153
|
+
fix: 'Set "sharedBranch" in supabase-worker-previews.json to another name, then run `supabase-worker-previews shared`.',
|
|
154
154
|
});
|
|
155
155
|
if (!shared.persistent)
|
|
156
156
|
out.push({
|
|
157
157
|
level: "warn",
|
|
158
158
|
message: `"${config.sharedBranch}" is not persistent`,
|
|
159
|
-
fix: "Run `
|
|
159
|
+
fix: "Run `supabase-worker-previews shared` to make it persistent.",
|
|
160
160
|
});
|
|
161
161
|
if (shared.git_branch !== config.trunk)
|
|
162
162
|
out.push({
|
|
163
163
|
level: "error",
|
|
164
164
|
message: `"${config.sharedBranch}" tracks ${shared.git_branch ?? "no git branch"}, not ${config.trunk}`,
|
|
165
|
-
fix: `Run \`
|
|
165
|
+
fix: `Run \`supabase-worker-previews shared\` to point it at ${config.trunk}.`,
|
|
166
166
|
});
|
|
167
167
|
const vars = readWranglerConfig(cwd)?.json.previews
|
|
168
168
|
?.vars;
|
|
@@ -171,7 +171,7 @@ export async function checkRemote(config, supabase, cwd = process.cwd()) {
|
|
|
171
171
|
out.push({
|
|
172
172
|
level: "error",
|
|
173
173
|
message: `previews.vars uses ${varsRef}, but "${config.sharedBranch}" is ${shared.project_ref}`,
|
|
174
|
-
fix: "Run `
|
|
174
|
+
fix: "Run `supabase-worker-previews shared` and paste the values it prints into previews.vars.",
|
|
175
175
|
});
|
|
176
176
|
else if (varsRef)
|
|
177
177
|
out.push({
|
|
@@ -230,6 +230,18 @@ export async function doctor(overrides, supabase, cwd = process.cwd()) {
|
|
|
230
230
|
const local = { title: "Local files", findings: [] };
|
|
231
231
|
const online = { title: "Supabase (online)", findings: [] };
|
|
232
232
|
let config;
|
|
233
|
+
if (configFileIn(cwd) === LEGACY_CONFIG_FILE)
|
|
234
|
+
local.findings.push({
|
|
235
|
+
level: "warn",
|
|
236
|
+
message: `${LEGACY_CONFIG_FILE} is the name from before 0.2.0`,
|
|
237
|
+
fix: `Rename it to ${CONFIG_FILE}; the old name is still read for now.`,
|
|
238
|
+
});
|
|
239
|
+
if (existsSync(join(cwd, LEGACY_DOTENV_FILE)) && !existsSync(join(cwd, DOTENV_FILE)))
|
|
240
|
+
local.findings.push({
|
|
241
|
+
level: "warn",
|
|
242
|
+
message: `${LEGACY_DOTENV_FILE} is the name from before 0.2.0`,
|
|
243
|
+
fix: `Rename it to ${DOTENV_FILE}, and update .gitignore; the old name is still loaded for now.`,
|
|
244
|
+
});
|
|
233
245
|
try {
|
|
234
246
|
config = loadConfig(overrides, cwd);
|
|
235
247
|
}
|
|
@@ -246,7 +258,7 @@ export async function doctor(overrides, supabase, cwd = process.cwd()) {
|
|
|
246
258
|
online.skipped = "skipped until the config above is fixed";
|
|
247
259
|
else if (!supabase)
|
|
248
260
|
online.skipped =
|
|
249
|
-
"skipped: SUPABASE_ACCESS_TOKEN is not set. Add it to .env.
|
|
261
|
+
"skipped: SUPABASE_ACCESS_TOKEN is not set. Add it to .env.supabase-worker-previews to also check branching, " +
|
|
250
262
|
`the shared database and the GitHub integration (${TOKENS_URL}).`;
|
|
251
263
|
else {
|
|
252
264
|
online.title = `Supabase (online, project ${config.supabaseProjectRef})`;
|
|
@@ -281,7 +293,7 @@ export function formatReport(sections, s = makeStyle(false)) {
|
|
|
281
293
|
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
282
294
|
const skipped = sections.some((section) => section.skipped) ? " (online checks skipped)" : "";
|
|
283
295
|
lines.push("", errors
|
|
284
|
-
? s.red(`${plural(errors, "error")}, ${plural(warnings, "warning")}${skipped}. Fix the errors and run
|
|
296
|
+
? s.red(`${plural(errors, "error")}, ${plural(warnings, "warning")}${skipped}. Fix the errors and run supabase-worker-previews doctor again.`)
|
|
285
297
|
: warnings
|
|
286
298
|
? s.yellow(`No errors, ${plural(warnings, "warning")}${skipped}.`)
|
|
287
299
|
: s.green(`All checks passed${skipped}.`));
|
package/dist/commands/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { CONFIG_FILE, migrationFiles, migrationsDir, PLACEHOLDER_REF, readWranglerConfig, WRANGLER_FILES, } from "../config.js";
|
|
5
|
+
import { CONFIG_FILE, configFileIn, migrationFiles, migrationsDir, PLACEHOLDER_REF, readWranglerConfig, WRANGLER_FILES, } from "../config.js";
|
|
6
6
|
import { QUICKSTART_URL, TOKENS_URL } from "../http.js";
|
|
7
7
|
import { hasDefaultPrivileges } from "./doctor.js";
|
|
8
8
|
const TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "templates");
|
|
@@ -49,9 +49,9 @@ export function init(options = {}, cwd = process.cwd()) {
|
|
|
49
49
|
const log = options.log ?? console.log;
|
|
50
50
|
const wrangler = readWranglerConfig(cwd);
|
|
51
51
|
if (!wrangler)
|
|
52
|
-
throw new Error(`No ${WRANGLER_FILES.join(", ")} in this directory. Run
|
|
52
|
+
throw new Error(`No ${WRANGLER_FILES.join(", ")} in this directory. Run supabase-worker-previews init in the Worker's directory.`);
|
|
53
53
|
if (!wrangler.name)
|
|
54
|
-
throw new Error(`${wrangler.file} has no "name". Add the Worker's name, then run
|
|
54
|
+
throw new Error(`${wrangler.file} has no "name". Add the Worker's name, then run supabase-worker-previews init again.`);
|
|
55
55
|
const write = (path, content) => {
|
|
56
56
|
if (options.dryRun)
|
|
57
57
|
return;
|
|
@@ -60,11 +60,11 @@ export function init(options = {}, cwd = process.cwd()) {
|
|
|
60
60
|
};
|
|
61
61
|
if (options.dryRun)
|
|
62
62
|
log("Dry run: nothing is written.\n");
|
|
63
|
-
const configPath = join(cwd,
|
|
63
|
+
const configPath = join(cwd, configFileIn(cwd));
|
|
64
64
|
let needsRef;
|
|
65
65
|
if (existsSync(configPath)) {
|
|
66
66
|
needsRef = !hasProjectRef(readFileSync(configPath, "utf8"));
|
|
67
|
-
log(`- ${
|
|
67
|
+
log(`- ${basename(configPath)} exists, left alone`);
|
|
68
68
|
}
|
|
69
69
|
else {
|
|
70
70
|
const linked = linkedProjectRef(cwd);
|
|
@@ -117,7 +117,7 @@ export function init(options = {}, cwd = process.cwd()) {
|
|
|
117
117
|
...(wrangler.json.previews
|
|
118
118
|
? []
|
|
119
119
|
: [`Add a "previews" block to ${wrangler.file}, redeclaring every binding the Worker uses.`]),
|
|
120
|
-
"Put SUPABASE_ACCESS_TOKEN, CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in .env.
|
|
120
|
+
"Put SUPABASE_ACCESS_TOKEN, CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in .env.supabase-worker-previews\n" +
|
|
121
121
|
`(git-ignore it; scopes: ${TOKENS_URL}),\n` +
|
|
122
122
|
"then run `npx supabase-worker-previews shared` and paste the previews.vars it prints.",
|
|
123
123
|
"Wrap the Worker's default export:\n" +
|
package/dist/commands/pr.js
CHANGED
|
@@ -4,10 +4,10 @@ import { apiErrorMessage } from "../http.js";
|
|
|
4
4
|
import { check, down, release, up } from "./branches.js";
|
|
5
5
|
export function readEvent(path = process.env.GITHUB_EVENT_PATH) {
|
|
6
6
|
if (!path)
|
|
7
|
-
throw new Error("GITHUB_EVENT_PATH is not set: `
|
|
7
|
+
throw new Error("GITHUB_EVENT_PATH is not set: `supabase-worker-previews pr` runs inside a pull_request workflow. To try one branch here, use `supabase-worker-previews up` or `supabase-worker-previews check`.");
|
|
8
8
|
const event = JSON.parse(readFileSync(path, "utf8"));
|
|
9
9
|
if (!event.pull_request)
|
|
10
|
-
throw new Error("`
|
|
10
|
+
throw new Error("`supabase-worker-previews pr` needs a pull_request event");
|
|
11
11
|
return event;
|
|
12
12
|
}
|
|
13
13
|
export const PR_ENV = [
|
|
@@ -25,14 +25,14 @@ export function skipReason(event, env = process.env) {
|
|
|
25
25
|
if (head !== undefined && head?.full_name !== event.repository.full_name)
|
|
26
26
|
return {
|
|
27
27
|
level: "notice",
|
|
28
|
-
message: `
|
|
28
|
+
message: `supabase-worker-previews pr skipped PR #${event.number}: it comes from ${head ? `the fork ${head.full_name}` : "a deleted fork"}, and GitHub Actions gives fork PRs no secrets`,
|
|
29
29
|
};
|
|
30
30
|
const missing = PR_ENV.filter((name) => !env[name]);
|
|
31
31
|
if (missing.length) {
|
|
32
32
|
const bot = env.GITHUB_ACTOR?.endsWith("[bot]") ?? false;
|
|
33
33
|
return {
|
|
34
34
|
level: bot ? "warning" : "error",
|
|
35
|
-
message: `
|
|
35
|
+
message: `supabase-worker-previews pr ${bot ? "skipped" : "cannot check"} PR #${event.number}: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} empty; add ${missing.length === 1 ? "it" : "them"} to the repository's Actions secrets`,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
return null;
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
export declare const CONFIG_FILE = "
|
|
1
|
+
export declare const CONFIG_FILE = "supabase-worker-previews.json";
|
|
2
|
+
/** The name before 0.2.0, still read when the current one is absent. */
|
|
3
|
+
export declare const LEGACY_CONFIG_FILE = "swp.config.json";
|
|
4
|
+
export declare const DOTENV_FILE = ".env.supabase-worker-previews";
|
|
5
|
+
export declare const LEGACY_DOTENV_FILE = ".env.swp";
|
|
6
|
+
/** The config file to read in `cwd`: the current name, else the legacy one if only that exists. */
|
|
7
|
+
export declare function configFileIn(cwd?: string): string;
|
|
2
8
|
export declare const WRANGLER_FILES: string[];
|
|
3
|
-
/** What `
|
|
9
|
+
/** What `supabase-worker-previews init` writes when it cannot find the project ref. */
|
|
4
10
|
export declare const PLACEHOLDER_REF = "<production project ref>";
|
|
5
11
|
export type Config = {
|
|
6
12
|
/** Worker name; defaults to `name` in the wrangler config. */
|
|
@@ -22,9 +28,9 @@ export type Config = {
|
|
|
22
28
|
previewName: "branch" | "pr";
|
|
23
29
|
/** Which Supabase API keys to hand a Preview when a project has both kinds. */
|
|
24
30
|
apiKeys: "legacy" | "new";
|
|
25
|
-
/** `
|
|
31
|
+
/** `supabase-worker-previews pr` keeps one status comment on the PR. Default true. */
|
|
26
32
|
prComment?: boolean;
|
|
27
|
-
/** `
|
|
33
|
+
/** `supabase-worker-previews pr` records a GitHub deployment for the PR head. Default true. */
|
|
28
34
|
githubDeployments?: boolean;
|
|
29
35
|
/** GitHub environment of those deployments, shared by every PR; `{branch}` makes one per branch. Default "Preview". */
|
|
30
36
|
deploymentEnvironment?: string;
|
package/dist/config.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { parse as parseToml } from "smol-toml";
|
|
4
4
|
import { parseJsonc } from "./jsonc.js";
|
|
5
5
|
import { didYouMean } from "./suggest.js";
|
|
6
|
-
export const CONFIG_FILE = "
|
|
6
|
+
export const CONFIG_FILE = "supabase-worker-previews.json";
|
|
7
|
+
/** The name before 0.2.0, still read when the current one is absent. */
|
|
8
|
+
export const LEGACY_CONFIG_FILE = "swp.config.json";
|
|
9
|
+
export const DOTENV_FILE = ".env.supabase-worker-previews";
|
|
10
|
+
export const LEGACY_DOTENV_FILE = ".env.swp";
|
|
11
|
+
/** The config file to read in `cwd`: the current name, else the legacy one if only that exists. */
|
|
12
|
+
export function configFileIn(cwd = process.cwd()) {
|
|
13
|
+
return !existsSync(join(cwd, CONFIG_FILE)) && existsSync(join(cwd, LEGACY_CONFIG_FILE))
|
|
14
|
+
? LEGACY_CONFIG_FILE
|
|
15
|
+
: CONFIG_FILE;
|
|
16
|
+
}
|
|
7
17
|
export const WRANGLER_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
8
|
-
/** What `
|
|
18
|
+
/** What `supabase-worker-previews init` writes when it cannot find the project ref. */
|
|
9
19
|
export const PLACEHOLDER_REF = "<production project ref>";
|
|
10
20
|
export const CONFIG_DEFAULTS = {
|
|
11
21
|
trunk: "main",
|
|
@@ -48,44 +58,46 @@ export function readWranglerConfig(cwd = process.cwd()) {
|
|
|
48
58
|
return { file, json, name: typeof json.name === "string" ? json.name : undefined };
|
|
49
59
|
}
|
|
50
60
|
export function loadConfig(overrides = {}, cwd = process.cwd()) {
|
|
51
|
-
const
|
|
61
|
+
const name = configFileIn(cwd);
|
|
62
|
+
const path = join(cwd, name);
|
|
52
63
|
const exists = existsSync(path);
|
|
53
64
|
const file = exists ? readConfigFile(path) : {};
|
|
54
65
|
const wrangler = readWranglerConfig(cwd);
|
|
55
66
|
const merged = { ...CONFIG_DEFAULTS, worker: wrangler?.name, ...file, ...defined(overrides) };
|
|
56
67
|
if (!merged.supabaseProjectRef)
|
|
57
68
|
throw new Error(exists
|
|
58
|
-
? `Set "supabaseProjectRef" (the production project) in ${
|
|
59
|
-
: `No ${
|
|
69
|
+
? `Set "supabaseProjectRef" (the production project) in ${name}`
|
|
70
|
+
: `No ${name} in this directory. Run \`supabase-worker-previews init\` first, or pass --project-ref <ref>.`);
|
|
60
71
|
if (merged.supabaseProjectRef === PLACEHOLDER_REF)
|
|
61
|
-
throw new Error(`"supabaseProjectRef" in ${
|
|
72
|
+
throw new Error(`"supabaseProjectRef" in ${name} is still ${PLACEHOLDER_REF}. ` +
|
|
62
73
|
"Set it to the production project's ref, the <ref> in https://<ref>.supabase.co.");
|
|
63
74
|
if (!merged.worker)
|
|
64
75
|
throw new Error(wrangler
|
|
65
|
-
? `${wrangler.file} has no "name"; add the Worker's name there or "worker" in ${
|
|
66
|
-
: `No ${WRANGLER_FILES.join(", ")} in this directory. Run
|
|
76
|
+
? `${wrangler.file} has no "name"; add the Worker's name there or "worker" in ${name}`
|
|
77
|
+
: `No ${WRANGLER_FILES.join(", ")} in this directory. Run supabase-worker-previews in the Worker's directory, or set "worker" in ${name}.`);
|
|
67
78
|
for (const [key, allowed] of Object.entries(CHOICES)) {
|
|
68
79
|
const value = merged[key];
|
|
69
80
|
if (!allowed.includes(value))
|
|
70
|
-
throw new Error(`"${key}" in ${
|
|
81
|
+
throw new Error(`"${key}" in ${name} must be ${allowed.map((a) => `"${a}"`).join(" or ")}`);
|
|
71
82
|
}
|
|
72
83
|
return merged;
|
|
73
84
|
}
|
|
74
85
|
function readConfigFile(path) {
|
|
86
|
+
const name = basename(path);
|
|
75
87
|
let json;
|
|
76
88
|
try {
|
|
77
89
|
json = JSON.parse(readFileSync(path, "utf8"));
|
|
78
90
|
}
|
|
79
91
|
catch (err) {
|
|
80
|
-
throw new Error(`Cannot parse ${
|
|
92
|
+
throw new Error(`Cannot parse ${name}: ${err.message}`, { cause: err });
|
|
81
93
|
}
|
|
82
94
|
if (!json || typeof json !== "object" || Array.isArray(json))
|
|
83
|
-
throw new Error(`${
|
|
95
|
+
throw new Error(`${name} must hold a JSON object`);
|
|
84
96
|
for (const key of Object.keys(json)) {
|
|
85
97
|
if (KEYS.includes(key))
|
|
86
98
|
continue;
|
|
87
99
|
const guess = didYouMean(key, KEYS);
|
|
88
|
-
throw new Error(`Unknown key "${key}" in ${
|
|
100
|
+
throw new Error(`Unknown key "${key}" in ${name}.${guess ? ` Did you mean "${guess}"?` : ""} See the README for the keys.`);
|
|
89
101
|
}
|
|
90
102
|
return json;
|
|
91
103
|
}
|
package/dist/feedback.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type FeedbackDeps = Deps & {
|
|
|
29
29
|
clock?: () => Date;
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
|
-
* The PR comment and GitHub deployment for one `
|
|
32
|
+
* The PR comment and GitHub deployment for one `supabase-worker-previews pr` run. Any GitHub API
|
|
33
33
|
* failure becomes a warning; only the wrapped database work decides the job.
|
|
34
34
|
*/
|
|
35
35
|
export declare class Feedback {
|
package/dist/feedback.js
CHANGED
|
@@ -44,7 +44,7 @@ export function warning(message) {
|
|
|
44
44
|
return `::warning::${message.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A")}`;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* The PR comment and GitHub deployment for one `
|
|
47
|
+
* The PR comment and GitHub deployment for one `supabase-worker-previews pr` run. Any GitHub API
|
|
48
48
|
* failure becomes a warning; only the wrapped database work decides the job.
|
|
49
49
|
*/
|
|
50
50
|
export class Feedback {
|
package/dist/run.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type Runner = {
|
|
|
12
12
|
export declare function makeRunner(dryRun: boolean, log?: (line: string) => void): Runner;
|
|
13
13
|
/**
|
|
14
14
|
* `--flag value`, `--flag=value` and bare `--flag`. A flag in `booleans` never
|
|
15
|
-
* takes the next argument, so `
|
|
15
|
+
* takes the next argument, so `supabase-worker-previews --dry-run up` keeps `up` as the command.
|
|
16
16
|
*/
|
|
17
17
|
export declare function parseArgs(argv: string[], booleans?: ReadonlySet<string>): {
|
|
18
18
|
positional: string[];
|
package/dist/run.js
CHANGED
|
@@ -29,7 +29,7 @@ export function makeRunner(dryRun, log = console.log) {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* `--flag value`, `--flag=value` and bare `--flag`. A flag in `booleans` never
|
|
32
|
-
* takes the next argument, so `
|
|
32
|
+
* takes the next argument, so `supabase-worker-previews --dry-run up` keeps `up` as the command.
|
|
33
33
|
*/
|
|
34
34
|
export function parseArgs(argv, booleans = new Set()) {
|
|
35
35
|
const positional = [];
|
package/dist/runtime.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ type EnvOf<H> = H extends {
|
|
|
50
50
|
/**
|
|
51
51
|
* Wraps a Worker's default export. Every handler sees the override applied;
|
|
52
52
|
* `fetch` also injects the public config and answers the identity route that
|
|
53
|
-
* `
|
|
53
|
+
* `supabase-worker-previews check` reads.
|
|
54
54
|
*/
|
|
55
55
|
export declare function withSupabasePreviews<H extends object, E extends object = EnvOf<H>>(handler: H & Handler, options?: PreviewOptions): H;
|
|
56
56
|
export {};
|
package/dist/runtime.js
CHANGED
|
@@ -96,7 +96,7 @@ function envOrProcessEnv(env) {
|
|
|
96
96
|
/**
|
|
97
97
|
* Wraps a Worker's default export. Every handler sees the override applied;
|
|
98
98
|
* `fetch` also injects the public config and answers the identity route that
|
|
99
|
-
* `
|
|
99
|
+
* `supabase-worker-previews check` reads.
|
|
100
100
|
*/
|
|
101
101
|
export function withSupabasePreviews(handler, options = {}) {
|
|
102
102
|
const { globalName = DEFAULT_GLOBAL, inject = true, identity = true } = options;
|
package/dist/supabase.js
CHANGED
|
@@ -148,7 +148,7 @@ export function explainCreateBranchError(ref, name, err) {
|
|
|
148
148
|
advice = `The organization's plan does not allow persistent branches${upgrade}.`;
|
|
149
149
|
else if (gate?.feature === "branching_limit" ||
|
|
150
150
|
/branch(es|ing)?[ _]limit|(maximum|max|too many)[\w ]* branches/i.test(err.body))
|
|
151
|
-
advice = `Project ${ref} has reached its branch limit. Delete branches you no longer need (\`
|
|
151
|
+
advice = `Project ${ref} has reached its branch limit. Delete branches you no longer need (\`supabase-worker-previews prune\` lists leftovers) or raise the limit${upgrade}.`;
|
|
152
152
|
else if (gate)
|
|
153
153
|
advice = `The organization's plan does not include ${gate.feature ?? "this feature"}${upgrade}.`;
|
|
154
154
|
else if (err.status === 401)
|
package/dist/usage.js
CHANGED
|
@@ -9,10 +9,22 @@ const TOKENS = ["SUPABASE_ACCESS_TOKEN", "CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACC
|
|
|
9
9
|
const WITH_GITHUB = [...TOKENS, "GITHUB_TOKEN"];
|
|
10
10
|
const DRY_RUN = { name: "dry-run", help: "Print the plan; change nothing" };
|
|
11
11
|
const OVERRIDES = [
|
|
12
|
-
{
|
|
12
|
+
{
|
|
13
|
+
name: "dotenv",
|
|
14
|
+
value: "<path>",
|
|
15
|
+
help: "Load tokens from this file (default: .env.supabase-worker-previews when present)",
|
|
16
|
+
},
|
|
13
17
|
{ name: "worker", value: "<name>", help: 'Worker name (default: "name" in the wrangler config)' },
|
|
14
|
-
{
|
|
15
|
-
|
|
18
|
+
{
|
|
19
|
+
name: "project-ref",
|
|
20
|
+
value: "<ref>",
|
|
21
|
+
help: "Production project ref (default: supabase-worker-previews.json)",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "trunk",
|
|
25
|
+
value: "<branch>",
|
|
26
|
+
help: "Branch the shared database tracks (default: supabase-worker-previews.json)",
|
|
27
|
+
},
|
|
16
28
|
];
|
|
17
29
|
const BRANCH = [
|
|
18
30
|
{ name: "branch", value: "<b>", help: "Git branch (default: the current one, or the CI branch)" },
|
|
@@ -27,8 +39,8 @@ export const COMMANDS = [
|
|
|
27
39
|
name: "init",
|
|
28
40
|
group: "Set up",
|
|
29
41
|
args: "[--project-ref <ref>] [--trunk <branch>] [--action]",
|
|
30
|
-
summary: "Scaffold
|
|
31
|
-
details: "Writes
|
|
42
|
+
summary: "Scaffold supabase-worker-previews.json, the grants migration and the PR workflow",
|
|
43
|
+
details: "Writes supabase-worker-previews.json, a first migration that grants the API roles default privileges, and\n" +
|
|
32
44
|
".github/workflows/supabase-previews.yml. Never overwrites a file; run it again safely.",
|
|
33
45
|
flags: [
|
|
34
46
|
{ name: "project-ref", value: "<ref>", help: "Production project ref (default: from `supabase link`)" },
|
|
@@ -45,7 +57,10 @@ export const COMMANDS = [
|
|
|
45
57
|
],
|
|
46
58
|
env: [],
|
|
47
59
|
dryRunEnv: [],
|
|
48
|
-
examples: [
|
|
60
|
+
examples: [
|
|
61
|
+
"supabase-worker-previews init --project-ref abcdefghijklmnopqrst",
|
|
62
|
+
"supabase-worker-previews init --action",
|
|
63
|
+
],
|
|
49
64
|
},
|
|
50
65
|
{
|
|
51
66
|
name: "doctor",
|
|
@@ -58,7 +73,7 @@ export const COMMANDS = [
|
|
|
58
73
|
flags: OVERRIDES,
|
|
59
74
|
env: [],
|
|
60
75
|
dryRunEnv: [],
|
|
61
|
-
examples: ["
|
|
76
|
+
examples: ["supabase-worker-previews doctor"],
|
|
62
77
|
},
|
|
63
78
|
{
|
|
64
79
|
name: "shared",
|
|
@@ -71,7 +86,7 @@ export const COMMANDS = [
|
|
|
71
86
|
flags: [DRY_RUN, ...OVERRIDES],
|
|
72
87
|
env: TOKENS,
|
|
73
88
|
dryRunEnv: ["SUPABASE_ACCESS_TOKEN"],
|
|
74
|
-
examples: ["
|
|
89
|
+
examples: ["supabase-worker-previews shared --dry-run", "supabase-worker-previews shared"],
|
|
75
90
|
},
|
|
76
91
|
{
|
|
77
92
|
name: "up",
|
|
@@ -83,7 +98,7 @@ export const COMMANDS = [
|
|
|
83
98
|
flags: [...BRANCH, DRY_RUN, ...OVERRIDES],
|
|
84
99
|
env: TOKENS,
|
|
85
100
|
dryRunEnv: [],
|
|
86
|
-
examples: ["
|
|
101
|
+
examples: ["supabase-worker-previews up --branch feat/notes --dry-run"],
|
|
87
102
|
},
|
|
88
103
|
{
|
|
89
104
|
name: "check",
|
|
@@ -100,7 +115,7 @@ export const COMMANDS = [
|
|
|
100
115
|
],
|
|
101
116
|
env: TOKENS,
|
|
102
117
|
dryRunEnv: [],
|
|
103
|
-
examples: ["
|
|
118
|
+
examples: ["supabase-worker-previews check --branch feat/notes --isolated"],
|
|
104
119
|
},
|
|
105
120
|
{
|
|
106
121
|
name: "down",
|
|
@@ -111,7 +126,7 @@ export const COMMANDS = [
|
|
|
111
126
|
flags: [...BRANCH, DRY_RUN, ...OVERRIDES],
|
|
112
127
|
env: TOKENS,
|
|
113
128
|
dryRunEnv: TOKENS,
|
|
114
|
-
examples: ["
|
|
129
|
+
examples: ["supabase-worker-previews down --branch feat/notes --dry-run"],
|
|
115
130
|
},
|
|
116
131
|
{
|
|
117
132
|
name: "pr",
|
|
@@ -129,7 +144,7 @@ export const COMMANDS = [
|
|
|
129
144
|
],
|
|
130
145
|
env: WITH_GITHUB,
|
|
131
146
|
dryRunEnv: WITH_GITHUB,
|
|
132
|
-
examples: ["
|
|
147
|
+
examples: ["supabase-worker-previews pr"],
|
|
133
148
|
},
|
|
134
149
|
{
|
|
135
150
|
name: "prune",
|
|
@@ -149,7 +164,7 @@ export const COMMANDS = [
|
|
|
149
164
|
],
|
|
150
165
|
env: WITH_GITHUB,
|
|
151
166
|
dryRunEnv: WITH_GITHUB,
|
|
152
|
-
examples: ["
|
|
167
|
+
examples: ["supabase-worker-previews prune", "supabase-worker-previews prune --yes"],
|
|
153
168
|
},
|
|
154
169
|
];
|
|
155
170
|
const ALL_FLAGS = [...COMMANDS.flatMap((c) => c.flags), ...HELP];
|
|
@@ -175,9 +190,9 @@ export function mainHelp(s = makeStyle(false)) {
|
|
|
175
190
|
...COMMANDS.filter((c) => c.group === group).map((c) => ` ${s.cyan(c.name.padEnd(width))} ${c.summary}`),
|
|
176
191
|
]);
|
|
177
192
|
return [
|
|
178
|
-
`${s.bold("
|
|
193
|
+
`${s.bold("supabase-worker-previews")} ${version()}: branch previews for Cloudflare Workers on Supabase branching`,
|
|
179
194
|
"",
|
|
180
|
-
`${s.bold("Usage:")}
|
|
195
|
+
`${s.bold("Usage:")} supabase-worker-previews <command> [flags]`,
|
|
181
196
|
...groups,
|
|
182
197
|
"",
|
|
183
198
|
s.bold("Common flags"),
|
|
@@ -192,14 +207,14 @@ export function mainHelp(s = makeStyle(false)) {
|
|
|
192
207
|
" npx supabase-worker-previews doctor",
|
|
193
208
|
" npx supabase-worker-previews shared --dry-run",
|
|
194
209
|
"",
|
|
195
|
-
`Run ${s.cyan("
|
|
210
|
+
`Run ${s.cyan("supabase-worker-previews <command> --help")} for one command. Docs: ${DOCS_URL}`,
|
|
196
211
|
].join("\n");
|
|
197
212
|
}
|
|
198
213
|
export function commandHelp(c, s = makeStyle(false)) {
|
|
199
214
|
return [
|
|
200
|
-
`${s.bold(`
|
|
215
|
+
`${s.bold(`supabase-worker-previews ${c.name}`)}: ${c.summary.charAt(0).toLowerCase()}${c.summary.slice(1)}`,
|
|
201
216
|
"",
|
|
202
|
-
`${s.bold("Usage:")}
|
|
217
|
+
`${s.bold("Usage:")} supabase-worker-previews ${c.name}${c.args ? ` ${c.args}` : ""}`,
|
|
203
218
|
"",
|
|
204
219
|
c.details,
|
|
205
220
|
"",
|
|
@@ -259,11 +274,11 @@ function unknownCommand(name) {
|
|
|
259
274
|
const guess = didYouMean(name, COMMANDS.map((c) => c.name));
|
|
260
275
|
return {
|
|
261
276
|
kind: "error",
|
|
262
|
-
message: `Unknown command "${name}".${guess ? ` Did you mean "
|
|
277
|
+
message: `Unknown command "${name}".${guess ? ` Did you mean "supabase-worker-previews ${guess}"?` : ""}\nRun supabase-worker-previews --help to see every command.`,
|
|
263
278
|
};
|
|
264
279
|
}
|
|
265
280
|
function usageError(message, name) {
|
|
266
|
-
return { kind: "error", message: `${message}\nRun
|
|
281
|
+
return { kind: "error", message: `${message}\nRun supabase-worker-previews ${name} --help for its usage.` };
|
|
267
282
|
}
|
|
268
283
|
const ENV_SOURCES = {
|
|
269
284
|
SUPABASE_ACCESS_TOKEN: "https://supabase.com/dashboard/account/tokens (organization-scoped)",
|
|
@@ -281,11 +296,11 @@ export function missingEnvMessage(name, dryRun, env = process.env) {
|
|
|
281
296
|
return [
|
|
282
297
|
`${name}${dryRun ? " --dry-run" : ""} needs ${missing.length === 1 ? "this variable" : "these variables"}, not set:`,
|
|
283
298
|
...missing.map((v) => ` ${v.padEnd(width)} ${ENV_SOURCES[v]}`),
|
|
284
|
-
`Put ${missing.length === 1 ? "it" : "them"} in .env.
|
|
299
|
+
`Put ${missing.length === 1 ? "it" : "them"} in .env.supabase-worker-previews (git-ignore it) or the environment. Scopes: ${TOKENS_URL}`,
|
|
285
300
|
].join("\n");
|
|
286
301
|
}
|
|
287
302
|
/** One line for the user: the message, and for a network failure the host and the system error. */
|
|
288
|
-
export function formatError(err, debug = !!process.env.SWP_DEBUG) {
|
|
303
|
+
export function formatError(err, debug = !!(process.env.SUPABASE_WORKER_PREVIEWS_DEBUG || process.env.SWP_DEBUG)) {
|
|
289
304
|
if (!(err instanceof Error))
|
|
290
305
|
return String(err);
|
|
291
306
|
if (debug && err.stack)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supabase-worker-previews",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Branch previews for Cloudflare Workers on Supabase branching: every git branch gets a Worker Preview, and every PR that changes the schema gets its own Supabase database.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Matt Ruby",
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"node": ">=22.0.0"
|
|
35
35
|
},
|
|
36
36
|
"bin": {
|
|
37
|
-
"swp": "dist/cli.js",
|
|
38
37
|
"supabase-worker-previews": "dist/cli.js"
|
|
39
38
|
},
|
|
40
39
|
"main": "./dist/runtime.js",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: Preview database
|
|
2
2
|
|
|
3
3
|
# The same job as supabase-previews.yml, through the published action. It runs
|
|
4
|
-
# the project's installed
|
|
4
|
+
# the project's installed supabase-worker-previews when there is one, else downloads the package.
|
|
5
5
|
|
|
6
6
|
on:
|
|
7
7
|
pull_request:
|