supabase-worker-previews 0.1.0 → 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 +38 -36
- 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 +10 -10
- 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 +44 -26
- package/package.json +2 -2
- package/templates/supabase-previews-action.yml +1 -1
- package/templates/supabase-previews.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
|
|
|
@@ -58,14 +58,14 @@ You need a Worker deployed by Workers Builds from a GitHub repo, wrangler 4.135.
|
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
npm install --save-dev supabase-worker-previews
|
|
61
|
-
npx
|
|
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.
|
|
68
|
-
3. `npx
|
|
68
|
+
3. `npx supabase-worker-previews shared` creates the shared Preview database and prints its `previews.vars`.
|
|
69
69
|
4. Wrap the Worker:
|
|
70
70
|
|
|
71
71
|
```ts
|
|
@@ -77,47 +77,49 @@ npx swp init --project-ref <production project ref>
|
|
|
77
77
|
|
|
78
78
|
and create the browser client from `readPublicConfig()`.
|
|
79
79
|
|
|
80
|
-
5. Turn on Preview builds in Workers Builds, add three Actions secrets, run `npx
|
|
80
|
+
5. Turn on Preview builds in Workers Builds, add three Actions secrets, run `npx supabase-worker-previews doctor`, and open a PR.
|
|
81
81
|
|
|
82
82
|
The **[full quickstart](docs/quickstart.md)** walks through every step with the dashboard settings and the output to expect. For a complete working project, see [`examples/hono-notes`](examples/hono-notes).
|
|
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
|
+
|
|
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).
|
|
98
100
|
|
|
99
101
|
## Safety
|
|
100
102
|
|
|
101
|
-
-
|
|
102
|
-
- `
|
|
103
|
-
- `
|
|
104
|
-
- `
|
|
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`.
|
|
105
107
|
|
|
106
|
-
[Security](docs/security.md) covers what
|
|
108
|
+
[Security](docs/security.md) covers what the CLI can touch and what each token can reach.
|
|
107
109
|
|
|
108
110
|
## Docs
|
|
109
111
|
|
|
110
|
-
| Page | For
|
|
111
|
-
| ------------------------------------------ |
|
|
112
|
-
| [Concepts](docs/concepts.md) | Worker Previews, Supabase branches, and how
|
|
113
|
-
| [Quickstart](docs/quickstart.md) | From an existing Worker to the first working PR Preview
|
|
114
|
-
| [Frameworks](docs/README.md#frameworks) | TanStack Start, Hono, React Router v7, Astro
|
|
115
|
-
| [Troubleshooting](docs/troubleshooting.md) | Symptoms, causes and fixes
|
|
116
|
-
| [How it works](docs/how-it-works.md) | Every moving part and why it is built that way
|
|
117
|
-
| [Configuration](docs/configuration.md) | Commands, flags, `
|
|
118
|
-
| [Tokens](docs/tokens.md) | Least-privilege Cloudflare, Supabase and GitHub credentials
|
|
119
|
-
| [Security](docs/security.md) | What
|
|
120
|
-
| [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 |
|
|
121
123
|
|
|
122
124
|
The full index is [docs/README.md](docs/README.md).
|
|
123
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);
|
|
@@ -104,7 +104,7 @@ export function init(options = {}, cwd = process.cwd()) {
|
|
|
104
104
|
log(`+ ${WORKFLOW}`);
|
|
105
105
|
log(options.action
|
|
106
106
|
? " It uses the published action, mattruby/supabase-worker-previews@v0."
|
|
107
|
-
: " It runs
|
|
107
|
+
: " It runs `npx supabase-worker-previews pr`; `npx supabase-worker-previews init --action` writes one that uses the published action instead.");
|
|
108
108
|
}
|
|
109
109
|
const steps = [
|
|
110
110
|
...(needsRef
|
|
@@ -117,15 +117,15 @@ 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
|
-
"then run `npx
|
|
122
|
+
"then run `npx supabase-worker-previews shared` and paste the previews.vars it prints.",
|
|
123
123
|
"Wrap the Worker's default export:\n" +
|
|
124
124
|
' import { withSupabasePreviews } from "supabase-worker-previews";\n' +
|
|
125
125
|
" export default withSupabasePreviews(app);",
|
|
126
126
|
"Workers Builds: turn on non-production branch builds, deploy command `npx wrangler preview`.",
|
|
127
127
|
"GitHub repo, Settings, Secrets and variables, Actions: add the same three tokens.",
|
|
128
|
-
"Run `npx
|
|
128
|
+
"Run `npx supabase-worker-previews doctor` until it passes, then commit and open a PR.",
|
|
129
129
|
];
|
|
130
130
|
log(`\nNext, by hand (the full guide: ${QUICKSTART_URL}):`);
|
|
131
131
|
steps.forEach((step, i) => log(` ${i + 1}. ${step.replace(/\n/g, "\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`)" },
|
|
@@ -37,12 +49,18 @@ export const COMMANDS = [
|
|
|
37
49
|
value: "<branch>",
|
|
38
50
|
help: "Branch the shared database tracks (default: origin/HEAD, else main)",
|
|
39
51
|
},
|
|
40
|
-
{
|
|
52
|
+
{
|
|
53
|
+
name: "action",
|
|
54
|
+
help: "Write a workflow that uses the published GitHub Action instead of npx supabase-worker-previews",
|
|
55
|
+
},
|
|
41
56
|
{ ...DRY_RUN, help: "Print what it would write; write nothing" },
|
|
42
57
|
],
|
|
43
58
|
env: [],
|
|
44
59
|
dryRunEnv: [],
|
|
45
|
-
examples: [
|
|
60
|
+
examples: [
|
|
61
|
+
"supabase-worker-previews init --project-ref abcdefghijklmnopqrst",
|
|
62
|
+
"supabase-worker-previews init --action",
|
|
63
|
+
],
|
|
46
64
|
},
|
|
47
65
|
{
|
|
48
66
|
name: "doctor",
|
|
@@ -55,7 +73,7 @@ export const COMMANDS = [
|
|
|
55
73
|
flags: OVERRIDES,
|
|
56
74
|
env: [],
|
|
57
75
|
dryRunEnv: [],
|
|
58
|
-
examples: ["
|
|
76
|
+
examples: ["supabase-worker-previews doctor"],
|
|
59
77
|
},
|
|
60
78
|
{
|
|
61
79
|
name: "shared",
|
|
@@ -68,7 +86,7 @@ export const COMMANDS = [
|
|
|
68
86
|
flags: [DRY_RUN, ...OVERRIDES],
|
|
69
87
|
env: TOKENS,
|
|
70
88
|
dryRunEnv: ["SUPABASE_ACCESS_TOKEN"],
|
|
71
|
-
examples: ["
|
|
89
|
+
examples: ["supabase-worker-previews shared --dry-run", "supabase-worker-previews shared"],
|
|
72
90
|
},
|
|
73
91
|
{
|
|
74
92
|
name: "up",
|
|
@@ -80,7 +98,7 @@ export const COMMANDS = [
|
|
|
80
98
|
flags: [...BRANCH, DRY_RUN, ...OVERRIDES],
|
|
81
99
|
env: TOKENS,
|
|
82
100
|
dryRunEnv: [],
|
|
83
|
-
examples: ["
|
|
101
|
+
examples: ["supabase-worker-previews up --branch feat/notes --dry-run"],
|
|
84
102
|
},
|
|
85
103
|
{
|
|
86
104
|
name: "check",
|
|
@@ -97,7 +115,7 @@ export const COMMANDS = [
|
|
|
97
115
|
],
|
|
98
116
|
env: TOKENS,
|
|
99
117
|
dryRunEnv: [],
|
|
100
|
-
examples: ["
|
|
118
|
+
examples: ["supabase-worker-previews check --branch feat/notes --isolated"],
|
|
101
119
|
},
|
|
102
120
|
{
|
|
103
121
|
name: "down",
|
|
@@ -108,7 +126,7 @@ export const COMMANDS = [
|
|
|
108
126
|
flags: [...BRANCH, DRY_RUN, ...OVERRIDES],
|
|
109
127
|
env: TOKENS,
|
|
110
128
|
dryRunEnv: TOKENS,
|
|
111
|
-
examples: ["
|
|
129
|
+
examples: ["supabase-worker-previews down --branch feat/notes --dry-run"],
|
|
112
130
|
},
|
|
113
131
|
{
|
|
114
132
|
name: "pr",
|
|
@@ -126,7 +144,7 @@ export const COMMANDS = [
|
|
|
126
144
|
],
|
|
127
145
|
env: WITH_GITHUB,
|
|
128
146
|
dryRunEnv: WITH_GITHUB,
|
|
129
|
-
examples: ["
|
|
147
|
+
examples: ["supabase-worker-previews pr"],
|
|
130
148
|
},
|
|
131
149
|
{
|
|
132
150
|
name: "prune",
|
|
@@ -146,7 +164,7 @@ export const COMMANDS = [
|
|
|
146
164
|
],
|
|
147
165
|
env: WITH_GITHUB,
|
|
148
166
|
dryRunEnv: WITH_GITHUB,
|
|
149
|
-
examples: ["
|
|
167
|
+
examples: ["supabase-worker-previews prune", "supabase-worker-previews prune --yes"],
|
|
150
168
|
},
|
|
151
169
|
];
|
|
152
170
|
const ALL_FLAGS = [...COMMANDS.flatMap((c) => c.flags), ...HELP];
|
|
@@ -172,9 +190,9 @@ export function mainHelp(s = makeStyle(false)) {
|
|
|
172
190
|
...COMMANDS.filter((c) => c.group === group).map((c) => ` ${s.cyan(c.name.padEnd(width))} ${c.summary}`),
|
|
173
191
|
]);
|
|
174
192
|
return [
|
|
175
|
-
`${s.bold("
|
|
193
|
+
`${s.bold("supabase-worker-previews")} ${version()}: branch previews for Cloudflare Workers on Supabase branching`,
|
|
176
194
|
"",
|
|
177
|
-
`${s.bold("Usage:")}
|
|
195
|
+
`${s.bold("Usage:")} supabase-worker-previews <command> [flags]`,
|
|
178
196
|
...groups,
|
|
179
197
|
"",
|
|
180
198
|
s.bold("Common flags"),
|
|
@@ -185,18 +203,18 @@ export function mainHelp(s = makeStyle(false)) {
|
|
|
185
203
|
` Where to get them: ${TOKENS_URL}`,
|
|
186
204
|
"",
|
|
187
205
|
s.bold("Examples"),
|
|
188
|
-
" npx
|
|
189
|
-
" npx
|
|
190
|
-
" npx
|
|
206
|
+
" npx supabase-worker-previews init --project-ref abcdefghijklmnopqrst",
|
|
207
|
+
" npx supabase-worker-previews doctor",
|
|
208
|
+
" npx supabase-worker-previews shared --dry-run",
|
|
191
209
|
"",
|
|
192
|
-
`Run ${s.cyan("
|
|
210
|
+
`Run ${s.cyan("supabase-worker-previews <command> --help")} for one command. Docs: ${DOCS_URL}`,
|
|
193
211
|
].join("\n");
|
|
194
212
|
}
|
|
195
213
|
export function commandHelp(c, s = makeStyle(false)) {
|
|
196
214
|
return [
|
|
197
|
-
`${s.bold(`
|
|
215
|
+
`${s.bold(`supabase-worker-previews ${c.name}`)}: ${c.summary.charAt(0).toLowerCase()}${c.summary.slice(1)}`,
|
|
198
216
|
"",
|
|
199
|
-
`${s.bold("Usage:")}
|
|
217
|
+
`${s.bold("Usage:")} supabase-worker-previews ${c.name}${c.args ? ` ${c.args}` : ""}`,
|
|
200
218
|
"",
|
|
201
219
|
c.details,
|
|
202
220
|
"",
|
|
@@ -256,11 +274,11 @@ function unknownCommand(name) {
|
|
|
256
274
|
const guess = didYouMean(name, COMMANDS.map((c) => c.name));
|
|
257
275
|
return {
|
|
258
276
|
kind: "error",
|
|
259
|
-
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.`,
|
|
260
278
|
};
|
|
261
279
|
}
|
|
262
280
|
function usageError(message, name) {
|
|
263
|
-
return { kind: "error", message: `${message}\nRun
|
|
281
|
+
return { kind: "error", message: `${message}\nRun supabase-worker-previews ${name} --help for its usage.` };
|
|
264
282
|
}
|
|
265
283
|
const ENV_SOURCES = {
|
|
266
284
|
SUPABASE_ACCESS_TOKEN: "https://supabase.com/dashboard/account/tokens (organization-scoped)",
|
|
@@ -278,11 +296,11 @@ export function missingEnvMessage(name, dryRun, env = process.env) {
|
|
|
278
296
|
return [
|
|
279
297
|
`${name}${dryRun ? " --dry-run" : ""} needs ${missing.length === 1 ? "this variable" : "these variables"}, not set:`,
|
|
280
298
|
...missing.map((v) => ` ${v.padEnd(width)} ${ENV_SOURCES[v]}`),
|
|
281
|
-
`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}`,
|
|
282
300
|
].join("\n");
|
|
283
301
|
}
|
|
284
302
|
/** One line for the user: the message, and for a network failure the host and the system error. */
|
|
285
|
-
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)) {
|
|
286
304
|
if (!(err instanceof Error))
|
|
287
305
|
return String(err);
|
|
288
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,7 @@
|
|
|
34
34
|
"node": ">=22.0.0"
|
|
35
35
|
},
|
|
36
36
|
"bin": {
|
|
37
|
-
"
|
|
37
|
+
"supabase-worker-previews": "dist/cli.js"
|
|
38
38
|
},
|
|
39
39
|
"main": "./dist/runtime.js",
|
|
40
40
|
"types": "./dist/runtime.d.ts",
|
|
@@ -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:
|