@clubedge/create-clubedge-app 0.21.0 → 0.23.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 +18 -0
- package/README.md +24 -10
- package/dist/cli/args.js +10 -0
- package/dist/cli/ui.js +3 -1
- package/dist/core/deploy.js +176 -0
- package/dist/deploy.js +282 -0
- package/dist/index.js +5 -0
- package/package.json +3 -3
- package/template/Dockerfile +5 -5
- package/template/apps/start/Dockerfile +4 -4
- package/template/apps/start/package.json +1 -1
- package/template/apps/web/package.json +1 -1
- package/template/package.json +1 -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 +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.23.0
|
|
6
|
+
|
|
7
|
+
- `deploy` moves Railway projects to `.railway/railway.ts`, Railway's configuration as code, which replaces `railway.json`: Railway stops reading `railway.json` on 2026-12-01 (#59).
|
|
8
|
+
- When the project has no Railway configuration file, it writes one with the project's own names: the migrations before each release and the `/api/health` check, as in `railway.json`. Variables stay as `deploy` set them.
|
|
9
|
+
- It adds Railway's TypeScript SDK (`railway`) as a dev dependency, and applies the file with `railway config apply` before each release. Commit `.railway/railway.ts`, `package.json`, and `pnpm-lock.yaml`.
|
|
10
|
+
- It needs Railway CLI 5.42.1 or newer: an installed `railway`, else one npx already downloaded, and only then a download.
|
|
11
|
+
- Still on Starter `v0.16.1`.
|
|
12
|
+
|
|
13
|
+
## 0.22.0
|
|
14
|
+
|
|
15
|
+
- Add `deploy`: `pnpm clubedge deploy` does the first deploy of a project that uses `deploy railway`, with Railway's CLI and your Railway login (#55).
|
|
16
|
+
- It asks first, then creates or reuses the Railway project, Postgres (and Redis with `cache redis`), the app's service, and a `*.up.railway.app` domain.
|
|
17
|
+
- It sets the variables it can: the database references, `APP_URL` and `NEXT_PUBLIC_APP_URL`, `PORT`, and a generated `BETTER_AUTH_SECRET` that later runs keep. `--env-file .env.production` adds your provider keys; the ones still unset are listed.
|
|
18
|
+
- It follows the deployment until Railway's final status: the migrations run before the release and `/api/health` is checked. On a failure it names the logs commands. Running it again reuses everything.
|
|
19
|
+
- Railway only for now; `--dry-run` shows the plan.
|
|
20
|
+
- Default to Starter `v0.16.1` (#57): both Dockerfiles install dependencies in the build stage, so `node_modules` is no longer copied between stages, which made builds slow and timed out a first build on Railway.
|
|
21
|
+
- Release drafts list a Starter pin pull request once (#54).
|
|
22
|
+
|
|
5
23
|
## 0.21.0
|
|
6
24
|
|
|
7
25
|
- Default to Starter `v0.16.0` (#52), which adds:
|
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.16.
|
|
130
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.16.1
|
|
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.16.
|
|
149
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.16.1 # 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.
|
|
@@ -189,32 +189,44 @@ Inside a project, `pnpm clubedge try` uploads that project instead, to share wor
|
|
|
189
189
|
|
|
190
190
|
When the app is ready, the CLI opens the preview in your browser (`--no-open` turns that off) and asks whether to share it: claiming the VM, a free Railway sign-up, is what makes the URL work for anyone. `--claim` opens the claim page without asking. `try --status` shows the last VM's preview, time left, and claim link; it connects only while the VM is running, so it never creates a new one. To open the app from another network yourself, forward it: `ssh -i <key> -N -L 3000:127.0.0.1:3000 railway.new`, then http://localhost:3000 (the CLI prints the exact command). The CLI asks before creating anything (`--yes` agrees), keeps its own SSH key in your config directory (`~/.config/clubedge`, or `%APPDATA%\clubedge` on Windows) apart from `~/.ssh`, and connects only to Railway's known host key. Railway's [terms](https://railway.com/legal/terms) apply, and Railway can change or pause its free VM.
|
|
191
191
|
|
|
192
|
+
## Deploy to Railway
|
|
193
|
+
|
|
194
|
+
In a project created with `--deploy railway` (or switched with `pnpm clubedge add deploy railway`), one command does the first deploy:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
pnpm clubedge deploy # asks before creating anything
|
|
198
|
+
pnpm clubedge deploy --env-file .env.production # with your production provider keys
|
|
199
|
+
pnpm clubedge deploy --dry-run # show the plan, create nothing
|
|
200
|
+
```
|
|
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.
|
|
203
|
+
|
|
192
204
|
## Version relationship
|
|
193
205
|
|
|
194
206
|
Each CLI release is tested against one specific Starter release:
|
|
195
207
|
|
|
196
208
|
```text
|
|
197
|
-
create-clubedge-app v0.
|
|
209
|
+
create-clubedge-app v0.23.0
|
|
198
210
|
|
|
|
199
211
|
v
|
|
200
|
-
Clubedge Starter v0.16.
|
|
201
|
-
commit
|
|
212
|
+
Clubedge Starter v0.16.1
|
|
213
|
+
commit 72ff89e53a55a4e4a6262b364ce73be3b1c7a6b9
|
|
202
214
|
```
|
|
203
215
|
|
|
204
216
|
The current relationship is:
|
|
205
217
|
|
|
206
218
|
| CLI release | Default Starter release | Meaning |
|
|
207
219
|
| --- | --- | --- |
|
|
208
|
-
| `create-clubedge-app@0.
|
|
220
|
+
| `create-clubedge-app@0.23.0` | `clubedge-starter@v0.16.1` | The CLI's required, tested default |
|
|
209
221
|
|
|
210
222
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
211
223
|
|
|
212
|
-
The `v0.16.
|
|
213
|
-
`
|
|
224
|
+
The `v0.16.1` tag is expected to resolve to commit
|
|
225
|
+
`72ff89e53a55a4e4a6262b364ce73be3b1c7a6b9`. The CLI verifies this before
|
|
214
226
|
scaffolding with the default reference and fails rather than silently using a
|
|
215
227
|
moved tag.
|
|
216
228
|
|
|
217
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.16.
|
|
229
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.16.1`. 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.
|
|
218
230
|
|
|
219
231
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
220
232
|
|
|
@@ -269,7 +281,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
269
281
|
1. Make and validate changes in `clubedge-starter`.
|
|
270
282
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
271
283
|
3. Update the Starter package versions.
|
|
272
|
-
4. Commit `main`, create a release tag such as `v0.16.
|
|
284
|
+
4. Commit `main`, create a release tag such as `v0.16.1`, and publish the Starter release.
|
|
273
285
|
|
|
274
286
|
### CLI maintainers
|
|
275
287
|
|
|
@@ -300,6 +312,8 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
300
312
|
| 0.19.0 | v0.15.0 | `pnpm clubedge try` uploads an existing project to the VM; the preview opens in the browser |
|
|
301
313
|
| 0.20.0 | v0.15.0 | The Starter's version of each conflicted file in `.clubedge/incoming/`, several modules at once, sharing a `try` preview by claiming the VM, `try --status` |
|
|
302
314
|
| 0.21.0 | v0.16.0 | Starter v0.16.0: upgrades delivered as pull requests, Node 24 actions; release tooling for maintainers |
|
|
315
|
+
| 0.22.0 | v0.16.1 | `pnpm clubedge deploy`: a guided first deploy to Railway; faster Docker builds |
|
|
316
|
+
| 0.23.0 | v0.16.1 | `deploy` moves Railway projects to `.railway/railway.ts` before Railway stops reading `railway.json` |
|
|
303
317
|
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
304
318
|
|
|
305
319
|
## Community
|
package/dist/cli/args.js
CHANGED
|
@@ -16,6 +16,7 @@ const OPTIONS = {
|
|
|
16
16
|
open: { type: "boolean", default: true },
|
|
17
17
|
claim: { type: "boolean", default: false },
|
|
18
18
|
status: { type: "boolean", default: false },
|
|
19
|
+
"env-file": { type: "string" },
|
|
19
20
|
help: { type: "boolean", short: "h", default: false },
|
|
20
21
|
version: { type: "boolean", short: "v", default: false },
|
|
21
22
|
};
|
|
@@ -89,6 +90,9 @@ export function parseCliArgs(argv) {
|
|
|
89
90
|
if (!creates && command.name !== "upgrade" && values.ref !== undefined) {
|
|
90
91
|
throw new UsageError("--ref only applies when creating or upgrading a project.");
|
|
91
92
|
}
|
|
93
|
+
if (command.name !== "deploy" && values["env-file"] !== undefined) {
|
|
94
|
+
throw new UsageError("--env-file only applies to deploy.");
|
|
95
|
+
}
|
|
92
96
|
if (command.name !== "try" && (values.claim || values.status)) {
|
|
93
97
|
throw new UsageError("--claim and --status only apply to try.");
|
|
94
98
|
}
|
|
@@ -120,6 +124,7 @@ export function parseCliArgs(argv) {
|
|
|
120
124
|
open: values.open,
|
|
121
125
|
claim: values.claim,
|
|
122
126
|
status: values.status,
|
|
127
|
+
envFile: values["env-file"],
|
|
123
128
|
help: values.help,
|
|
124
129
|
version: values.version,
|
|
125
130
|
};
|
|
@@ -156,6 +161,11 @@ function parseCommand(positionals) {
|
|
|
156
161
|
throw new UsageError(`Usage: ${name}, run inside a project.`);
|
|
157
162
|
return { name };
|
|
158
163
|
}
|
|
164
|
+
if (name === "deploy") {
|
|
165
|
+
if (module !== undefined)
|
|
166
|
+
throw new UsageError("Usage: deploy [--env-file <path>], run inside a project.");
|
|
167
|
+
return { name };
|
|
168
|
+
}
|
|
159
169
|
if (name === "try") {
|
|
160
170
|
if (module !== undefined) {
|
|
161
171
|
throw new UsageError("Usage: try [--framework <id>] [module flags] (a new project), or try inside a project to upload it.");
|
package/dist/cli/ui.js
CHANGED
|
@@ -92,9 +92,10 @@ Usage:
|
|
|
92
92
|
pnpm dlx @clubedge/create-clubedge-app status (inside a project)
|
|
93
93
|
pnpm dlx @clubedge/create-clubedge-app doctor (inside a project)
|
|
94
94
|
pnpm dlx @clubedge/create-clubedge-app try [options] (experimental)
|
|
95
|
+
pnpm dlx @clubedge/create-clubedge-app deploy [options] (inside a project)
|
|
95
96
|
|
|
96
97
|
Inside a project with the clubedge script, use pnpm clubedge add, remove,
|
|
97
|
-
upgrade, status, doctor, or
|
|
98
|
+
upgrade, status, doctor, try, or deploy. Without a module or option, add and remove ask
|
|
98
99
|
for them.
|
|
99
100
|
|
|
100
101
|
add and remove change modules of an existing project, several at once as one
|
|
@@ -126,6 +127,7 @@ ${selectionHelp(manifest)}
|
|
|
126
127
|
--no-open try: do not open the preview in the browser
|
|
127
128
|
--claim try: open the page that claims the VM, to share the preview
|
|
128
129
|
--status try: the last VM's preview, time left, and claim link
|
|
130
|
+
--env-file <path> deploy: production values for the service, such as .env.production
|
|
129
131
|
-y, --yes Accept defaults and never prompt (default directory: my-app)
|
|
130
132
|
--no-install Skip dependency installation
|
|
131
133
|
--no-git Skip Git repository initialization
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// What `deploy` sets up on Railway for a project, as pure functions: the service name, the
|
|
2
|
+
// production variables, and reading the Railway CLI's output. src/deploy.ts runs the CLI.
|
|
3
|
+
/** The port the production image listens on (the Starter's Dockerfile sets PORT=3000). */
|
|
4
|
+
export const appPort = 3000;
|
|
5
|
+
/** Railway's names for the databases `railway add --database` creates, used in references. */
|
|
6
|
+
export const railwayDatabases = { postgres: "Postgres", redis: "Redis" };
|
|
7
|
+
/** A Railway service name from the package name: lowercase letters, digits, and dashes. */
|
|
8
|
+
export function serviceName(packageName) {
|
|
9
|
+
const name = packageName.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10
|
+
return name || "web";
|
|
11
|
+
}
|
|
12
|
+
/** The databases a project needs on Railway: Postgres always (Drizzle), Redis with the redis cache. */
|
|
13
|
+
export function databasesFor(modules) {
|
|
14
|
+
return modules.cache === "redis" ? ["postgres", "redis"] : ["postgres"];
|
|
15
|
+
}
|
|
16
|
+
/** The variable names a project's .env.example lists, in order. */
|
|
17
|
+
export function exampleKeys(example) {
|
|
18
|
+
const keys = example
|
|
19
|
+
.split(/\r?\n/)
|
|
20
|
+
.map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1])
|
|
21
|
+
.filter((key) => Boolean(key));
|
|
22
|
+
return [...new Set(keys)];
|
|
23
|
+
}
|
|
24
|
+
/** KEY=value lines of an env file; quotes around a value are removed, comments skipped. */
|
|
25
|
+
export function parseEnvFile(text) {
|
|
26
|
+
const values = {};
|
|
27
|
+
for (const line of text.split(/\r?\n/)) {
|
|
28
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
29
|
+
if (!match)
|
|
30
|
+
continue;
|
|
31
|
+
const raw = match[2].trim();
|
|
32
|
+
// A quoted value keeps everything inside the quotes, and may still have a comment after them.
|
|
33
|
+
const quoted = /^(["'])(.*?)\1\s*(?:#.*)?$/.exec(raw);
|
|
34
|
+
values[match[1]] = quoted ? quoted[2] : raw.replace(/\s+#.*$/, "");
|
|
35
|
+
}
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The service's production variables. The CLI provides what it can: references to Railway's
|
|
40
|
+
* databases, a generated Better Auth secret (unless the service already has one), the public URL,
|
|
41
|
+
* and the port. Values from --env-file come next and win over those, except the database
|
|
42
|
+
* references, which must point at Railway's databases. Everything else the example lists is
|
|
43
|
+
* reported as unset; values the service already has count as set.
|
|
44
|
+
*/
|
|
45
|
+
export function planVariables(input) {
|
|
46
|
+
const { modules, keys, url, existing, envFile, secret } = input;
|
|
47
|
+
const reference = (database, variable) => `\${{${database}.${variable}}}`;
|
|
48
|
+
const provided = {
|
|
49
|
+
NODE_ENV: "production",
|
|
50
|
+
PORT: String(appPort),
|
|
51
|
+
DATABASE_URL: reference(railwayDatabases.postgres, "DATABASE_URL"),
|
|
52
|
+
...(modules.cache === "redis" ? { REDIS_URL: reference(railwayDatabases.redis, "REDIS_URL") } : {}),
|
|
53
|
+
};
|
|
54
|
+
// The public URL under both names: TanStack Start reads APP_URL, Next.js NEXT_PUBLIC_APP_URL.
|
|
55
|
+
const set = { ...provided, APP_URL: url, NEXT_PUBLIC_APP_URL: url };
|
|
56
|
+
if (modules.auth === "better-auth" && !existing.BETTER_AUTH_SECRET)
|
|
57
|
+
set.BETTER_AUTH_SECRET = secret();
|
|
58
|
+
for (const [key, value] of Object.entries(envFile)) {
|
|
59
|
+
if (key === "DATABASE_URL" || key === "REDIS_URL" || value === "")
|
|
60
|
+
continue;
|
|
61
|
+
set[key] = value;
|
|
62
|
+
}
|
|
63
|
+
const unset = keys.filter((key) => set[key] === undefined && !existing[key]);
|
|
64
|
+
return { set, unset };
|
|
65
|
+
}
|
|
66
|
+
/** The public domain in `railway domain` output (JSON or text). */
|
|
67
|
+
export function parseRailwayDomain(output) {
|
|
68
|
+
const domain = /([a-z0-9-]+(?:\.[a-z0-9-]+)*\.up\.railway\.app)/i.exec(output)?.[1];
|
|
69
|
+
return domain ? domain.toLowerCase() : null;
|
|
70
|
+
}
|
|
71
|
+
/** The service names in `railway status --json`, wherever the CLI puts them. */
|
|
72
|
+
export function serviceNames(status) {
|
|
73
|
+
const names = new Set();
|
|
74
|
+
const visit = (value, inServices) => {
|
|
75
|
+
if (Array.isArray(value))
|
|
76
|
+
return value.forEach((item) => visit(item, inServices));
|
|
77
|
+
if (!value || typeof value !== "object")
|
|
78
|
+
return;
|
|
79
|
+
for (const [key, child] of Object.entries(value)) {
|
|
80
|
+
if (inServices && key === "name" && typeof child === "string")
|
|
81
|
+
names.add(child);
|
|
82
|
+
visit(child, inServices || key === "services");
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
visit(status, false);
|
|
86
|
+
return [...names];
|
|
87
|
+
}
|
|
88
|
+
/** `railway variable list --json`: a KEY -> value object, or an array of { name, value }. */
|
|
89
|
+
export function parseVariables(output) {
|
|
90
|
+
try {
|
|
91
|
+
const value = JSON.parse(output);
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return Object.fromEntries(value
|
|
94
|
+
.filter((item) => typeof item?.name === "string")
|
|
95
|
+
.map((item) => [item.name, String(item.value ?? "")]));
|
|
96
|
+
}
|
|
97
|
+
if (value && typeof value === "object") {
|
|
98
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, String(child ?? "")]));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Not JSON: no variables known.
|
|
103
|
+
}
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
/** Deployment statuses Railway does not move on from. */
|
|
107
|
+
export const finalStatuses = ["SUCCESS", "FAILED", "CRASHED", "REMOVED", "SKIPPED"];
|
|
108
|
+
/** The deployment `railway up` started, from the dashboard link it prints (`...?id=<uuid>`). */
|
|
109
|
+
export function parseDeploymentId(output) {
|
|
110
|
+
return /[?&]id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i.exec(output)?.[1] ?? null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A deployment's status from `railway deployment list --json` (newest first): the one with `id`,
|
|
114
|
+
* or else the newest created at or after `since`. Null while Railway does not list it yet.
|
|
115
|
+
*/
|
|
116
|
+
export function deploymentStatus(output, id, since) {
|
|
117
|
+
let list;
|
|
118
|
+
try {
|
|
119
|
+
list = JSON.parse(output);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (!Array.isArray(list))
|
|
125
|
+
return null;
|
|
126
|
+
const deployments = list.filter((item) => typeof item?.id === "string" && typeof item?.status === "string");
|
|
127
|
+
const found = id
|
|
128
|
+
? deployments.find((deployment) => deployment.id === id)
|
|
129
|
+
: deployments.find((deployment) => deployment.createdAt && new Date(deployment.createdAt).getTime() >= since.getTime() - 5_000);
|
|
130
|
+
return found ? { id: found.id, status: found.status } : null;
|
|
131
|
+
}
|
|
132
|
+
/** The oldest Railway CLI that applies `.railway/railway.ts`: its IaC engine ships in the CLI. */
|
|
133
|
+
export const minimumRailwayCli = [5, 42, 1];
|
|
134
|
+
/** The version in `railway --version` output, such as "railway 5.67.1". */
|
|
135
|
+
export function railwayCliVersion(output) {
|
|
136
|
+
const match = /\b(\d+)\.(\d+)\.(\d+)\b/.exec(output);
|
|
137
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
138
|
+
}
|
|
139
|
+
export function meetsMinimumCli(version) {
|
|
140
|
+
for (let i = 0; i < 3; i++) {
|
|
141
|
+
if (version[i] !== minimumRailwayCli[i])
|
|
142
|
+
return version[i] > minimumRailwayCli[i];
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
export const railwayIacPath = ".railway/railway.ts";
|
|
147
|
+
/**
|
|
148
|
+
* The project's Railway configuration as code (`.railway/railway.ts`), which replaces railway.json:
|
|
149
|
+
* Railway stops reading railway.json on 2026-12-01. Railway builds the root Dockerfile by itself,
|
|
150
|
+
* the variables stay managed by `deploy` and the dashboard, and the region is Railway's default.
|
|
151
|
+
*/
|
|
152
|
+
export function railwayIacSource(service) {
|
|
153
|
+
const name = JSON.stringify(service);
|
|
154
|
+
return `// Railway's configuration for this project, applied with \`railway config apply\` (pnpm clubedge
|
|
155
|
+
// deploy does it before each release). It replaces railway.json, which Railway stops reading on
|
|
156
|
+
// 2026-12-01. Railway builds the root Dockerfile; variables are set with \`railway variable set\`.
|
|
157
|
+
import { defineRailway, project, service } from "railway/iac";
|
|
158
|
+
|
|
159
|
+
// This repository manages only its own resources in the Railway project.
|
|
160
|
+
export const partial = ${name};
|
|
161
|
+
|
|
162
|
+
export default defineRailway(() => {
|
|
163
|
+
const app = service(${name}, {
|
|
164
|
+
// Migrations run before each release; a failed migration stops it.
|
|
165
|
+
preDeploy: "node /app/migrate.mjs",
|
|
166
|
+
healthcheck: "/api/health",
|
|
167
|
+
healthcheckTimeout: 60,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return project(${name}, {
|
|
171
|
+
variables: { managed: false },
|
|
172
|
+
resources: [app],
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
`;
|
|
176
|
+
}
|
package/dist/deploy.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// `deploy`: a guided first deploy to Railway, with Railway's own CLI. It creates (or reuses) the
|
|
2
|
+
// project, its databases, the app's service and domain, sets the production variables, and runs
|
|
3
|
+
// `railway up`; Railway then builds the image, runs the migrations, and checks /api/health.
|
|
4
|
+
import { cancel, confirm, isCancel, log, note, outro, spinner } from "@clack/prompts";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import { UsageError } from "./cli/args.js";
|
|
12
|
+
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 { readProject } from "./core/project.js";
|
|
15
|
+
import { cliCommand } from "./modify.js";
|
|
16
|
+
import { resolvePnpm } from "./steps/install.js";
|
|
17
|
+
import { pathExists } from "./utils/fs.js";
|
|
18
|
+
import { run } from "./utils/process.js";
|
|
19
|
+
const railwayTermsUrl = "https://railway.com/legal/terms";
|
|
20
|
+
const deployDocsUrl = "https://starter.clubedge.live/docs/starter/deploy";
|
|
21
|
+
const windows = process.platform === "win32";
|
|
22
|
+
const binaryName = windows ? "railway.exe" : "railway";
|
|
23
|
+
async function cliVersion(command, shell) {
|
|
24
|
+
return await new Promise((resolveVersion) => {
|
|
25
|
+
const [file, ...args] = command;
|
|
26
|
+
const child = spawn(file, [...args, "--version"], { shell, stdio: ["ignore", "pipe", "ignore"] });
|
|
27
|
+
let out = "";
|
|
28
|
+
child.stdout?.on("data", (chunk) => (out += chunk.toString()));
|
|
29
|
+
child.once("error", () => resolveVersion(null));
|
|
30
|
+
child.once("close", (code) => resolveVersion(code === 0 ? railwayCliVersion(out) : null));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** The railway.exe behind `railway` on PATH (npm installs a railway.cmd shim next to its package). */
|
|
34
|
+
async function installedBinary() {
|
|
35
|
+
if (!windows)
|
|
36
|
+
return (await cliVersion(["railway"], false)) ? "railway" : null;
|
|
37
|
+
const found = await new Promise((resolveWhere) => {
|
|
38
|
+
const child = spawn("where", ["railway"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
39
|
+
let out = "";
|
|
40
|
+
child.stdout?.on("data", (chunk) => (out += chunk.toString()));
|
|
41
|
+
child.once("error", () => resolveWhere(""));
|
|
42
|
+
child.once("close", () => resolveWhere(out));
|
|
43
|
+
});
|
|
44
|
+
for (const line of found.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
|
|
45
|
+
if (line.toLowerCase().endsWith(".exe"))
|
|
46
|
+
return line;
|
|
47
|
+
const shimmed = join(dirname(line), "node_modules", "@railway", "cli", "bin", binaryName);
|
|
48
|
+
if (await pathExists(shimmed))
|
|
49
|
+
return shimmed;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/** Railway CLI binaries npx already downloaded, newest first: no download on a slow connection. */
|
|
54
|
+
async function cachedBinaries() {
|
|
55
|
+
const cache = process.env.npm_config_cache ||
|
|
56
|
+
(windows && process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "npm-cache") : join(homedir(), ".npm"));
|
|
57
|
+
const found = [];
|
|
58
|
+
for (const entry of await readdir(join(cache, "_npx")).catch(() => [])) {
|
|
59
|
+
const path = join(cache, "_npx", entry, "node_modules", "@railway", "cli", "bin", binaryName);
|
|
60
|
+
const stats = await stat(path).catch(() => null);
|
|
61
|
+
if (stats?.isFile())
|
|
62
|
+
found.push({ path, time: stats.mtimeMs });
|
|
63
|
+
}
|
|
64
|
+
return found.sort((a, b) => b.time - a.time).map((entry) => entry.path);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A Railway CLI new enough for `.railway/railway.ts`: the installed one, then one npx already
|
|
68
|
+
* downloaded, and only then a download through npx.
|
|
69
|
+
*/
|
|
70
|
+
async function railwayCli() {
|
|
71
|
+
const usable = async (binary) => {
|
|
72
|
+
const version = await cliVersion([binary], false);
|
|
73
|
+
return version !== null && meetsMinimumCli(version);
|
|
74
|
+
};
|
|
75
|
+
const installed = await installedBinary();
|
|
76
|
+
if (installed && (await usable(installed)))
|
|
77
|
+
return { command: [installed], executable: installed, shell: false };
|
|
78
|
+
for (const binary of await cachedBinaries()) {
|
|
79
|
+
if (await usable(binary))
|
|
80
|
+
return { command: [binary], executable: binary, shell: false };
|
|
81
|
+
}
|
|
82
|
+
await cliVersion(["npx", "--yes", "@railway/cli@latest"], windows);
|
|
83
|
+
for (const binary of await cachedBinaries()) {
|
|
84
|
+
if (await usable(binary))
|
|
85
|
+
return { command: [binary], executable: binary, shell: false };
|
|
86
|
+
}
|
|
87
|
+
throw new UsageError(`deploy needs Railway CLI ${minimumRailwayCli.join(".")} or newer. Install it with \`npm install -g @railway/cli\`, then run deploy again.`);
|
|
88
|
+
}
|
|
89
|
+
async function railway(cli, args, options = {}) {
|
|
90
|
+
const [command, ...prefix] = cli.command;
|
|
91
|
+
return await new Promise((resolvePromise, reject) => {
|
|
92
|
+
const child = spawn(command, [...prefix, ...args], {
|
|
93
|
+
shell: cli.shell,
|
|
94
|
+
env: { ...process.env, _: cli.executable },
|
|
95
|
+
stdio: options.inherit ? "inherit" : ["pipe", "pipe", "pipe"],
|
|
96
|
+
});
|
|
97
|
+
let stdout = "";
|
|
98
|
+
let stderr = "";
|
|
99
|
+
child.stdout?.on("data", (chunk) => (stdout += chunk.toString()));
|
|
100
|
+
child.stderr?.on("data", (chunk) => (stderr += chunk.toString()));
|
|
101
|
+
child.once("error", reject);
|
|
102
|
+
// close, not exit: it fires once stdout and stderr are fully read, so JSON output is never cut off.
|
|
103
|
+
child.once("close", (code) => {
|
|
104
|
+
if (code === 0)
|
|
105
|
+
resolvePromise(stdout.trim());
|
|
106
|
+
else
|
|
107
|
+
reject(new Error(`railway ${args[0]} failed${stderr.trim() ? `: ${stderr.trim().split("\n").slice(-3).join(" ")}` : "."}`));
|
|
108
|
+
});
|
|
109
|
+
if (child.stdin) {
|
|
110
|
+
// A command that exits before reading its input must not crash the CLI with EPIPE.
|
|
111
|
+
child.stdin.on("error", () => { });
|
|
112
|
+
child.stdin.end(options.input ?? "");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const attempt = async (run) => run().catch(() => null);
|
|
117
|
+
async function agree(args, service, databases) {
|
|
118
|
+
note([
|
|
119
|
+
"This deploys the project to Railway, in your Railway account:",
|
|
120
|
+
"",
|
|
121
|
+
`• A project with ${databases.map((db) => railwayDatabases[db]).join(" and ")}, and a service "${service}" with a public *.up.railway.app domain.`,
|
|
122
|
+
"• The production variables Railway can provide; your provider keys come from --env-file.",
|
|
123
|
+
"• Railway builds the Dockerfile, runs the migrations before the release, and checks /api/health; the first build takes several minutes.",
|
|
124
|
+
`• The service's settings go in ${railwayIacPath}, which replaces railway.json; the railway package is added to read it.`,
|
|
125
|
+
"",
|
|
126
|
+
`Railway bills usage on your plan. Running deploy again reuses all of it. Railway's terms: ${railwayTermsUrl}`,
|
|
127
|
+
].join("\n"), "Deploy to Railway");
|
|
128
|
+
if (args.yes)
|
|
129
|
+
return true;
|
|
130
|
+
if (!process.stdin.isTTY)
|
|
131
|
+
throw new UsageError("deploy creates resources in your Railway account. Run it in a terminal to confirm, or pass --yes.");
|
|
132
|
+
const answer = await confirm({ message: "Deploy?", initialValue: true });
|
|
133
|
+
return !isCancel(answer) && answer;
|
|
134
|
+
}
|
|
135
|
+
export async function deployProject(args) {
|
|
136
|
+
const project = await readProject(process.cwd());
|
|
137
|
+
const cli = await cliCommand(project);
|
|
138
|
+
const platform = project.modules.deploy ?? "docker";
|
|
139
|
+
if (platform !== "railway") {
|
|
140
|
+
throw new UsageError(`deploy sets up Railway for now, and this project deploys with "${platform}". Switch with \`${cli} add deploy railway\`, or follow ${deployDocsUrl} for ${platform}.`);
|
|
141
|
+
}
|
|
142
|
+
const service = serviceName(project.identity.packageName);
|
|
143
|
+
const databases = databasesFor(project.modules);
|
|
144
|
+
const keys = exampleKeys(await readFile(join(project.root, ".env.example"), "utf8").catch(() => ""));
|
|
145
|
+
const envFile = args.envFile ? parseEnvFile(await readFile(resolve(process.cwd(), args.envFile), "utf8")) : {};
|
|
146
|
+
const secret = () => randomBytes(32).toString("hex");
|
|
147
|
+
if (args.dryRun) {
|
|
148
|
+
const preview = planVariables({ modules: project.modules, keys, url: "https://<service>.up.railway.app", existing: {}, envFile, secret: () => "<generated>" });
|
|
149
|
+
log.message([
|
|
150
|
+
`• Railway project "${service}" with ${databases.join(" and ")}, and the service "${service}"`,
|
|
151
|
+
`• A *.up.railway.app domain for port ${appPort}`,
|
|
152
|
+
`• ${railwayIacPath} (written if missing, with Railway's SDK added), applied with railway config apply`,
|
|
153
|
+
`• Variables: ${Object.keys(preview.set).join(", ")}`,
|
|
154
|
+
...(preview.unset.length ? [`• Not set: ${preview.unset.join(", ")}`] : []),
|
|
155
|
+
`• railway up: build, migrations, health check`,
|
|
156
|
+
].join("\n"));
|
|
157
|
+
outro("Dry run: nothing was created. Run again without --dry-run to deploy.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!(await agree(args, service, databases))) {
|
|
161
|
+
cancel("Nothing was deployed.");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const progress = spinner();
|
|
165
|
+
progress.start("Preparing the Railway CLI");
|
|
166
|
+
const rw = await railwayCli();
|
|
167
|
+
// The project's directory, so Railway links it there.
|
|
168
|
+
process.chdir(project.root);
|
|
169
|
+
if (!(await attempt(() => railway(rw, ["whoami"])))) {
|
|
170
|
+
progress.stop("Signing in to Railway");
|
|
171
|
+
if (!process.stdin.isTTY)
|
|
172
|
+
throw new UsageError("Sign in to Railway first with `railway login`, or set RAILWAY_API_TOKEN.");
|
|
173
|
+
await railway(rw, ["login"], { inherit: true });
|
|
174
|
+
progress.start("Signed in");
|
|
175
|
+
}
|
|
176
|
+
progress.message("Finding or creating the Railway project");
|
|
177
|
+
let status = await attempt(() => railway(rw, ["status", "--json"]));
|
|
178
|
+
if (!status) {
|
|
179
|
+
await railway(rw, ["init", "--name", service, "--json"]);
|
|
180
|
+
status = await attempt(() => railway(rw, ["status", "--json"]));
|
|
181
|
+
}
|
|
182
|
+
let existing = [];
|
|
183
|
+
try {
|
|
184
|
+
existing = serviceNames(JSON.parse(status ?? "{}"));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Unknown status format: create what is missing, as for a new project.
|
|
188
|
+
}
|
|
189
|
+
for (const database of databases) {
|
|
190
|
+
if (existing.includes(railwayDatabases[database]))
|
|
191
|
+
continue;
|
|
192
|
+
progress.message(`Adding ${railwayDatabases[database]}`);
|
|
193
|
+
await railway(rw, ["add", "--database", database]);
|
|
194
|
+
}
|
|
195
|
+
if (!existing.includes(service)) {
|
|
196
|
+
progress.message(`Adding the service "${service}"`);
|
|
197
|
+
await railway(rw, ["add", "--service", service]);
|
|
198
|
+
}
|
|
199
|
+
progress.message("Creating the public domain");
|
|
200
|
+
const listed = await attempt(() => railway(rw, ["domain", "list", "--service", service]));
|
|
201
|
+
const domain = parseRailwayDomain(listed ?? "") ??
|
|
202
|
+
parseRailwayDomain(await railway(rw, ["domain", "--service", service, "--port", String(appPort), "--json"]));
|
|
203
|
+
if (!domain)
|
|
204
|
+
throw new Error("Railway created no *.up.railway.app domain. Create one in the service's settings, then run deploy again.");
|
|
205
|
+
const url = `https://${domain}`;
|
|
206
|
+
progress.message("Setting the production variables");
|
|
207
|
+
const current = parseVariables((await attempt(() => railway(rw, ["variable", "list", "--service", service, "--json"]))) ?? "");
|
|
208
|
+
const plan = planVariables({ modules: project.modules, keys, url, existing: current, envFile, secret });
|
|
209
|
+
for (const [key, value] of Object.entries(plan.set)) {
|
|
210
|
+
if (current[key] === value)
|
|
211
|
+
continue;
|
|
212
|
+
await railway(rw, ["variable", "set", key, "--stdin", "--service", service, "--skip-deploys"], { input: value });
|
|
213
|
+
}
|
|
214
|
+
// The service's settings as code: .railway/railway.ts replaces railway.json, which Railway stops
|
|
215
|
+
// reading on 2026-12-01. It needs Railway's TypeScript SDK, and applies before each release.
|
|
216
|
+
const iacFiles = [railwayIacPath, ".railway/railway.py", ".railway/railway.go"];
|
|
217
|
+
const changed = [];
|
|
218
|
+
let hasIac = false;
|
|
219
|
+
for (const file of iacFiles)
|
|
220
|
+
if (await pathExists(join(project.root, file)))
|
|
221
|
+
hasIac = true;
|
|
222
|
+
if (!hasIac) {
|
|
223
|
+
await mkdir(join(project.root, ".railway"), { recursive: true });
|
|
224
|
+
await writeFile(join(project.root, railwayIacPath), railwayIacSource(service));
|
|
225
|
+
changed.push(railwayIacPath);
|
|
226
|
+
}
|
|
227
|
+
if (await pathExists(join(project.root, railwayIacPath))) {
|
|
228
|
+
const manifest = JSON.parse(await readFile(join(project.root, "package.json"), "utf8"));
|
|
229
|
+
if (!manifest.devDependencies?.railway && !manifest.dependencies?.railway) {
|
|
230
|
+
const pnpm = await resolvePnpm();
|
|
231
|
+
if (!pnpm)
|
|
232
|
+
throw new UsageError("pnpm was not found. Enable it with `corepack enable`, then run deploy again.");
|
|
233
|
+
progress.stop("Adding Railway's TypeScript SDK (railway), which .railway/railway.ts imports");
|
|
234
|
+
const [command, ...prefix] = pnpm;
|
|
235
|
+
await run(command, [...prefix, "add", "--save-dev", "--workspace-root", "railway"], project.root);
|
|
236
|
+
changed.push("package.json", "pnpm-lock.yaml");
|
|
237
|
+
progress.start("Applying .railway/railway.ts");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
progress.message(`Applying ${railwayIacPath}`);
|
|
241
|
+
await railway(rw, ["config", "apply", "--yes"]);
|
|
242
|
+
progress.stop(`Railway is ready: ${url}`);
|
|
243
|
+
if (changed.length) {
|
|
244
|
+
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).`);
|
|
245
|
+
}
|
|
246
|
+
// Start the deploy detached, then follow its status: `railway up` can return before the build
|
|
247
|
+
// ends, and only Railway's final status says whether the release went live.
|
|
248
|
+
const deploying = spinner();
|
|
249
|
+
deploying.start("Uploading the project");
|
|
250
|
+
const started = new Date();
|
|
251
|
+
const upload = await railway(rw, ["up", "--service", service, "--detach"]);
|
|
252
|
+
const id = parseDeploymentId(upload);
|
|
253
|
+
let deployment = "QUEUED";
|
|
254
|
+
const deadline = Date.now() + 45 * 60_000;
|
|
255
|
+
while (!finalStatuses.includes(deployment)) {
|
|
256
|
+
if (Date.now() > deadline)
|
|
257
|
+
break;
|
|
258
|
+
const elapsed = Math.round((Date.now() - started.getTime()) / 1000);
|
|
259
|
+
deploying.message(`Railway: ${deployment.toLowerCase()} (${Math.floor(elapsed / 60)}m ${elapsed % 60}s; the first build takes several minutes)`);
|
|
260
|
+
await new Promise((wait) => setTimeout(wait, 10_000));
|
|
261
|
+
const list = await attempt(() => railway(rw, ["deployment", "list", "--service", service, "--json"]));
|
|
262
|
+
deployment = deploymentStatus(list ?? "", id, started)?.status ?? deployment;
|
|
263
|
+
}
|
|
264
|
+
if (deployment !== "SUCCESS") {
|
|
265
|
+
deploying.stop(`The deploy ended as ${deployment}`);
|
|
266
|
+
throw new Error([
|
|
267
|
+
`Railway's deploy ended as ${deployment}. Read why with \`railway logs --build --service ${service}\` (the build) or \`railway logs --service ${service}\` (the app and its health check).`,
|
|
268
|
+
"A build that ran out of time (DeadlineExceeded) usually passes on a second try. Otherwise fix the cause and run deploy again: it reuses what exists.",
|
|
269
|
+
].join("\n"));
|
|
270
|
+
}
|
|
271
|
+
deploying.stop("Deployed: the migrations ran and /api/health answered");
|
|
272
|
+
note([
|
|
273
|
+
`${bold("URL")} ${url}`,
|
|
274
|
+
`${bold("Service")} ${service}`,
|
|
275
|
+
...(plan.unset.length ? ["", `${bold("Not set")} ${plan.unset.join(", ")}`, dim("Set the ones your modules use: railway variable set KEY --stdin, or the dashboard.")] : []),
|
|
276
|
+
].join("\n"), "Deployed to Railway");
|
|
277
|
+
outro([
|
|
278
|
+
`${dim("Logs")} railway logs --service ${service}`,
|
|
279
|
+
`${dim("Dashboard")} railway open`,
|
|
280
|
+
`${dim("Again")} ${cli} deploy ${dim("(reuses the project; --env-file to add variables)")}`,
|
|
281
|
+
].join("\n"));
|
|
282
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { readBundledTemplate, resolveTemplateSource } from "./core/source.js";
|
|
|
11
11
|
import { cliUrl, cliVersion, siteUrl, starterPin } from "./package-info.js";
|
|
12
12
|
import { runDoctor, showStatus } from "./inspect.js";
|
|
13
13
|
import { changeProject } from "./modify.js";
|
|
14
|
+
import { deployProject } from "./deploy.js";
|
|
14
15
|
import { tryOnRailway } from "./try.js";
|
|
15
16
|
import { initializeGit } from "./steps/git.js";
|
|
16
17
|
import { detectLauncher, installDependencies, resolvePnpm } from "./steps/install.js";
|
|
@@ -235,6 +236,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
235
236
|
intro("Check this project's setup");
|
|
236
237
|
await runDoctor(args);
|
|
237
238
|
}
|
|
239
|
+
else if (args.command.name === "deploy") {
|
|
240
|
+
intro("Deploy this project to Railway");
|
|
241
|
+
await deployProject(args);
|
|
242
|
+
}
|
|
238
243
|
else if (args.command.name === "try") {
|
|
239
244
|
intro("Try a project on a free Railway VM");
|
|
240
245
|
await tryOnRailway(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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.16.
|
|
26
|
-
"starterCommit": "
|
|
25
|
+
"starterRef": "v0.16.1",
|
|
26
|
+
"starterCommit": "72ff89e53a55a4e4a6262b364ce73be3b1c7a6b9"
|
|
27
27
|
},
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"repository": {
|
package/template/Dockerfile
CHANGED
|
@@ -8,13 +8,13 @@ FROM base AS pruner
|
|
|
8
8
|
COPY . .
|
|
9
9
|
RUN pnpm dlx turbo@^2.5.0 prune @clubedge/web --docker
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
# Install in the stage that builds: copying node_modules from a separate install stage (many
|
|
12
|
+
# small files) took minutes on some builders, and timed out a Railway build. The manifests come
|
|
13
|
+
# first, so the install layer is still cached until a dependency changes.
|
|
15
14
|
FROM base AS builder
|
|
16
15
|
ENV NEXT_TELEMETRY_DISABLED=1
|
|
17
|
-
COPY --from=
|
|
16
|
+
COPY --from=pruner /app/out/json/ .
|
|
17
|
+
RUN pnpm install --frozen-lockfile
|
|
18
18
|
COPY --from=pruner /app/out/full/ .
|
|
19
19
|
COPY tsconfig.base.json ./
|
|
20
20
|
RUN pnpm build
|
|
@@ -16,12 +16,12 @@ ARG APP_PACKAGE
|
|
|
16
16
|
COPY . .
|
|
17
17
|
RUN pnpm dlx turbo@^2.5.0 prune ${APP_PACKAGE} --docker
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Install in the stage that builds: copying node_modules from a separate install stage (many
|
|
20
|
+
# small files) took minutes on some builders, and timed out a Railway build. The manifests come
|
|
21
|
+
# first, so the install layer is still cached until a dependency changes.
|
|
22
|
+
FROM base AS builder
|
|
20
23
|
COPY --from=pruner /app/out/json/ .
|
|
21
24
|
RUN pnpm install --frozen-lockfile
|
|
22
|
-
|
|
23
|
-
FROM base AS builder
|
|
24
|
-
COPY --from=deps /app/ .
|
|
25
25
|
COPY --from=pruner /app/out/full/ .
|
|
26
26
|
COPY tsconfig.base.json ./
|
|
27
27
|
RUN pnpm build
|
package/template/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/observability",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Observability interface: error reports and spans for @clubedge/core's telemetry hooks, and an in-memory provider for tests.",
|
|
5
5
|
"private": true,
|
|
6
6
|
"license": "Apache-2.0",
|
package/template.lock.json
CHANGED