supabase-worker-previews 0.0.0 → 0.1.1

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +152 -0
  5. package/dist/cloudflare.d.ts +37 -0
  6. package/dist/cloudflare.js +95 -0
  7. package/dist/commands/branches.d.ts +62 -0
  8. package/dist/commands/branches.js +273 -0
  9. package/dist/commands/doctor.d.ts +24 -0
  10. package/dist/commands/doctor.js +289 -0
  11. package/dist/commands/init.d.ts +11 -0
  12. package/dist/commands/init.js +132 -0
  13. package/dist/commands/pr.d.ts +37 -0
  14. package/dist/commands/pr.js +78 -0
  15. package/dist/commands/prune.d.ts +32 -0
  16. package/dist/commands/prune.js +110 -0
  17. package/dist/config.d.ts +50 -0
  18. package/dist/config.js +105 -0
  19. package/dist/feedback.d.ts +64 -0
  20. package/dist/feedback.js +238 -0
  21. package/dist/github.d.ts +33 -0
  22. package/dist/github.js +78 -0
  23. package/dist/http.d.ts +7 -0
  24. package/dist/http.js +44 -0
  25. package/dist/jsonc.d.ts +2 -0
  26. package/dist/jsonc.js +56 -0
  27. package/dist/preview-name.d.ts +3 -0
  28. package/dist/preview-name.js +15 -0
  29. package/dist/run.d.ts +21 -0
  30. package/dist/run.js +59 -0
  31. package/dist/runtime.d.ts +56 -0
  32. package/dist/runtime.js +121 -0
  33. package/dist/style.d.ts +8 -0
  34. package/dist/style.js +21 -0
  35. package/dist/suggest.d.ts +2 -0
  36. package/dist/suggest.js +22 -0
  37. package/dist/supabase.d.ts +78 -0
  38. package/dist/supabase.js +168 -0
  39. package/dist/usage.d.ts +46 -0
  40. package/dist/usage.js +297 -0
  41. package/package.json +84 -9
  42. package/templates/default-privileges.sql +9 -0
  43. package/templates/supabase-previews-action.yml +29 -0
  44. package/templates/supabase-previews.yml +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Ruby
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1 +1,143 @@
1
- Placeholder. See https://github.com/mattruby/supabase-worker-previews
1
+ # supabase-worker-previews
2
+
3
+ [![npm](https://img.shields.io/npm/v/supabase-worker-previews)](https://www.npmjs.com/package/supabase-worker-previews)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/mattruby/supabase-worker-previews/ci.yml?branch=main&label=CI)](https://github.com/mattruby/supabase-worker-previews/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/github/license/mattruby/supabase-worker-previews)](LICENSE)
6
+
7
+ **Vercel-style PR Previews for a Cloudflare Worker on Supabase, each on the right database, with a check that proves it.**
8
+
9
+ Every branch gets a [Worker Preview](https://developers.cloudflare.com/workers/previews/) on a shared Preview database. Every PR that changes `supabase/` gets its own database, migrated and seeded from the PR. One build serves all of them, and no Preview ever serves production.
10
+
11
+ ## What you get
12
+
13
+ On every pull request, one comment that stays up to date:
14
+
15
+ > **Supabase Worker Preview** for `my-app`
16
+ >
17
+ > | Status | Preview | Database | Commit | Updated (UTC) |
18
+ > | :--------------------- | :----------------- | :--------------------------------------------- | :------------- | :--------------- |
19
+ > | **Passed** ([logs](#)) | [Visit Preview](#) | Shared [`preview`](#) (`abcdefghijklmnopqrst`) | [`1a2b3c4`](#) | 2026-10-01 03:10 |
20
+
21
+ Plus:
22
+
23
+ - a **View deployment** button on the PR, pointing at the Preview;
24
+ - a **Preview database** check that fails if the Preview serves the wrong database, and fails at once if it serves production;
25
+ - cleanup when the PR closes: the Preview and its own database are deleted, and the comment says so.
26
+
27
+ **You need this if** your app runs on Cloudflare Workers, deploys with [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/), keeps its data in Supabase, and you want every PR to have a working preview that cannot touch production data. If you are on Vercel, Supabase's own integration already does this; see [Compared with Vercel](docs/vs-vercel.md).
28
+
29
+ ## How it works
30
+
31
+ ```mermaid
32
+ flowchart LR
33
+ push["git push"] --> builds["Workers Builds"]
34
+ builds -- trunk --> prod["Production Worker"]
35
+ builds -- any other branch --> preview["Worker Preview<br/>npx wrangler preview"]
36
+ prod --> proddb[("Supabase project<br/>(production)")]
37
+ preview -- "previews.vars" --> shareddb[("Shared Preview database<br/>persistent branch 'preview'")]
38
+ pr["PR changes supabase/<br/>or has label isolated-db"] --> integ["Supabase GitHub integration"]
39
+ integ --> prdb[("The PR's own database")]
40
+ pr --> action["GitHub Actions: swp pr"]
41
+ action -- "SUPABASE_OVERRIDE secret" --> preview
42
+ preview -. "PRs with their own database" .-> prdb
43
+ action -- "swp check reads<br/>/.well-known/supabase-preview" --> preview
44
+ ```
45
+
46
+ `swp` adds no infrastructure. Cloudflare builds the Previews and Supabase makes and migrates the databases. `swp` connects the two:
47
+
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; `swp pr` points the PR's Preview at it with one secret, `SUPABASE_OVERRIDE`.
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. **`swp check` asks the Preview which database it serves** and fails the PR unless it is the right one.
52
+
53
+ New to Worker Previews or Supabase branching? [Concepts](docs/concepts.md) explains both in five minutes.
54
+
55
+ ## Quickstart
56
+
57
+ You need a Worker deployed by Workers Builds from a GitHub repo, wrangler 4.135.0 or later, Node 22 or later, and a Supabase project on a plan with branching.
58
+
59
+ ```bash
60
+ npm install --save-dev supabase-worker-previews
61
+ npx supabase-worker-previews init --project-ref <production project ref>
62
+ ```
63
+
64
+ `init` writes `swp.config.json`, a grants migration and the PR workflow, and prints the rest. Then:
65
+
66
+ 1. Connect the Supabase GitHub integration (automatic branching on, deploy to production off).
67
+ 2. Add a `previews` block to your wrangler config.
68
+ 3. `npx supabase-worker-previews shared` creates the shared Preview database and prints its `previews.vars`.
69
+ 4. Wrap the Worker:
70
+
71
+ ```ts
72
+ import { withSupabasePreviews } from "supabase-worker-previews";
73
+ import app from "./app";
74
+
75
+ export default withSupabasePreviews(app);
76
+ ```
77
+
78
+ and create the browser client from `readPublicConfig()`.
79
+
80
+ 5. Turn on Preview builds in Workers Builds, add three Actions secrets, run `npx supabase-worker-previews doctor`, and open a PR.
81
+
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
+
84
+ ## Commands
85
+
86
+ | Group | Command | Does |
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` |
96
+
97
+ `swp` is the short name once the package is installed; the full name `supabase-worker-previews` runs the same CLI. With `npx`, always use the full name: `npx swp` on a machine without this package installed downloads [an unrelated npm package named `swp`](https://www.npmjs.com/package/swp) that deletes dependency and build folders.
98
+
99
+ `swp <command> --help` shows one command's flags and examples, and `swp --version` prints the version. Flags, environment variables, every `swp.config.json` field and the GitHub Action inputs are in the [configuration reference](docs/configuration.md).
100
+
101
+ ## Safety
102
+
103
+ - `swp` never writes to, repoints or deletes the production project, and never deletes a persistent branch.
104
+ - `swp check` fails the first time a Preview serves production, with no retry.
105
+ - `swp doctor` fails if `previews.vars` names production or holds a secret.
106
+ - `swp prune` only lists until you pass `--yes`, and every command that changes something takes `--dry-run`.
107
+
108
+ [Security](docs/security.md) covers what `swp` can touch and what each token can reach.
109
+
110
+ ## Docs
111
+
112
+ | Page | For |
113
+ | ------------------------------------------ | ------------------------------------------------------------------ |
114
+ | [Concepts](docs/concepts.md) | Worker Previews, Supabase branches, and how `swp` 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, `swp.config.json`, the GitHub Action, the runtime |
120
+ | [Tokens](docs/tokens.md) | Least-privilege Cloudflare, Supabase and GitHub credentials |
121
+ | [Security](docs/security.md) | What `swp` 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
+
124
+ The full index is [docs/README.md](docs/README.md).
125
+
126
+ ## Claude Code plugin
127
+
128
+ The repo is also a Claude Code plugin. Its skill carries the [measured platform behaviour](skills/supabase-worker-previews/references/gotchas.md) behind every design choice, so Claude can set up and debug Previews with it:
129
+
130
+ ```text
131
+ /plugin marketplace add mattruby/supabase-worker-previews
132
+ /plugin install supabase-worker-previews
133
+ ```
134
+
135
+ ## Status
136
+
137
+ **Beta.** Cloudflare [launched Worker Previews on 2026-09-22](https://developers.cloudflare.com/changelog/post/2026-09-22-worker-previews/) as an open beta, and this package is 0.x. Expect both the platform and the CLI to change. Bug reports and platform findings are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md).
138
+
139
+ Not affiliated with Supabase or Cloudflare.
140
+
141
+ ## License
142
+
143
+ [MIT](LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { CloudflareApi } from "./cloudflare.js";
5
+ import { loadConfig, migrationFiles } from "./config.js";
6
+ import { check, down, shared, up } from "./commands/branches.js";
7
+ import { doctor, formatReport } from "./commands/doctor.js";
8
+ import { init } from "./commands/init.js";
9
+ import { prune } from "./commands/prune.js";
10
+ import { annotation, pr, readEvent, skipReason } from "./commands/pr.js";
11
+ import { makeRunner, sleep } from "./run.js";
12
+ import { err as errStyle, out } from "./style.js";
13
+ import { SupabaseApi } from "./supabase.js";
14
+ import { formatError, missingEnvMessage, resolveInvocation, version } from "./usage.js";
15
+ function git(args) {
16
+ try {
17
+ return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function currentBranch(flag) {
24
+ if (typeof flag === "string")
25
+ return flag;
26
+ if (process.env.WORKERS_CI_BRANCH)
27
+ return process.env.WORKERS_CI_BRANCH;
28
+ if (process.env.GITHUB_HEAD_REF)
29
+ return process.env.GITHUB_HEAD_REF;
30
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
31
+ if (!branch)
32
+ throw new Error("Cannot tell the git branch here (not a git checkout?). Pass --branch <name>.");
33
+ if (branch === "HEAD")
34
+ throw new Error("HEAD is detached, so there is no branch to use. Pass --branch <name>.");
35
+ return branch;
36
+ }
37
+ /** `owner/name` from a github.com origin remote, over https or ssh. */
38
+ function originRepo() {
39
+ const url = git(["remote", "get-url", "origin"]);
40
+ return url?.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/)?.[1];
41
+ }
42
+ function env(name) {
43
+ return process.env[name] || `<${name}>`;
44
+ }
45
+ function str(v) {
46
+ return typeof v === "string" ? v : undefined;
47
+ }
48
+ function prNumber(flag) {
49
+ if (flag === undefined)
50
+ return undefined;
51
+ const n = Number(flag);
52
+ if (!Number.isInteger(n) || n <= 0)
53
+ throw new Error(`--pr takes a pull request number, not "${String(flag)}"`);
54
+ return n;
55
+ }
56
+ async function main(argv) {
57
+ const invocation = resolveInvocation(argv, out);
58
+ if (invocation.kind === "help") {
59
+ console.log(invocation.text);
60
+ return invocation.code;
61
+ }
62
+ if (invocation.kind === "version") {
63
+ console.log(version());
64
+ return 0;
65
+ }
66
+ if (invocation.kind === "error") {
67
+ console.error(`${errStyle.red("swp:")} ${invocation.message}`);
68
+ return 2;
69
+ }
70
+ const { command, flags } = invocation;
71
+ const dryRun = flags["dry-run"] === true;
72
+ const envFile = str(flags.dotenv) ?? (existsSync(".env.swp") ? ".env.swp" : undefined);
73
+ if (envFile) {
74
+ if (!existsSync(envFile))
75
+ throw new Error(`--dotenv ${envFile}: no such file`);
76
+ process.loadEnvFile(envFile);
77
+ }
78
+ if (command === "init") {
79
+ init({
80
+ supabaseProjectRef: str(flags["project-ref"]),
81
+ trunk: str(flags.trunk),
82
+ action: flags.action === true,
83
+ dryRun,
84
+ log: (line) => console.log(line.startsWith("+ ") ? out.green(line) : line.startsWith("- ") ? out.dim(line) : line),
85
+ });
86
+ return 0;
87
+ }
88
+ const overrides = {
89
+ worker: str(flags.worker),
90
+ supabaseProjectRef: str(flags["project-ref"]),
91
+ trunk: str(flags.trunk),
92
+ prComment: flags["no-comment"] ? false : undefined,
93
+ githubDeployments: flags["no-deployments"] ? false : undefined,
94
+ };
95
+ if (command === "doctor") {
96
+ const token = process.env.SUPABASE_ACCESS_TOKEN;
97
+ const report = formatReport(await doctor(overrides, token ? new SupabaseApi(token) : undefined), out);
98
+ console.log(report.text);
99
+ return report.pass ? 0 : 1;
100
+ }
101
+ const config = loadConfig(overrides);
102
+ const prNum = prNumber(flags.pr);
103
+ const event = command === "pr" ? readEvent() : undefined;
104
+ const skip = event && skipReason(event);
105
+ if (skip) {
106
+ console.log(annotation(skip));
107
+ return skip.level === "error" ? 1 : 0;
108
+ }
109
+ const repo = command === "prune" ? (str(flags.repo) ?? process.env.GITHUB_REPOSITORY ?? originRepo()) : undefined;
110
+ if (command === "prune" && !repo)
111
+ throw new Error("Cannot tell the GitHub repo: pass --repo <owner/name>, or set GITHUB_REPOSITORY.");
112
+ const missing = missingEnvMessage(command, dryRun);
113
+ if (missing)
114
+ throw new Error(missing);
115
+ const runner = makeRunner(dryRun, (line) => console.log(line.startsWith(" $ ") ? out.dim(line) : line));
116
+ if (dryRun)
117
+ runner.log(out.dim("Dry run: reads only, changes nothing."));
118
+ const deps = {
119
+ config,
120
+ runner,
121
+ supabase: new SupabaseApi(env("SUPABASE_ACCESS_TOKEN")),
122
+ cloudflare: new CloudflareApi(runner, { apiToken: env("CLOUDFLARE_API_TOKEN"), accountId: env("CLOUDFLARE_ACCOUNT_ID") }, config.worker),
123
+ migrationCount: () => migrationFiles(config).length,
124
+ sleep,
125
+ fetchImpl: fetch,
126
+ };
127
+ switch (command) {
128
+ case "shared":
129
+ await shared(deps);
130
+ break;
131
+ case "up":
132
+ await up(currentBranch(flags.branch), deps, prNum);
133
+ break;
134
+ case "check":
135
+ await check(currentBranch(flags.branch), flags.isolated === true, deps, prNum);
136
+ break;
137
+ case "down":
138
+ await down(currentBranch(flags.branch), deps, prNum);
139
+ break;
140
+ case "pr":
141
+ await pr(event, { ...deps, githubToken: env("GITHUB_TOKEN") });
142
+ break;
143
+ case "prune":
144
+ await prune({ ...deps, githubToken: env("GITHUB_TOKEN"), repo: repo, yes: flags.yes === true });
145
+ break;
146
+ }
147
+ return 0;
148
+ }
149
+ main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
150
+ console.error(`${errStyle.red("swp:")} ${formatError(err)}`);
151
+ process.exit(1);
152
+ });
@@ -0,0 +1,37 @@
1
+ import type { Runner } from "./run.js";
2
+ export type CloudflareAuth = {
3
+ apiToken: string;
4
+ accountId: string;
5
+ };
6
+ export type PreviewRecord = {
7
+ id: string;
8
+ name: string;
9
+ slug: string;
10
+ urls: string[];
11
+ deployed_on: string | null;
12
+ };
13
+ /** Workers Builds names a Preview after the raw git branch; Cloudflare derives the slug and hostname. */
14
+ export declare function matchPreview(previews: PreviewRecord[], gitBranch: string): PreviewRecord | undefined;
15
+ /** Preview operations go through wrangler; only the subdomain lookup is a direct API call. */
16
+ export declare class CloudflareApi {
17
+ private runner;
18
+ private auth;
19
+ private worker;
20
+ private fetchImpl;
21
+ private subdomain?;
22
+ constructor(runner: Runner, auth: CloudflareAuth, worker: string, fetchImpl?: typeof fetch);
23
+ private get env();
24
+ private wrangler;
25
+ private api;
26
+ workersSubdomain(): Promise<string>;
27
+ listPreviews(): Promise<PreviewRecord[]>;
28
+ findPreview(gitBranch: string): Promise<PreviewRecord | undefined>;
29
+ /** Writing to a Preview creates a new deployment of it. */
30
+ putPreviewSecrets(preview: string, secrets: Record<string, string>): void;
31
+ listPreviewSecrets(preview: string): string[];
32
+ /** Creates a new deployment of the Preview, like any secret write. */
33
+ deletePreviewSecret(preview: string, key: string): void;
34
+ /** Merges: keys absent from `secrets` stay. Only Previews created afterwards copy it. */
35
+ putBaseSecrets(secrets: Record<string, string>): void;
36
+ deletePreview(preview: string): void;
37
+ }
@@ -0,0 +1,95 @@
1
+ import { apiErrorMessage } from "./http.js";
2
+ import { previewName } from "./preview-name.js";
3
+ /** Workers Builds names a Preview after the raw git branch; Cloudflare derives the slug and hostname. */
4
+ export function matchPreview(previews, gitBranch) {
5
+ return (previews.find((p) => p.name === gitBranch) ?? previews.find((p) => p.slug === previewName(gitBranch)));
6
+ }
7
+ /** Preview operations go through wrangler; only the subdomain lookup is a direct API call. */
8
+ export class CloudflareApi {
9
+ runner;
10
+ auth;
11
+ worker;
12
+ fetchImpl;
13
+ subdomain;
14
+ constructor(runner, auth, worker, fetchImpl = fetch) {
15
+ this.runner = runner;
16
+ this.auth = auth;
17
+ this.worker = worker;
18
+ this.fetchImpl = fetchImpl;
19
+ }
20
+ get env() {
21
+ return { CLOUDFLARE_API_TOKEN: this.auth.apiToken, CLOUDFLARE_ACCOUNT_ID: this.auth.accountId };
22
+ }
23
+ wrangler(args, opts = {}) {
24
+ return this.runner.exec("npx", ["wrangler", ...args, "--worker-name", this.worker], {
25
+ env: this.env,
26
+ ...opts,
27
+ });
28
+ }
29
+ async api(path) {
30
+ const res = await this.fetchImpl(`https://api.cloudflare.com/client/v4/accounts/${this.auth.accountId}${path}`, {
31
+ headers: { authorization: `Bearer ${this.auth.apiToken}` },
32
+ });
33
+ const text = await res.text();
34
+ const json = parseJson(text);
35
+ if (!res.ok || json?.result === undefined)
36
+ throw new Error(apiErrorMessage(`Cloudflare API GET ${path}`, res.status, text, "CLOUDFLARE_API_TOKEN"));
37
+ return json.result;
38
+ }
39
+ async workersSubdomain() {
40
+ this.subdomain ??= (await this.api("/workers/subdomain")).subdomain;
41
+ return this.subdomain;
42
+ }
43
+ async listPreviews() {
44
+ const all = [];
45
+ for (let page = 1;; page += 1) {
46
+ const batch = await this.api(`/workers/workers/${this.worker}/previews?per_page=100&page=${page}`);
47
+ all.push(...batch);
48
+ if (batch.length < 100)
49
+ return all;
50
+ }
51
+ }
52
+ async findPreview(gitBranch) {
53
+ return matchPreview(await this.listPreviews(), gitBranch);
54
+ }
55
+ /** Writing to a Preview creates a new deployment of it. */
56
+ putPreviewSecrets(preview, secrets) {
57
+ if (this.runner.dryRun) {
58
+ this.runner.log(` $ npx wrangler preview secret bulk --name ${preview} (${Object.keys(secrets).join(", ")})`);
59
+ return;
60
+ }
61
+ this.wrangler(["preview", "secret", "bulk", "--name", preview], { input: JSON.stringify(secrets) });
62
+ }
63
+ listPreviewSecrets(preview) {
64
+ const out = this.wrangler(["preview", "secret", "list", "--name", preview, "--json"], {
65
+ captureStderr: true,
66
+ });
67
+ const start = out.search(/^\[/m);
68
+ return start === -1 ? [] : JSON.parse(out.slice(start)).map((s) => s.name);
69
+ }
70
+ /** Creates a new deployment of the Preview, like any secret write. */
71
+ deletePreviewSecret(preview, key) {
72
+ this.wrangler(["preview", "secret", "delete", key, "--name", preview, "--skip-confirmation"], {
73
+ captureStderr: true,
74
+ });
75
+ }
76
+ /** Merges: keys absent from `secrets` stay. Only Previews created afterwards copy it. */
77
+ putBaseSecrets(secrets) {
78
+ if (this.runner.dryRun) {
79
+ this.runner.log(` $ npx wrangler preview base-config secret bulk (${Object.keys(secrets).join(", ")})`);
80
+ return;
81
+ }
82
+ this.wrangler(["preview", "base-config", "secret", "bulk"], { input: JSON.stringify(secrets) });
83
+ }
84
+ deletePreview(preview) {
85
+ this.wrangler(["preview", "delete", "--name", preview, "--skip-confirmation"], { captureStderr: true });
86
+ }
87
+ }
88
+ function parseJson(text) {
89
+ try {
90
+ return JSON.parse(text);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
@@ -0,0 +1,62 @@
1
+ import type { CloudflareApi } from "../cloudflare.js";
2
+ import type { Config } from "../config.js";
3
+ import type { Runner } from "../run.js";
4
+ import { type Branch, type SupabaseApi } from "../supabase.js";
5
+ import { type SupabaseValues } from "../runtime.js";
6
+ export type Deps = {
7
+ config: Config;
8
+ runner: Runner;
9
+ supabase: SupabaseApi;
10
+ cloudflare: CloudflareApi;
11
+ migrationCount: () => number;
12
+ sleep: (ms: number) => Promise<void>;
13
+ fetchImpl: typeof fetch;
14
+ now?: () => number;
15
+ };
16
+ /** The non-default Supabase branch tied to a git branch, made by the GitHub integration or by `up`. */
17
+ export declare function branchFor(gitBranch: string, deps: Deps): Promise<Branch | undefined>;
18
+ /** The Preview a git branch deploys to: named after the branch, or `pr-<number>` when configured. */
19
+ export declare function previewKey(gitBranch: string, config: Config, pr?: number): string;
20
+ /** A branch `swp` may delete: never production, the shared database, the trunk's or a persistent one. */
21
+ export declare function isDisposable(branch: Branch, config: Config): boolean;
22
+ /** Never repoint, write to or delete the production project through a branch record. */
23
+ export declare function assertIsolated(branch: Branch, parent: string): void;
24
+ export declare function supabaseValues(ref: string, deps: Deps): Promise<SupabaseValues>;
25
+ /**
26
+ * The shared Preview database: a persistent branch that tracks the trunk, so
27
+ * the GitHub integration migrates it on every trunk push. Its public values
28
+ * belong in wrangler `previews.vars`; only its secret key goes in the Preview
29
+ * base config.
30
+ */
31
+ export declare function shared(deps: Deps): Promise<string>;
32
+ /**
33
+ * Gives a git branch's Preview its own database. The GitHub integration makes
34
+ * one for any PR that changes supabase/; for other branches this asks for one,
35
+ * which Supabase still migrates and seeds from that git branch.
36
+ */
37
+ export declare function up(gitBranch: string, deps: Deps, pr?: number): Promise<string>;
38
+ /**
39
+ * Puts a Preview back on the shared database once its PR no longer needs its
40
+ * own: drops the override, then deletes the branch `up` made for it. The
41
+ * integration only makes branches for PRs that change supabase/, which never
42
+ * get here.
43
+ */
44
+ export declare function release(gitBranch: string, deps: Deps, pr?: number): Promise<void>;
45
+ /** Which database a deployed Preview serves, from the identity route or, failing that, its HTML. */
46
+ export declare function servedRef(baseUrl: string, deps: Deps): Promise<{
47
+ ref?: string;
48
+ seen: string;
49
+ }>;
50
+ /**
51
+ * Fails unless the Preview serves the database it should: its own branch
52
+ * when isolated, else the shared one. Production fails at once; anything
53
+ * else is retried while Workers Builds and `up` finish.
54
+ */
55
+ export declare function check(gitBranch: string, isolated: boolean, deps: Deps, pr?: number): Promise<void>;
56
+ /** Removes the Preview and, if the integration has not already, its own database. */
57
+ export declare function down(gitBranch: string, deps: Deps, pr?: number): Promise<void>;
58
+ /**
59
+ * Ready when the branch holds every migration in this checkout. Branch status
60
+ * runs ahead of the migrations, so the status alone is not enough.
61
+ */
62
+ export declare function waitForMigrations(ref: string, deps: Deps): Promise<void>;