@clubedge/create-clubedge-app 0.25.0 → 0.27.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/CHANGELOG.md +20 -0
- package/README.md +16 -14
- package/dist/core/deploy.js +44 -14
- package/dist/deploy-fly.js +25 -13
- package/dist/deploy.js +58 -14
- package/package.json +3 -3
- package/template/README.md +3 -2
- package/template/SETUP.md +4 -0
- package/template/apps/start/netlify.toml +3 -2
- package/template/apps/start/package.json +3 -2
- package/template/apps/start/scripts/check-env.ts +34 -0
- package/template/apps/start/src/env/production.test.ts +65 -0
- package/template/apps/start/src/env/production.ts +97 -0
- package/template/apps/start/vercel.json +1 -1
- package/template/apps/web/netlify.toml +3 -2
- package/template/apps/web/package.json +3 -2
- package/template/apps/web/scripts/check-env.ts +34 -0
- package/template/apps/web/src/env/production.test.ts +65 -0
- package/template/apps/web/src/env/production.ts +97 -0
- package/template/apps/web/vercel.json +1 -1
- package/template/clubedge.template.json +3 -0
- package/template/package.json +2 -1
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- package/template/packages/email/package.json +1 -1
- package/template/packages/email-resend/package.json +1 -1
- package/template/packages/email-smtp/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs-pg-boss/package.json +1 -1
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/observability/package.json +1 -1
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +1 -1
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template.lock.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.27.0
|
|
6
|
+
|
|
7
|
+
- Default to Starter `v0.18.0` (#77), which stops Vercel and Netlify builds when a production variable is missing:
|
|
8
|
+
- On those hosts the platform only runs the app, and the database, Redis, storage, and email come from your providers. `pnpm check:env` now runs first in the build command of `vercel.json` and `netlify.toml`, before migrations.
|
|
9
|
+
- It lists each variable the project's modules need that is unset, points at `localhost`, or isn't an absolute URL, and stops the build. That includes `.env.example`'s `example.com` sender in `EMAIL_FROM`.
|
|
10
|
+
- Generated projects check only their own modules. `SKIP_ENV_CHECK=1` builds anyway.
|
|
11
|
+
- If you deploy to Vercel or Netlify: run `pnpm clubedge upgrade`, then set the variables for every environment you build (Production and Preview).
|
|
12
|
+
|
|
13
|
+
## 0.26.0
|
|
14
|
+
|
|
15
|
+
- `deploy` uses your own databases when the env file has their URLs (#74). The platform hosts everything as before when it doesn't.
|
|
16
|
+
- With `DATABASE_URL` or `REDIS_URL` in the env file (Supabase, Neon, Upstash), `deploy` creates no Railway or Fly database for it, sets your URL, and the platform only runs the app. Each URL counts on its own, so only `DATABASE_URL` keeps Redis on the platform.
|
|
17
|
+
- `deploy` reads `.env.production` from the project's root when there is one and no `--env-file` is given. An `--env-file` that doesn't exist is now a clear error.
|
|
18
|
+
- URLs to `localhost` (a development env file) are ignored with a warning.
|
|
19
|
+
- A later run without the env file keeps the database URL the service already has, instead of creating the platform's database and switching the app over. On Railway, `deploy` stops before changing anything if it can't list an existing service's variables. On Fly, an existing `DATABASE_URL` secret skips creating `<app>-db`.
|
|
20
|
+
- When the app moves from Railway's database to its own, `deploy` notes that the old one is unused; it never deletes it.
|
|
21
|
+
- The showcase records a `try` walkthrough on a free Railway VM (#73).
|
|
22
|
+
- `pnpm release` counts squash-merged pull requests in the changelog draft (#75).
|
|
23
|
+
- Still on Starter `v0.17.1`.
|
|
24
|
+
|
|
5
25
|
## 0.25.0
|
|
6
26
|
|
|
7
27
|
- `deploy` for Fly.io (experimental) (#71): in a project with `deploy fly`, `pnpm clubedge deploy` uses Fly's CLI and your Fly login.
|
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
127
127
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
128
128
|
|
|
129
129
|
# Reproduce a specific Starter release or commit
|
|
130
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
130
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.18.0
|
|
131
131
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
132
132
|
|
|
133
133
|
# Test local Starter changes before releasing them
|
|
@@ -146,7 +146,7 @@ pnpm clubedge add email resend payments stripe # several modules as
|
|
|
146
146
|
pnpm clubedge remove storage payments
|
|
147
147
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
148
148
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
149
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
149
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.18.0 # or a specific tag, branch, or commit
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
The CLI renders the project twice, from the Starter commit and modules recorded in your `package.json` and from the new state, and merges the difference into your project. The new state has other modules for `add` and `remove`, and the newer Starter with the same modules for `upgrade`. Modules a newer Starter introduces start as `none` where possible, so an upgrade never adds a service on its own.
|
|
@@ -194,43 +194,43 @@ When the app is ready, the CLI opens the preview in your browser (`--no-open` tu
|
|
|
194
194
|
In a project created with `--deploy railway` (or switched with `pnpm clubedge add deploy railway`), one command does the first deploy:
|
|
195
195
|
|
|
196
196
|
```sh
|
|
197
|
-
pnpm clubedge deploy # asks before creating anything
|
|
198
|
-
pnpm clubedge deploy --env-file .env.
|
|
197
|
+
pnpm clubedge deploy # asks before creating anything; reads .env.production
|
|
198
|
+
pnpm clubedge deploy --env-file .env.staging # with another file of production keys
|
|
199
199
|
pnpm clubedge deploy --dry-run # show the plan, create nothing
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
With [Railway's CLI](https://docs.railway.com/cli) (the installed `railway`, or `npx @railway/cli`) and your Railway login, it creates or reuses a project named after the package, Postgres (and Redis with `cache redis`), the app's service, and a `*.up.railway.app` domain. It sets the variables it can: `DATABASE_URL` and `REDIS_URL` as references to those databases, `APP_URL` and `NEXT_PUBLIC_APP_URL`, `PORT`, and a generated `BETTER_AUTH_SECRET` that later runs keep. Values from `--env-file` come on top; the variables `.env.example` lists that nothing sets are reported, for the modules that need them (storage, email, payments). The service's settings (the migrations before each release, the `/api/health` check) go in `.railway/railway.ts`, Railway's configuration as code, which replaces `railway.json` (Railway stops reading it on 2026-12-01): `deploy` writes it when the project has none, adds Railway's TypeScript SDK (`railway`) as a dev dependency, and applies it with `railway config apply`; commit them. Then `railway up` builds the Dockerfile, and `deploy` waits for Railway's final status before printing the URL. It needs Railway CLI 5.42.1 or newer: an installed `railway`, else one npx already downloaded, else it downloads one. Running `deploy` again reuses everything, so it also adds variables and ships a new release. Railway bills usage on your plan.
|
|
202
|
+
With [Railway's CLI](https://docs.railway.com/cli) (the installed `railway`, or `npx @railway/cli`) and your Railway login, it creates or reuses a project named after the package, Postgres (and Redis with `cache redis`), the app's service, and a `*.up.railway.app` domain. It sets the variables it can: `DATABASE_URL` and `REDIS_URL` as references to those databases, `APP_URL` and `NEXT_PUBLIC_APP_URL`, `PORT`, and a generated `BETTER_AUTH_SECRET` that later runs keep. Values from the env file (`.env.production` when there is one, or `--env-file`) come on top. With a `DATABASE_URL` or `REDIS_URL` in it (Supabase, Neon, Upstash), `deploy` creates no Railway database for it and the app uses yours; URLs to `localhost` are ignored, and a later run without the file keeps the URL the service has. The variables `.env.example` lists that nothing sets are reported, for the modules that need them (storage, email, payments). The service's settings (the migrations before each release, the `/api/health` check) go in `.railway/railway.ts`, Railway's configuration as code, which replaces `railway.json` (Railway stops reading it on 2026-12-01): `deploy` writes it when the project has none, adds Railway's TypeScript SDK (`railway`) as a dev dependency, and applies it with `railway config apply`; commit them. Then `railway up` builds the Dockerfile, and `deploy` waits for Railway's final status before printing the URL. It needs Railway CLI 5.42.1 or newer: an installed `railway`, else one npx already downloaded, else it downloads one. Running `deploy` again reuses everything, so it also adds variables and ships a new release. Railway bills usage on your plan.
|
|
203
203
|
|
|
204
204
|
### Deploy to Fly.io (experimental)
|
|
205
205
|
|
|
206
|
-
In a project with `--deploy fly`, `pnpm clubedge deploy` does the same with [Fly's CLI](https://fly.io/docs/flyctl/install/) (install it first; it is not on npm) and your Fly login: it creates the app (`fly launch`, which writes its name and region into `fly.toml`; commit it), an unmanaged Postgres app attached as `DATABASE_URL` (one `shared-cpu-1x` machine and a 1 GB volume)
|
|
206
|
+
In a project with `--deploy fly`, `pnpm clubedge deploy` does the same with [Fly's CLI](https://fly.io/docs/flyctl/install/) (install it first; it is not on npm) and your Fly login: it creates the app (`fly launch`, which writes its name and region into `fly.toml`; commit it), an unmanaged Postgres app attached as `DATABASE_URL` (one `shared-cpu-1x` machine and a 1 GB volume) and Upstash Redis with `cache redis`, each unless the env file has its URL, and imports the secrets (the public URL `https://<app>.fly.dev`, a kept `BETTER_AUTH_SECRET`, and `--env-file` values). Then `fly deploy` builds the Dockerfile with `NEXT_PUBLIC_APP_URL` as a build argument, runs the migrations as the release command, and waits for the health checks. Fly bills these on your plan; for Fly's Managed Postgres, create and attach it yourself before deploying. Running it again reuses everything.
|
|
207
207
|
|
|
208
208
|
## Version relationship
|
|
209
209
|
|
|
210
210
|
Each CLI release is tested against one specific Starter release:
|
|
211
211
|
|
|
212
212
|
```text
|
|
213
|
-
create-clubedge-app v0.
|
|
213
|
+
create-clubedge-app v0.27.0
|
|
214
214
|
|
|
|
215
215
|
v
|
|
216
|
-
Clubedge Starter v0.
|
|
217
|
-
commit
|
|
216
|
+
Clubedge Starter v0.18.0
|
|
217
|
+
commit bcd084d7d2d2aac0600ccb35491f48c251fb3a74
|
|
218
218
|
```
|
|
219
219
|
|
|
220
220
|
The current relationship is:
|
|
221
221
|
|
|
222
222
|
| CLI release | Default Starter release | Meaning |
|
|
223
223
|
| --- | --- | --- |
|
|
224
|
-
| `create-clubedge-app@0.
|
|
224
|
+
| `create-clubedge-app@0.27.0` | `clubedge-starter@v0.18.0` | The CLI's required, tested default |
|
|
225
225
|
|
|
226
226
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
227
227
|
|
|
228
|
-
The `v0.
|
|
229
|
-
`
|
|
228
|
+
The `v0.18.0` tag is expected to resolve to commit
|
|
229
|
+
`bcd084d7d2d2aac0600ccb35491f48c251fb3a74`. The CLI verifies this before
|
|
230
230
|
scaffolding with the default reference and fails rather than silently using a
|
|
231
231
|
moved tag.
|
|
232
232
|
|
|
233
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
233
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.18.0`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
|
|
234
234
|
|
|
235
235
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
236
236
|
|
|
@@ -285,7 +285,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
285
285
|
1. Make and validate changes in `clubedge-starter`.
|
|
286
286
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
287
287
|
3. Update the Starter package versions.
|
|
288
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
288
|
+
4. Commit `main`, create a release tag such as `v0.18.0`, and publish the Starter release.
|
|
289
289
|
|
|
290
290
|
### CLI maintainers
|
|
291
291
|
|
|
@@ -320,6 +320,8 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
320
320
|
| 0.23.0 | v0.16.1 | `deploy` moves Railway projects to `.railway/railway.ts` before Railway stops reading `railway.json` |
|
|
321
321
|
| 0.24.0 | v0.17.0 | Starter v0.17.0: Railway configuration as code (`.railway/railway.ts`) instead of `railway.json` |
|
|
322
322
|
| 0.25.0 | v0.17.1 | `deploy` to Fly.io (experimental); weekly Dependabot updates |
|
|
323
|
+
| 0.26.0 | v0.17.1 | `deploy` uses your own databases (Supabase, Neon, Upstash) when the env file has their URLs, and reads `.env.production` |
|
|
324
|
+
| 0.27.0 | v0.18.0 | Starter v0.18.0: Vercel and Netlify builds stop when a production variable is missing (`pnpm check:env`) |
|
|
323
325
|
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
324
326
|
|
|
325
327
|
## Community
|
package/dist/core/deploy.js
CHANGED
|
@@ -9,9 +9,40 @@ export function serviceName(packageName) {
|
|
|
9
9
|
const name = packageName.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10
10
|
return name || "web";
|
|
11
11
|
}
|
|
12
|
-
/** The
|
|
13
|
-
export
|
|
14
|
-
|
|
12
|
+
/** The variable each database's URL goes in. */
|
|
13
|
+
export const databaseVariables = { postgres: "DATABASE_URL", redis: "REDIS_URL" };
|
|
14
|
+
/** A URL to this machine: from a development env file, unreachable from the deployed app. */
|
|
15
|
+
export function isLocalUrl(value) {
|
|
16
|
+
const host = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]*@)?(\[[^\]]*\]|[^:/?#]*)/i.exec(value.trim())?.[1]?.toLowerCase() ?? "";
|
|
17
|
+
return ["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "host.docker.internal"].includes(host) || host.endsWith(".localhost");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The database URLs an env file provides, for databases hosted elsewhere (Supabase, Neon, Upstash):
|
|
21
|
+
* deploy uses them instead of creating the platform's own. Only the databases the project uses
|
|
22
|
+
* count, and not empty or local URLs.
|
|
23
|
+
*/
|
|
24
|
+
export function externalDatabaseUrls(modules, envFile) {
|
|
25
|
+
const urls = {};
|
|
26
|
+
for (const database of databasesFor(modules)) {
|
|
27
|
+
const key = databaseVariables[database];
|
|
28
|
+
const value = envFile[key];
|
|
29
|
+
if (value && !isLocalUrl(value))
|
|
30
|
+
urls[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return urls;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The databases a project needs from the platform: Postgres always (Drizzle), Redis with the redis
|
|
36
|
+
* cache, except the ones whose URL comes from elsewhere (`external`: variable names).
|
|
37
|
+
*/
|
|
38
|
+
export function databasesFor(modules, external = []) {
|
|
39
|
+
const skip = new Set(external);
|
|
40
|
+
const needed = modules.cache === "redis" ? ["postgres", "redis"] : ["postgres"];
|
|
41
|
+
return needed.filter((database) => !skip.has(databaseVariables[database]));
|
|
42
|
+
}
|
|
43
|
+
/** References to Railway's databases, for the service's variables. */
|
|
44
|
+
export function railwayReferences(databases) {
|
|
45
|
+
return Object.fromEntries(databases.map((database) => [databaseVariables[database], `\${{${railwayDatabases[database]}.${databaseVariables[database]}}}`]));
|
|
15
46
|
}
|
|
16
47
|
/** The variable names a project's .env.example lists, in order. */
|
|
17
48
|
export function exampleKeys(example) {
|
|
@@ -36,30 +67,29 @@ export function parseEnvFile(text) {
|
|
|
36
67
|
return values;
|
|
37
68
|
}
|
|
38
69
|
/**
|
|
39
|
-
* The service's production variables. The CLI provides what it can:
|
|
40
|
-
* databases, a generated Better Auth secret (unless the service already
|
|
41
|
-
* and the port. Values from --env-file come next and win over those
|
|
42
|
-
*
|
|
43
|
-
* reported as unset; values the service already has count as set.
|
|
70
|
+
* The service's production variables. The CLI provides what it can: the database URLs (references
|
|
71
|
+
* to Railway's databases by default), a generated Better Auth secret (unless the service already
|
|
72
|
+
* has one), the public URL, and the port. Values from --env-file come next and win over those;
|
|
73
|
+
* their database URLs replace the platform's, except local ones (a development env file).
|
|
74
|
+
* Everything else the example lists is reported as unset; values the service already has count as set.
|
|
44
75
|
*/
|
|
45
76
|
export function planVariables(input) {
|
|
46
77
|
const { modules, keys, url, envFile, secret, attached = [] } = input;
|
|
47
78
|
const existing = { ...input.existing, ...Object.fromEntries(attached.map((key) => [key, "(attached)"])) };
|
|
48
|
-
const
|
|
49
|
-
const databases = input.databases ??
|
|
50
|
-
DATABASE_URL: reference(railwayDatabases.postgres, "DATABASE_URL"),
|
|
51
|
-
...(modules.cache === "redis" ? { REDIS_URL: reference(railwayDatabases.redis, "REDIS_URL") } : {}),
|
|
52
|
-
};
|
|
79
|
+
const external = externalDatabaseUrls(modules, envFile);
|
|
80
|
+
const databases = input.databases ?? railwayReferences(databasesFor(modules, Object.keys(external)));
|
|
53
81
|
const provided = { NODE_ENV: "production", PORT: String(appPort), ...databases };
|
|
54
82
|
// The public URL under both names: TanStack Start reads APP_URL, Next.js NEXT_PUBLIC_APP_URL.
|
|
55
83
|
const set = { ...provided, APP_URL: url, NEXT_PUBLIC_APP_URL: url };
|
|
56
84
|
if (modules.auth === "better-auth" && !existing.BETTER_AUTH_SECRET)
|
|
57
85
|
set.BETTER_AUTH_SECRET = secret();
|
|
86
|
+
const databaseKeys = Object.values(databaseVariables);
|
|
58
87
|
for (const [key, value] of Object.entries(envFile)) {
|
|
59
|
-
if (
|
|
88
|
+
if (value === "" || databaseKeys.includes(key))
|
|
60
89
|
continue;
|
|
61
90
|
set[key] = value;
|
|
62
91
|
}
|
|
92
|
+
Object.assign(set, external);
|
|
63
93
|
const unset = keys.filter((key) => set[key] === undefined && !existing[key]);
|
|
64
94
|
return { set, unset };
|
|
65
95
|
}
|
package/dist/deploy-fly.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// `deploy` for Fly.io (experimental): creates or reuses the app (written to fly.toml), an
|
|
2
|
-
// unmanaged Postgres attached as DATABASE_URL, Upstash Redis with cache redis
|
|
3
|
-
// and runs `fly deploy`, which waits for the release
|
|
2
|
+
// unmanaged Postgres attached as DATABASE_URL, Upstash Redis with cache redis (each unless the env
|
|
3
|
+
// file points it elsewhere), sets the secrets, and runs `fly deploy`, which waits for the release
|
|
4
|
+
// command (migrations) and the health checks.
|
|
4
5
|
import { cancel, confirm, isCancel, log, note, outro, spinner } from "@clack/prompts";
|
|
5
6
|
import { randomBytes } from "node:crypto";
|
|
6
7
|
import { readFile } from "node:fs/promises";
|
|
7
|
-
import { join
|
|
8
|
+
import { join } from "node:path";
|
|
8
9
|
import process from "node:process";
|
|
9
10
|
import { UsageError } from "./cli/args.js";
|
|
10
11
|
import { bold, dim } from "./cli/ui.js";
|
|
11
|
-
import { exampleKeys, flyAppUrl, flySecretNames, flySecretsInput, flyTomlValue,
|
|
12
|
+
import { exampleKeys, externalDatabaseUrls, flyAppUrl, flySecretNames, flySecretsInput, flyTomlValue, parseRedisUrl, planVariables, serviceName, } from "./core/deploy.js";
|
|
12
13
|
import { spawnCommand } from "./utils/process.js";
|
|
13
14
|
const flyInstallUrl = "https://fly.io/docs/flyctl/install/";
|
|
14
15
|
const flyPricingUrl = "https://fly.io/docs/about/pricing/";
|
|
@@ -42,7 +43,7 @@ async function flyCli(cwd) {
|
|
|
42
43
|
}
|
|
43
44
|
throw new UsageError(`deploy to Fly.io needs Fly's CLI. Install it (${flyInstallUrl}), then run deploy again.`);
|
|
44
45
|
}
|
|
45
|
-
export async function deployToFly(args, project, cli) {
|
|
46
|
+
export async function deployToFly(args, project, cli, envFile) {
|
|
46
47
|
const root = project.root;
|
|
47
48
|
const tomlPath = join(root, "fly.toml");
|
|
48
49
|
const toml = await readFile(tomlPath, "utf8").catch(() => "");
|
|
@@ -51,16 +52,25 @@ export async function deployToFly(args, project, cli) {
|
|
|
51
52
|
const named = flyTomlValue(toml, "app");
|
|
52
53
|
// Fly app names are global: a new app gets a short random suffix.
|
|
53
54
|
const app = named ?? `${serviceName(project.identity.packageName)}-${randomBytes(2).toString("hex")}`;
|
|
54
|
-
const redis = project.modules.cache === "redis";
|
|
55
55
|
const keys = exampleKeys(await readFile(join(root, ".env.example"), "utf8").catch(() => ""));
|
|
56
|
-
const
|
|
56
|
+
const external = externalDatabaseUrls(project.modules, envFile);
|
|
57
|
+
// The databases to create on Fly: the ones the env file doesn't point elsewhere.
|
|
58
|
+
const postgres = !external.DATABASE_URL;
|
|
59
|
+
const redis = project.modules.cache === "redis" && !external.REDIS_URL;
|
|
60
|
+
const elsewhere = Object.keys(external);
|
|
61
|
+
const hostedLine = [
|
|
62
|
+
...(postgres ? [`a Postgres app "${app}-db" (unmanaged: one shared-cpu-1x machine and a 1 GB volume)`] : []),
|
|
63
|
+
...(redis ? [`Upstash Redis "${app}-redis"`] : []),
|
|
64
|
+
];
|
|
57
65
|
const secret = () => randomBytes(32).toString("hex");
|
|
58
66
|
const url = flyAppUrl(app);
|
|
59
67
|
if (args.dryRun) {
|
|
60
|
-
const preview = planVariables({ modules: project.modules, keys, url, existing: {}, envFile, secret: () => "<generated>", databases: {}, attached: ["DATABASE_URL", ...(redis ? ["REDIS_URL"] : [])] });
|
|
68
|
+
const preview = planVariables({ modules: project.modules, keys, url, existing: {}, envFile, secret: () => "<generated>", databases: {}, attached: [...(postgres ? ["DATABASE_URL"] : []), ...(redis ? ["REDIS_URL"] : [])] });
|
|
61
69
|
log.message([
|
|
62
70
|
`• Fly app "${app}"${named ? " (from fly.toml)" : ", written to fly.toml"}, at ${url}`,
|
|
63
|
-
`• Postgres "${app}-db" (unmanaged, one small machine), attached as DATABASE_URL
|
|
71
|
+
...(postgres ? [`• Postgres "${app}-db" (unmanaged, one small machine), attached as DATABASE_URL`] : []),
|
|
72
|
+
...(redis ? [`• Upstash Redis "${app}-redis" as REDIS_URL`] : []),
|
|
73
|
+
...(elsewhere.length ? [`• ${elsewhere.join(" and ")} from the env file, instead of Fly's databases`] : []),
|
|
64
74
|
`• Secrets: ${Object.keys(preview.set).join(", ")}`,
|
|
65
75
|
...(preview.unset.length ? [`• Not set: ${preview.unset.join(", ")}`] : []),
|
|
66
76
|
`• fly deploy, with NEXT_PUBLIC_APP_URL as a build argument: migrations, then health checks`,
|
|
@@ -71,7 +81,8 @@ export async function deployToFly(args, project, cli) {
|
|
|
71
81
|
note([
|
|
72
82
|
"This deploys the project to Fly.io, in your Fly account (experimental):",
|
|
73
83
|
"",
|
|
74
|
-
`• The app "${app}"
|
|
84
|
+
`• The app "${app}"${hostedLine.length ? `, ${hostedLine.join(", and ")}` : ""}.`,
|
|
85
|
+
...(elsewhere.length ? [`• ${elsewhere.join(" and ")} from the env file: those databases stay where they are, and Fly runs the app.`] : []),
|
|
75
86
|
"• The secrets Fly can't provide come from --env-file; the ones still unset are listed before the build.",
|
|
76
87
|
"• fly deploy builds the Dockerfile, runs the migrations as the release command, and waits for the health checks.",
|
|
77
88
|
"",
|
|
@@ -105,12 +116,13 @@ export async function deployToFly(args, project, cli) {
|
|
|
105
116
|
const region = flyTomlValue(await readFile(tomlPath, "utf8"), "primary_region");
|
|
106
117
|
const regionArgs = region ? ["--region", region] : [];
|
|
107
118
|
const secrets = new Set(flySecretNames((await attempt(() => fly(flyBin, ["secrets", "list", "--app", app, "--json"], root))) ?? ""));
|
|
119
|
+
// A DATABASE_URL secret already set (Fly's attach, or an earlier env file) is kept as it is.
|
|
108
120
|
const database = `${app}-db`;
|
|
109
|
-
if (!(await attempt(() => fly(flyBin, ["status", "--app", database], root)))) {
|
|
121
|
+
if (postgres && !secrets.has("DATABASE_URL") && !(await attempt(() => fly(flyBin, ["status", "--app", database], root)))) {
|
|
110
122
|
progress.message(`Creating Postgres "${database}"`);
|
|
111
123
|
await fly(flyBin, ["postgres", "create", "--name", database, "--org", "personal", ...regionArgs, "--initial-cluster-size", "1", "--vm-size", "shared-cpu-1x", "--volume-size", "1", "--autostart"], root);
|
|
112
124
|
}
|
|
113
|
-
if (!secrets.has("DATABASE_URL")) {
|
|
125
|
+
if (postgres && !secrets.has("DATABASE_URL")) {
|
|
114
126
|
progress.message("Attaching Postgres (DATABASE_URL)");
|
|
115
127
|
await fly(flyBin, ["postgres", "attach", database, "--app", app, "--yes"], root);
|
|
116
128
|
}
|
|
@@ -128,7 +140,7 @@ export async function deployToFly(args, project, cli) {
|
|
|
128
140
|
envFile,
|
|
129
141
|
secret,
|
|
130
142
|
databases: redisUrl ? { REDIS_URL: redisUrl } : {},
|
|
131
|
-
attached: ["DATABASE_URL", ...(redis && !redisUrl && secrets.has("REDIS_URL") ? ["REDIS_URL"] : [])],
|
|
143
|
+
attached: [...(postgres ? ["DATABASE_URL"] : []), ...(redis && !redisUrl && secrets.has("REDIS_URL") ? ["REDIS_URL"] : [])],
|
|
132
144
|
});
|
|
133
145
|
// The image sets NODE_ENV and fly.toml the port: the rest are secrets, staged for the deploy.
|
|
134
146
|
const { NODE_ENV: _node, PORT: _port, ...values } = plan.set;
|
package/dist/deploy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `deploy`: a guided first deploy to Railway, with Railway's own CLI. It creates (or reuses) the
|
|
2
|
-
// project,
|
|
3
|
-
// `railway up`; Railway then builds the image, runs the
|
|
2
|
+
// project, the databases the env file doesn't point elsewhere, the app's service and domain, sets
|
|
3
|
+
// the production variables, and runs `railway up`; Railway then builds the image, runs the
|
|
4
|
+
// migrations, and checks /api/health.
|
|
4
5
|
import { cancel, confirm, isCancel, log, note, outro, spinner } from "@clack/prompts";
|
|
5
6
|
import { spawn } from "node:child_process";
|
|
6
7
|
import { randomBytes } from "node:crypto";
|
|
@@ -10,7 +11,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
10
11
|
import process from "node:process";
|
|
11
12
|
import { UsageError } from "./cli/args.js";
|
|
12
13
|
import { bold, dim } from "./cli/ui.js";
|
|
13
|
-
import { appPort, databasesFor, deploymentStatus, exampleKeys, finalStatuses, meetsMinimumCli, minimumRailwayCli, parseDeploymentId, parseEnvFile, parseRailwayDomain, parseVariables, planVariables, railwayCliVersion, railwayDatabases, railwayIacPath, railwayIacSource, serviceName, serviceNames, } from "./core/deploy.js";
|
|
14
|
+
import { appPort, databaseVariables, databasesFor, deploymentStatus, exampleKeys, externalDatabaseUrls, finalStatuses, isLocalUrl, meetsMinimumCli, minimumRailwayCli, parseDeploymentId, parseEnvFile, parseRailwayDomain, parseVariables, planVariables, railwayCliVersion, railwayDatabases, railwayIacPath, railwayIacSource, railwayReferences, serviceName, serviceNames, } from "./core/deploy.js";
|
|
14
15
|
import { readProject } from "./core/project.js";
|
|
15
16
|
import { deployToFly } from "./deploy-fly.js";
|
|
16
17
|
import { cliCommand } from "./modify.js";
|
|
@@ -19,6 +20,28 @@ import { pathExists } from "./utils/fs.js";
|
|
|
19
20
|
import { run, spawnCommand } from "./utils/process.js";
|
|
20
21
|
const railwayTermsUrl = "https://railway.com/legal/terms";
|
|
21
22
|
const deployDocsUrl = "https://starter.clubedge.live/docs/starter/deploy";
|
|
23
|
+
/**
|
|
24
|
+
* The production env file: --env-file, else the project's .env.production when there is one. Its
|
|
25
|
+
* values go on top of what the platform provides; its database URLs replace the platform's databases.
|
|
26
|
+
*/
|
|
27
|
+
async function readDeployEnv(args, project) {
|
|
28
|
+
const path = args.envFile ? resolve(process.cwd(), args.envFile) : join(project.root, ".env.production");
|
|
29
|
+
if (!(await pathExists(path))) {
|
|
30
|
+
if (args.envFile)
|
|
31
|
+
throw new UsageError(`The env file ${args.envFile} does not exist.`);
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
if (!args.envFile)
|
|
35
|
+
log.info("Using .env.production (pass --env-file for another file).");
|
|
36
|
+
const values = parseEnvFile(await readFile(path, "utf8"));
|
|
37
|
+
const local = databasesFor(project.modules)
|
|
38
|
+
.map((database) => databaseVariables[database])
|
|
39
|
+
.filter((key) => values[key] && isLocalUrl(values[key]));
|
|
40
|
+
if (local.length) {
|
|
41
|
+
log.warn(`${local.join(" and ")} in the env file point at this machine, which the deployed app can't reach: ignored, so deploy creates the platform's own.`);
|
|
42
|
+
}
|
|
43
|
+
return values;
|
|
44
|
+
}
|
|
22
45
|
const windows = process.platform === "win32";
|
|
23
46
|
const binaryName = windows ? "railway.exe" : "railway";
|
|
24
47
|
async function cliVersion(command, shell) {
|
|
@@ -111,12 +134,14 @@ async function railway(cli, args, options = {}) {
|
|
|
111
134
|
});
|
|
112
135
|
}
|
|
113
136
|
const attempt = async (run) => run().catch(() => null);
|
|
114
|
-
async function agree(args, service, databases) {
|
|
137
|
+
async function agree(args, service, databases, external) {
|
|
138
|
+
const hosted = databases.map((db) => railwayDatabases[db]).join(" and ");
|
|
115
139
|
note([
|
|
116
140
|
"This deploys the project to Railway, in your Railway account:",
|
|
117
141
|
"",
|
|
118
|
-
`• A project with ${
|
|
119
|
-
"
|
|
142
|
+
`• A project${hosted ? ` with ${hosted}` : ""}, and a service "${service}" with a public *.up.railway.app domain.`,
|
|
143
|
+
...(external.length ? [`• ${external.join(" and ")} from the env file: those databases stay where they are, and Railway runs the app.`] : []),
|
|
144
|
+
"• The production variables Railway can provide; your provider keys come from the env file.",
|
|
120
145
|
"• Railway builds the Dockerfile, runs the migrations before the release, and checks /api/health; the first build takes several minutes.",
|
|
121
146
|
`• The service's settings go in ${railwayIacPath}, which replaces railway.json; the railway package is added to read it.`,
|
|
122
147
|
"",
|
|
@@ -134,19 +159,21 @@ export async function deployProject(args) {
|
|
|
134
159
|
const cli = await cliCommand(project);
|
|
135
160
|
const platform = project.modules.deploy ?? "docker";
|
|
136
161
|
if (platform === "fly")
|
|
137
|
-
return await deployToFly(args, project, cli);
|
|
162
|
+
return await deployToFly(args, project, cli, await readDeployEnv(args, project));
|
|
138
163
|
if (platform !== "railway") {
|
|
139
164
|
throw new UsageError(`deploy sets up Railway and Fly.io for now, and this project deploys with "${platform}". Switch with \`${cli} add deploy railway\` (or fly), or follow ${deployDocsUrl} for ${platform}.`);
|
|
140
165
|
}
|
|
141
166
|
const service = serviceName(project.identity.packageName);
|
|
142
|
-
const databases = databasesFor(project.modules);
|
|
143
167
|
const keys = exampleKeys(await readFile(join(project.root, ".env.example"), "utf8").catch(() => ""));
|
|
144
|
-
const envFile =
|
|
168
|
+
const envFile = await readDeployEnv(args, project);
|
|
169
|
+
const external = Object.keys(externalDatabaseUrls(project.modules, envFile));
|
|
170
|
+
const databases = databasesFor(project.modules, external);
|
|
145
171
|
const secret = () => randomBytes(32).toString("hex");
|
|
146
172
|
if (args.dryRun) {
|
|
147
173
|
const preview = planVariables({ modules: project.modules, keys, url: "https://<service>.up.railway.app", existing: {}, envFile, secret: () => "<generated>" });
|
|
148
174
|
log.message([
|
|
149
|
-
`• Railway project "${service}" with ${databases.join(" and ")}, and the service "${service}"`,
|
|
175
|
+
`• Railway project "${service}"${databases.length ? ` with ${databases.map((db) => railwayDatabases[db]).join(" and ")}` : ""}, and the service "${service}"`,
|
|
176
|
+
...(external.length ? [`• ${external.join(" and ")} from the env file, instead of Railway's databases`] : []),
|
|
150
177
|
`• A *.up.railway.app domain for port ${appPort}`,
|
|
151
178
|
`• ${railwayIacPath} (written if missing, with Railway's SDK added), applied with railway config apply`,
|
|
152
179
|
`• Variables: ${Object.keys(preview.set).join(", ")}`,
|
|
@@ -156,7 +183,7 @@ export async function deployProject(args) {
|
|
|
156
183
|
outro("Dry run: nothing was created. Run again without --dry-run to deploy.");
|
|
157
184
|
return;
|
|
158
185
|
}
|
|
159
|
-
if (!(await agree(args, service, databases))) {
|
|
186
|
+
if (!(await agree(args, service, databases, external))) {
|
|
160
187
|
cancel("Nothing was deployed.");
|
|
161
188
|
return;
|
|
162
189
|
}
|
|
@@ -185,7 +212,19 @@ export async function deployProject(args) {
|
|
|
185
212
|
catch {
|
|
186
213
|
// Unknown status format: create what is missing, as for a new project.
|
|
187
214
|
}
|
|
188
|
-
|
|
215
|
+
// A database URL the service already has without that database in the project points elsewhere
|
|
216
|
+
// (an earlier --env-file): keep it. A new service has nothing to list yet; an existing one must
|
|
217
|
+
// list its variables, or deploy could create Railway's database and switch the app over.
|
|
218
|
+
const variables = await attempt(() => railway(rw, ["variable", "list", "--service", service, "--json"]));
|
|
219
|
+
if (variables === null && existing.includes(service)) {
|
|
220
|
+
throw new Error(`Railway didn't list the variables of the service "${service}", so deploy changed nothing. Run deploy again; if it keeps failing, check \`railway variable list --service ${service}\`.`);
|
|
221
|
+
}
|
|
222
|
+
const current = parseVariables(variables ?? "");
|
|
223
|
+
const kept = databasesFor(project.modules, external)
|
|
224
|
+
.filter((database) => current[databaseVariables[database]] && !existing.includes(railwayDatabases[database]))
|
|
225
|
+
.map((database) => databaseVariables[database]);
|
|
226
|
+
const hosted = databasesFor(project.modules, [...external, ...kept]);
|
|
227
|
+
for (const database of hosted) {
|
|
189
228
|
if (existing.includes(railwayDatabases[database]))
|
|
190
229
|
continue;
|
|
191
230
|
progress.message(`Adding ${railwayDatabases[database]}`);
|
|
@@ -203,8 +242,7 @@ export async function deployProject(args) {
|
|
|
203
242
|
throw new Error("Railway created no *.up.railway.app domain. Create one in the service's settings, then run deploy again.");
|
|
204
243
|
const url = `https://${domain}`;
|
|
205
244
|
progress.message("Setting the production variables");
|
|
206
|
-
const
|
|
207
|
-
const plan = planVariables({ modules: project.modules, keys, url, existing: current, envFile, secret });
|
|
245
|
+
const plan = planVariables({ modules: project.modules, keys, url, existing: current, envFile, secret, databases: railwayReferences(hosted) });
|
|
208
246
|
for (const [key, value] of Object.entries(plan.set)) {
|
|
209
247
|
if (current[key] === value)
|
|
210
248
|
continue;
|
|
@@ -239,6 +277,12 @@ export async function deployProject(args) {
|
|
|
239
277
|
progress.message(`Applying ${railwayIacPath}`);
|
|
240
278
|
await railway(rw, ["config", "apply", "--yes"]);
|
|
241
279
|
progress.stop(`Railway is ready: ${url}`);
|
|
280
|
+
const unused = databasesFor(project.modules)
|
|
281
|
+
.filter((database) => !hosted.includes(database) && existing.includes(railwayDatabases[database]))
|
|
282
|
+
.map((database) => railwayDatabases[database]);
|
|
283
|
+
if (unused.length) {
|
|
284
|
+
log.info(`The app no longer uses Railway's ${unused.join(" and ")}: its URL comes from elsewhere now. Delete it in the dashboard (railway open) if you don't need its data.`);
|
|
285
|
+
}
|
|
242
286
|
if (changed.length) {
|
|
243
287
|
log.info(`Commit ${changed.join(", ")}: Railway reads the service's settings from ${railwayIacPath} now. railway.json is no longer needed (Railway stops reading it on 2026-12-01).`);
|
|
244
288
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Create a Next.js or TanStack Start application from the Clubedge Starter reference implementation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.9.0",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"clubedge": {
|
|
24
24
|
"starterRepository": "Clubedge/clubedge-starter",
|
|
25
|
-
"starterRef": "v0.
|
|
26
|
-
"starterCommit": "
|
|
25
|
+
"starterRef": "v0.18.0",
|
|
26
|
+
"starterCommit": "bcd084d7d2d2aac0600ccb35491f48c251fb3a74"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/README.md
CHANGED
|
@@ -76,8 +76,8 @@ Every combination in the CI matrix is generated as a real project and runs its f
|
|
|
76
76
|
- Deployment to Fly.io with `fly.toml`: migrations run as the release command. <!-- clubedge:only deploy=fly -->
|
|
77
77
|
- Deployment to Railway with `.railway/railway.ts`: migrations run before each release, and `pnpm clubedge deploy` sets up the project. <!-- clubedge:only deploy=railway -->
|
|
78
78
|
- Deployment to Render with a `render.yaml` Blueprint: migrations run before each deploy. <!-- clubedge:only deploy=render -->
|
|
79
|
-
- Deployment to Vercel with `apps/web/vercel.json`:
|
|
80
|
-
- Deployment to Netlify with `apps/web/netlify.toml`:
|
|
79
|
+
- Deployment to Vercel with `apps/web/vercel.json`: the build stops when a production variable is missing, then runs migrations. <!-- clubedge:only deploy=vercel -->
|
|
80
|
+
- Deployment to Netlify with `apps/web/netlify.toml`: the build stops when a production variable is missing, then runs migrations. <!-- clubedge:only deploy=netlify -->
|
|
81
81
|
|
|
82
82
|
**Quality**
|
|
83
83
|
|
|
@@ -270,6 +270,7 @@ Playwright's first run may require installing Chromium with `pnpm exec playwrigh
|
|
|
270
270
|
Local services:
|
|
271
271
|
|
|
272
272
|
- `pnpm check:services`: check every configured service with a real round trip.
|
|
273
|
+
- `pnpm check:env`: check that the environment has every production variable the project's modules need. Vercel and Netlify builds run it first. <!-- clubedge:only deploy=vercel|netlify -->
|
|
273
274
|
- `pnpm infra:up` and `pnpm infra:down`: start and stop the local services from `docker-compose.dev.yml`. <!-- clubedge:only infra=docker -->
|
|
274
275
|
- `pnpm infra:reset`: stop them and delete their data. `pnpm infra:logs` follows their logs. <!-- clubedge:only infra=docker -->
|
|
275
276
|
- `pnpm infra:up` and `pnpm infra:down`: start and stop the local Supabase stack. <!-- clubedge:only infra=supabase -->
|
package/template/SETUP.md
CHANGED
|
@@ -408,6 +408,8 @@ Render runs `node /app/migrate.mjs` as the pre-deploy command before each deploy
|
|
|
408
408
|
`apps/web/vercel.json` deploys the app to [Vercel](https://vercel.com). Import the repository, set the project's Root Directory to `apps/web`, add the production variables listed below, and deploy. Vercel installs the whole pnpm workspace and builds the app. <!-- clubedge:only framework=next -->
|
|
409
409
|
`apps/web/vercel.json` deploys the app to [Vercel](https://vercel.com). Import the repository, set the project's Root Directory to `apps/web`, add the production variables listed below, and deploy. The build detects Vercel and writes Vercel's Build Output, with server routes as functions. <!-- clubedge:only framework=tanstack-start -->
|
|
410
410
|
|
|
411
|
+
Vercel only runs the app: the database, Redis, file storage, and email come from your providers (for example Supabase, Upstash, and Cloudflare R2). The build command starts with `pnpm check:env`, which stops the build when a variable the project's modules need is unset, or points at `localhost`, and lists each one. Set them for every environment you deploy (Production and Preview); `SKIP_ENV_CHECK=1` builds anyway.
|
|
412
|
+
|
|
411
413
|
Vercel has no step that runs before a release, so the build command applies migrations (`pnpm db:migrate`) against the `DATABASE_URL` of the deployment's environment. Give Preview deployments their own database, or remove the migration step from `buildCommand` and run migrations yourself. Serverless functions open a connection per instance: use your provider's pooled connection URL (for example Supabase's transaction pooler or Neon's pooled endpoint).
|
|
412
414
|
|
|
413
415
|
Vercel runs no worker for [background jobs](#background-jobs). Set `CRON_SECRET` and add `"crons": [{ "path": "/api/jobs/run", "schedule": "* * * * *" }]` to `vercel.json`; Vercel Cron sends the secret itself. Cron jobs more frequent than daily need a paid Vercel plan; otherwise call the endpoint from another scheduler. <!-- clubedge:only jobs=pg-boss -->
|
|
@@ -418,6 +420,8 @@ Vercel runs no worker for [background jobs](#background-jobs). Set `CRON_SECRET`
|
|
|
418
420
|
`apps/web/netlify.toml` deploys the app to [Netlify](https://www.netlify.com). Import the repository, set the site's Base directory to `apps/web`, add the production variables listed below, and deploy. Netlify installs its Next.js runtime automatically. <!-- clubedge:only framework=next -->
|
|
419
421
|
`apps/web/netlify.toml` deploys the app to [Netlify](https://www.netlify.com). Import the repository, set the site's Base directory to `apps/web`, add the production variables listed below, and deploy. The build detects Netlify and writes its functions and static assets. <!-- clubedge:only framework=tanstack-start -->
|
|
420
422
|
|
|
423
|
+
Netlify only runs the app: the database, Redis, file storage, and email come from your providers (for example Supabase, Upstash, and Cloudflare R2). The build command starts with `pnpm check:env`, which stops the build when a variable the project's modules need is unset, or points at `localhost`, and lists each one. Set them for every deploy context you build (production and deploy previews); `SKIP_ENV_CHECK=1` builds anyway.
|
|
424
|
+
|
|
421
425
|
Netlify has no step that runs before a release, so the build command applies migrations (`pnpm db:migrate`) against the `DATABASE_URL` of the deploy context. Give deploy previews their own database, or remove the migration step from the build command and run migrations yourself. Serverless functions open a connection per instance: use your provider's pooled connection URL (for example Supabase's transaction pooler or Neon's pooled endpoint).
|
|
422
426
|
|
|
423
427
|
Netlify runs no worker for [background jobs](#background-jobs). Set `JOBS_WORKER=off` and `CRON_SECRET`, then call `/api/jobs/run` every minute from a [scheduled function](https://docs.netlify.com/build/functions/scheduled-functions/) or another scheduler, with the header `Authorization: Bearer <CRON_SECRET>`. <!-- clubedge:only jobs=pg-boss -->
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Netlify (https://docs.netlify.com/configure-builds/file-based-configuration/). Set the site's
|
|
2
2
|
# base directory to apps/web; the build detects Netlify and writes its functions and assets.
|
|
3
3
|
[build]
|
|
4
|
-
#
|
|
5
|
-
|
|
4
|
+
# check:env stops the build when a variable the project needs is unset; then migrations run,
|
|
5
|
+
# against the DATABASE_URL set for the deploy context.
|
|
6
|
+
command = "pnpm run check:env && pnpm --dir ../.. db:migrate && pnpm run build"
|
|
6
7
|
publish = "dist"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"build": "vite build",
|
|
10
10
|
"start": "node --env-file-if-exists=.env.local .output/server/index.mjs",
|
|
11
11
|
"typecheck": "tsr generate && tsc --noEmit",
|
|
12
|
-
"check:services": "tsx scripts/check-services.ts"
|
|
12
|
+
"check:services": "tsx scripts/check-services.ts",
|
|
13
|
+
"check:env": "tsx scripts/check-env.ts"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"@clubedge/auth": "workspace:*",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Checks the production variables before a deploy build: `pnpm check:env`. Vercel's and Netlify's
|
|
2
|
+
// build commands run it first, so a missing key stops the build. It reads the build's environment,
|
|
3
|
+
// not .env.local; SKIP_ENV_CHECK=1 builds anyway.
|
|
4
|
+
import { checkProductionEnv } from "../src/env/production";
|
|
5
|
+
|
|
6
|
+
function main() {
|
|
7
|
+
if (process.env.SKIP_ENV_CHECK === "1") {
|
|
8
|
+
console.warn(
|
|
9
|
+
"check:env skipped (SKIP_ENV_CHECK=1): missing variables fail at runtime instead.",
|
|
10
|
+
);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const { missing, local, invalid } = checkProductionEnv(process.env);
|
|
14
|
+
if (!missing.length && !local.length && !invalid.length) {
|
|
15
|
+
console.log("✓ check:env: every production variable this project needs is set.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const lines = [
|
|
19
|
+
...missing.map(({ key, use }) => `✗ ${key} is not set (${use})`),
|
|
20
|
+
...local.map((key) => `✗ ${key} points at localhost, which the deployed app can't reach`),
|
|
21
|
+
...invalid.map((problem) => `✗ ${problem}`),
|
|
22
|
+
];
|
|
23
|
+
console.error(
|
|
24
|
+
[
|
|
25
|
+
"check:env: the production environment is incomplete, so the build stops here.",
|
|
26
|
+
...lines,
|
|
27
|
+
"Fix them in the host's environment variables for this environment (Production or Preview),",
|
|
28
|
+
"then deploy again. SKIP_ENV_CHECK=1 builds anyway.",
|
|
29
|
+
].join("\n"),
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main();
|