@clubedge/create-clubedge-app 0.22.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 +8 -0
- package/README.md +4 -3
- package/dist/core/deploy.js +45 -0
- package/dist/deploy.js +109 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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
|
+
|
|
5
13
|
## 0.22.0
|
|
6
14
|
|
|
7
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).
|
package/README.md
CHANGED
|
@@ -199,14 +199,14 @@ pnpm clubedge deploy --env-file .env.production # with your production provide
|
|
|
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). Then `railway up` builds the Dockerfile, Railway
|
|
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
203
|
|
|
204
204
|
## Version relationship
|
|
205
205
|
|
|
206
206
|
Each CLI release is tested against one specific Starter release:
|
|
207
207
|
|
|
208
208
|
```text
|
|
209
|
-
create-clubedge-app v0.
|
|
209
|
+
create-clubedge-app v0.23.0
|
|
210
210
|
|
|
|
211
211
|
v
|
|
212
212
|
Clubedge Starter v0.16.1
|
|
@@ -217,7 +217,7 @@ The current relationship is:
|
|
|
217
217
|
|
|
218
218
|
| CLI release | Default Starter release | Meaning |
|
|
219
219
|
| --- | --- | --- |
|
|
220
|
-
| `create-clubedge-app@0.
|
|
220
|
+
| `create-clubedge-app@0.23.0` | `clubedge-starter@v0.16.1` | The CLI's required, tested default |
|
|
221
221
|
|
|
222
222
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
223
223
|
|
|
@@ -313,6 +313,7 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
313
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` |
|
|
314
314
|
| 0.21.0 | v0.16.0 | Starter v0.16.0: upgrades delivered as pull requests, Node 24 actions; release tooling for maintainers |
|
|
315
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` |
|
|
316
317
|
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
317
318
|
|
|
318
319
|
## Community
|
package/dist/core/deploy.js
CHANGED
|
@@ -129,3 +129,48 @@ export function deploymentStatus(output, id, since) {
|
|
|
129
129
|
: deployments.find((deployment) => deployment.createdAt && new Date(deployment.createdAt).getTime() >= since.getTime() - 5_000);
|
|
130
130
|
return found ? { id: found.id, status: found.status } : null;
|
|
131
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
CHANGED
|
@@ -4,31 +4,94 @@
|
|
|
4
4
|
import { cancel, confirm, isCancel, log, note, outro, spinner } from "@clack/prompts";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
8
|
-
import {
|
|
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";
|
|
9
10
|
import process from "node:process";
|
|
10
11
|
import { UsageError } from "./cli/args.js";
|
|
11
12
|
import { bold, dim } from "./cli/ui.js";
|
|
12
|
-
import { appPort, databasesFor, deploymentStatus, exampleKeys, finalStatuses, parseDeploymentId, parseEnvFile, parseRailwayDomain, parseVariables, planVariables, railwayDatabases, serviceName, serviceNames, } from "./core/deploy.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";
|
|
13
14
|
import { readProject } from "./core/project.js";
|
|
14
15
|
import { cliCommand } from "./modify.js";
|
|
15
|
-
import {
|
|
16
|
+
import { resolvePnpm } from "./steps/install.js";
|
|
17
|
+
import { pathExists } from "./utils/fs.js";
|
|
18
|
+
import { run } from "./utils/process.js";
|
|
16
19
|
const railwayTermsUrl = "https://railway.com/legal/terms";
|
|
17
20
|
const deployDocsUrl = "https://starter.clubedge.live/docs/starter/deploy";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
*/
|
|
22
70
|
async function railwayCli() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.`);
|
|
26
88
|
}
|
|
27
89
|
async function railway(cli, args, options = {}) {
|
|
28
|
-
const [command, ...prefix] = cli;
|
|
90
|
+
const [command, ...prefix] = cli.command;
|
|
29
91
|
return await new Promise((resolvePromise, reject) => {
|
|
30
92
|
const child = spawn(command, [...prefix, ...args], {
|
|
31
|
-
shell:
|
|
93
|
+
shell: cli.shell,
|
|
94
|
+
env: { ...process.env, _: cli.executable },
|
|
32
95
|
stdio: options.inherit ? "inherit" : ["pipe", "pipe", "pipe"],
|
|
33
96
|
});
|
|
34
97
|
let stdout = "";
|
|
@@ -58,6 +121,7 @@ async function agree(args, service, databases) {
|
|
|
58
121
|
`• A project with ${databases.map((db) => railwayDatabases[db]).join(" and ")}, and a service "${service}" with a public *.up.railway.app domain.`,
|
|
59
122
|
"• The production variables Railway can provide; your provider keys come from --env-file.",
|
|
60
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.`,
|
|
61
125
|
"",
|
|
62
126
|
`Railway bills usage on your plan. Running deploy again reuses all of it. Railway's terms: ${railwayTermsUrl}`,
|
|
63
127
|
].join("\n"), "Deploy to Railway");
|
|
@@ -85,6 +149,7 @@ export async function deployProject(args) {
|
|
|
85
149
|
log.message([
|
|
86
150
|
`• Railway project "${service}" with ${databases.join(" and ")}, and the service "${service}"`,
|
|
87
151
|
`• A *.up.railway.app domain for port ${appPort}`,
|
|
152
|
+
`• ${railwayIacPath} (written if missing, with Railway's SDK added), applied with railway config apply`,
|
|
88
153
|
`• Variables: ${Object.keys(preview.set).join(", ")}`,
|
|
89
154
|
...(preview.unset.length ? [`• Not set: ${preview.unset.join(", ")}`] : []),
|
|
90
155
|
`• railway up: build, migrations, health check`,
|
|
@@ -146,7 +211,38 @@ export async function deployProject(args) {
|
|
|
146
211
|
continue;
|
|
147
212
|
await railway(rw, ["variable", "set", key, "--stdin", "--service", service, "--skip-deploys"], { input: value });
|
|
148
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"]);
|
|
149
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
|
+
}
|
|
150
246
|
// Start the deploy detached, then follow its status: `railway up` can return before the build
|
|
151
247
|
// ends, and only Railway's final status says whether the release went live.
|
|
152
248
|
const deploying = spinner();
|
package/package.json
CHANGED