@clubedge/create-clubedge-app 0.24.1 → 0.25.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 +10 -0
- package/README.md +7 -2
- package/dist/cli/ui.js +1 -1
- package/dist/core/deploy.js +46 -4
- package/dist/deploy-fly.js +167 -0
- package/dist/deploy.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.25.0
|
|
6
|
+
|
|
7
|
+
- `deploy` for Fly.io (experimental) (#71): in a project with `deploy fly`, `pnpm clubedge deploy` uses Fly's CLI and your Fly login.
|
|
8
|
+
- It creates or reuses 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`.
|
|
9
|
+
- It imports the secrets through stdin: the public URL `https://<app>.fly.dev`, a `BETTER_AUTH_SECRET` kept on later runs, and `--env-file` values; the ones still unset are listed before the build.
|
|
10
|
+
- `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.
|
|
11
|
+
- Fly's Managed Postgres (from $38/month) is a manual choice: create and attach it before deploying.
|
|
12
|
+
- Weekly Dependabot updates for the CLI's npm dependencies and GitHub Actions (#67). The Starter gets the same, without passing it on to generated projects, which receive updates as upgrade pull requests.
|
|
13
|
+
- Still on Starter `v0.17.1`.
|
|
14
|
+
|
|
5
15
|
## 0.24.1
|
|
6
16
|
|
|
7
17
|
- Default to Starter `v0.17.1` (#65), which fixes deployed Next.js apps sending sign-in email links and Stripe return URLs to `http://localhost:3000`: the Dockerfile now takes `NEXT_PUBLIC_APP_URL` as a build argument, which Railway and Render pass from the service's variables. `pnpm clubedge deploy` already sets it on Railway.
|
package/README.md
CHANGED
|
@@ -201,12 +201,16 @@ pnpm clubedge deploy --dry-run # show the plan, create nothin
|
|
|
201
201
|
|
|
202
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
|
+
### Deploy to Fly.io (experimental)
|
|
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), Upstash Redis with `cache redis`, 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
|
+
|
|
204
208
|
## Version relationship
|
|
205
209
|
|
|
206
210
|
Each CLI release is tested against one specific Starter release:
|
|
207
211
|
|
|
208
212
|
```text
|
|
209
|
-
create-clubedge-app v0.
|
|
213
|
+
create-clubedge-app v0.25.0
|
|
210
214
|
|
|
|
211
215
|
v
|
|
212
216
|
Clubedge Starter v0.17.1
|
|
@@ -217,7 +221,7 @@ The current relationship is:
|
|
|
217
221
|
|
|
218
222
|
| CLI release | Default Starter release | Meaning |
|
|
219
223
|
| --- | --- | --- |
|
|
220
|
-
| `create-clubedge-app@0.
|
|
224
|
+
| `create-clubedge-app@0.25.0` | `clubedge-starter@v0.17.1` | The CLI's required, tested default |
|
|
221
225
|
|
|
222
226
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
223
227
|
|
|
@@ -315,6 +319,7 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
315
319
|
| 0.22.0 | v0.16.1 | `pnpm clubedge deploy`: a guided first deploy to Railway; faster Docker builds |
|
|
316
320
|
| 0.23.0 | v0.16.1 | `deploy` moves Railway projects to `.railway/railway.ts` before Railway stops reading `railway.json` |
|
|
317
321
|
| 0.24.0 | v0.17.0 | Starter v0.17.0: Railway configuration as code (`.railway/railway.ts`) instead of `railway.json` |
|
|
322
|
+
| 0.25.0 | v0.17.1 | `deploy` to Fly.io (experimental); weekly Dependabot updates |
|
|
318
323
|
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
319
324
|
|
|
320
325
|
## Community
|
package/dist/cli/ui.js
CHANGED
|
@@ -127,7 +127,7 @@ ${selectionHelp(manifest)}
|
|
|
127
127
|
--no-open try: do not open the preview in the browser
|
|
128
128
|
--claim try: open the page that claims the VM, to share the preview
|
|
129
129
|
--status try: the last VM's preview, time left, and claim link
|
|
130
|
-
--env-file <path> deploy: production values
|
|
130
|
+
--env-file <path> deploy (Railway, Fly.io): production values, such as .env.production
|
|
131
131
|
-y, --yes Accept defaults and never prompt (default directory: my-app)
|
|
132
132
|
--no-install Skip dependency installation
|
|
133
133
|
--no-git Skip Git repository initialization
|
package/dist/core/deploy.js
CHANGED
|
@@ -43,14 +43,14 @@ export function parseEnvFile(text) {
|
|
|
43
43
|
* reported as unset; values the service already has count as set.
|
|
44
44
|
*/
|
|
45
45
|
export function planVariables(input) {
|
|
46
|
-
const { modules, keys, url,
|
|
46
|
+
const { modules, keys, url, envFile, secret, attached = [] } = input;
|
|
47
|
+
const existing = { ...input.existing, ...Object.fromEntries(attached.map((key) => [key, "(attached)"])) };
|
|
47
48
|
const reference = (database, variable) => `\${{${database}.${variable}}}`;
|
|
48
|
-
const
|
|
49
|
-
NODE_ENV: "production",
|
|
50
|
-
PORT: String(appPort),
|
|
49
|
+
const databases = input.databases ?? {
|
|
51
50
|
DATABASE_URL: reference(railwayDatabases.postgres, "DATABASE_URL"),
|
|
52
51
|
...(modules.cache === "redis" ? { REDIS_URL: reference(railwayDatabases.redis, "REDIS_URL") } : {}),
|
|
53
52
|
};
|
|
53
|
+
const provided = { NODE_ENV: "production", PORT: String(appPort), ...databases };
|
|
54
54
|
// The public URL under both names: TanStack Start reads APP_URL, Next.js NEXT_PUBLIC_APP_URL.
|
|
55
55
|
const set = { ...provided, APP_URL: url, NEXT_PUBLIC_APP_URL: url };
|
|
56
56
|
if (modules.auth === "better-auth" && !existing.BETTER_AUTH_SECRET)
|
|
@@ -174,3 +174,45 @@ export default defineRailway(() => {
|
|
|
174
174
|
});
|
|
175
175
|
`;
|
|
176
176
|
}
|
|
177
|
+
/* ------------------------------------ Fly.io ------------------------------------ */
|
|
178
|
+
/** The public URL Fly gives an app. */
|
|
179
|
+
export const flyAppUrl = (app) => `https://${app}.fly.dev`;
|
|
180
|
+
/** A top-level `key = "value"` in fly.toml, which `fly launch` writes (app, primary_region). */
|
|
181
|
+
export function flyTomlValue(toml, key) {
|
|
182
|
+
for (const line of toml.split(/\r?\n/)) {
|
|
183
|
+
if (/^\s*\[/.test(line))
|
|
184
|
+
return null; // top-level keys come before the first table
|
|
185
|
+
const match = new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']+)["']`).exec(line);
|
|
186
|
+
if (match)
|
|
187
|
+
return match[1];
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
/** The Redis URL in `fly redis create` or `fly redis status` output. */
|
|
192
|
+
export function parseRedisUrl(output) {
|
|
193
|
+
return /\b(rediss?:\/\/[^\s"'`]+)/.exec(output)?.[1] ?? null;
|
|
194
|
+
}
|
|
195
|
+
/** The secret names in `fly secrets list --json`: objects with Name (or name), or a plain list. */
|
|
196
|
+
export function flySecretNames(output) {
|
|
197
|
+
try {
|
|
198
|
+
const value = JSON.parse(output);
|
|
199
|
+
if (!Array.isArray(value))
|
|
200
|
+
return [];
|
|
201
|
+
return value
|
|
202
|
+
.map((item) => (typeof item === "string" ? item : (item?.Name ?? item?.name)))
|
|
203
|
+
.filter((name) => typeof name === "string");
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** `fly secrets import` input: NAME=VALUE lines. A value with a line break can't be imported this way. */
|
|
210
|
+
export function flySecretsInput(values) {
|
|
211
|
+
for (const [key, value] of Object.entries(values)) {
|
|
212
|
+
if (/[\r\n]/.test(value))
|
|
213
|
+
throw new Error(`${key} has a line break, which Fly's secrets import can't take. Set it with fly secrets set.`);
|
|
214
|
+
}
|
|
215
|
+
return Object.entries(values)
|
|
216
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
217
|
+
.join("\n");
|
|
218
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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, sets the secrets,
|
|
3
|
+
// and runs `fly deploy`, which waits for the release command (migrations) and the health checks.
|
|
4
|
+
import { cancel, confirm, isCancel, log, note, outro, spinner } from "@clack/prompts";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { UsageError } from "./cli/args.js";
|
|
10
|
+
import { bold, dim } from "./cli/ui.js";
|
|
11
|
+
import { exampleKeys, flyAppUrl, flySecretNames, flySecretsInput, flyTomlValue, parseEnvFile, parseRedisUrl, planVariables, serviceName, } from "./core/deploy.js";
|
|
12
|
+
import { spawnCommand } from "./utils/process.js";
|
|
13
|
+
const flyInstallUrl = "https://fly.io/docs/flyctl/install/";
|
|
14
|
+
const flyPricingUrl = "https://fly.io/docs/about/pricing/";
|
|
15
|
+
/** Runs Fly's CLI (a binary, so no shell); values go through stdin. */
|
|
16
|
+
async function fly(command, args, cwd, options = {}) {
|
|
17
|
+
return await new Promise((resolvePromise, reject) => {
|
|
18
|
+
const child = spawnCommand(command, args, { cwd, stdio: options.inherit ? "inherit" : ["pipe", "pipe", "pipe"] }, false);
|
|
19
|
+
let stdout = "";
|
|
20
|
+
let stderr = "";
|
|
21
|
+
child.stdout?.on("data", (chunk) => (stdout += chunk.toString()));
|
|
22
|
+
child.stderr?.on("data", (chunk) => (stderr += chunk.toString()));
|
|
23
|
+
child.once("error", reject);
|
|
24
|
+
child.once("close", (code) => {
|
|
25
|
+
if (code === 0)
|
|
26
|
+
resolvePromise(`${stdout}\n${stderr}`.trim());
|
|
27
|
+
else
|
|
28
|
+
reject(new Error(`fly ${args[0]} failed${stderr.trim() ? `: ${stderr.trim().split("\n").slice(-3).join(" ")}` : "."}`));
|
|
29
|
+
});
|
|
30
|
+
if (child.stdin) {
|
|
31
|
+
child.stdin.on("error", () => { });
|
|
32
|
+
child.stdin.end(options.input ?? "");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const attempt = async (run) => run().catch(() => null);
|
|
37
|
+
/** Fly's CLI: `fly`, or its other name `flyctl`. */
|
|
38
|
+
async function flyCli(cwd) {
|
|
39
|
+
for (const name of ["fly", "flyctl"]) {
|
|
40
|
+
if (await attempt(() => fly(name, ["version"], cwd)))
|
|
41
|
+
return name;
|
|
42
|
+
}
|
|
43
|
+
throw new UsageError(`deploy to Fly.io needs Fly's CLI. Install it (${flyInstallUrl}), then run deploy again.`);
|
|
44
|
+
}
|
|
45
|
+
export async function deployToFly(args, project, cli) {
|
|
46
|
+
const root = project.root;
|
|
47
|
+
const tomlPath = join(root, "fly.toml");
|
|
48
|
+
const toml = await readFile(tomlPath, "utf8").catch(() => "");
|
|
49
|
+
if (!toml)
|
|
50
|
+
throw new UsageError(`This project has no fly.toml. Switch with \`${cli} add deploy fly\`.`);
|
|
51
|
+
const named = flyTomlValue(toml, "app");
|
|
52
|
+
// Fly app names are global: a new app gets a short random suffix.
|
|
53
|
+
const app = named ?? `${serviceName(project.identity.packageName)}-${randomBytes(2).toString("hex")}`;
|
|
54
|
+
const redis = project.modules.cache === "redis";
|
|
55
|
+
const keys = exampleKeys(await readFile(join(root, ".env.example"), "utf8").catch(() => ""));
|
|
56
|
+
const envFile = args.envFile ? parseEnvFile(await readFile(resolve(process.cwd(), args.envFile), "utf8")) : {};
|
|
57
|
+
const secret = () => randomBytes(32).toString("hex");
|
|
58
|
+
const url = flyAppUrl(app);
|
|
59
|
+
if (args.dryRun) {
|
|
60
|
+
const preview = planVariables({ modules: project.modules, keys, url, existing: {}, envFile, secret: () => "<generated>", databases: {}, attached: ["DATABASE_URL", ...(redis ? ["REDIS_URL"] : [])] });
|
|
61
|
+
log.message([
|
|
62
|
+
`• 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${redis ? `; Upstash Redis "${app}-redis" as REDIS_URL` : ""}`,
|
|
64
|
+
`• Secrets: ${Object.keys(preview.set).join(", ")}`,
|
|
65
|
+
...(preview.unset.length ? [`• Not set: ${preview.unset.join(", ")}`] : []),
|
|
66
|
+
`• fly deploy, with NEXT_PUBLIC_APP_URL as a build argument: migrations, then health checks`,
|
|
67
|
+
].join("\n"));
|
|
68
|
+
outro("Dry run: nothing was created. Run again without --dry-run to deploy.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
note([
|
|
72
|
+
"This deploys the project to Fly.io, in your Fly account (experimental):",
|
|
73
|
+
"",
|
|
74
|
+
`• The app "${app}", a Postgres app "${app}-db" (unmanaged: one shared-cpu-1x machine and a 1 GB volume)${redis ? `, and Upstash Redis "${app}-redis"` : ""}.`,
|
|
75
|
+
"• The secrets Fly can't provide come from --env-file; the ones still unset are listed before the build.",
|
|
76
|
+
"• fly deploy builds the Dockerfile, runs the migrations as the release command, and waits for the health checks.",
|
|
77
|
+
"",
|
|
78
|
+
`Fly bills these on your plan (${flyPricingUrl}). For Fly's Managed Postgres instead, create it yourself and attach it before deploying.`,
|
|
79
|
+
].join("\n"), "Deploy to Fly.io");
|
|
80
|
+
if (!args.yes) {
|
|
81
|
+
if (!process.stdin.isTTY)
|
|
82
|
+
throw new UsageError("deploy creates resources in your Fly account. Run it in a terminal to confirm, or pass --yes.");
|
|
83
|
+
const answer = await confirm({ message: "Deploy?", initialValue: true });
|
|
84
|
+
if (isCancel(answer) || !answer) {
|
|
85
|
+
cancel("Nothing was deployed.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const progress = spinner();
|
|
90
|
+
progress.start("Finding Fly's CLI");
|
|
91
|
+
const flyBin = await flyCli(root);
|
|
92
|
+
if (!(await attempt(() => fly(flyBin, ["auth", "whoami"], root)))) {
|
|
93
|
+
progress.stop("Signing in to Fly.io");
|
|
94
|
+
if (!process.stdin.isTTY)
|
|
95
|
+
throw new UsageError("Sign in to Fly.io first with `fly auth login`, or set FLY_API_TOKEN.");
|
|
96
|
+
await fly(flyBin, ["auth", "login"], root, { inherit: true });
|
|
97
|
+
progress.start("Signed in");
|
|
98
|
+
}
|
|
99
|
+
let launched = false;
|
|
100
|
+
if (!named) {
|
|
101
|
+
progress.message(`Creating the app "${app}"`);
|
|
102
|
+
await fly(flyBin, ["launch", "--no-deploy", "--copy-config", "--name", app, "--org", "personal", "--yes", "--no-db", "--no-redis", "--no-object-storage", "--ha=false"], root);
|
|
103
|
+
launched = true;
|
|
104
|
+
}
|
|
105
|
+
const region = flyTomlValue(await readFile(tomlPath, "utf8"), "primary_region");
|
|
106
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
107
|
+
const secrets = new Set(flySecretNames((await attempt(() => fly(flyBin, ["secrets", "list", "--app", app, "--json"], root))) ?? ""));
|
|
108
|
+
const database = `${app}-db`;
|
|
109
|
+
if (!(await attempt(() => fly(flyBin, ["status", "--app", database], root)))) {
|
|
110
|
+
progress.message(`Creating Postgres "${database}"`);
|
|
111
|
+
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
|
+
}
|
|
113
|
+
if (!secrets.has("DATABASE_URL")) {
|
|
114
|
+
progress.message("Attaching Postgres (DATABASE_URL)");
|
|
115
|
+
await fly(flyBin, ["postgres", "attach", database, "--app", app, "--yes"], root);
|
|
116
|
+
}
|
|
117
|
+
let redisUrl = null;
|
|
118
|
+
if (redis && !secrets.has("REDIS_URL")) {
|
|
119
|
+
progress.message(`Creating Upstash Redis "${app}-redis"`);
|
|
120
|
+
redisUrl = parseRedisUrl(await fly(flyBin, ["redis", "create", "--name", `${app}-redis`, "--org", "personal", ...regionArgs, "--no-replicas", "--enable-eviction"], root));
|
|
121
|
+
}
|
|
122
|
+
progress.message("Setting the secrets");
|
|
123
|
+
const plan = planVariables({
|
|
124
|
+
modules: project.modules,
|
|
125
|
+
keys,
|
|
126
|
+
url,
|
|
127
|
+
existing: Object.fromEntries([...secrets].map((name) => [name, "(set)"])),
|
|
128
|
+
envFile,
|
|
129
|
+
secret,
|
|
130
|
+
databases: redisUrl ? { REDIS_URL: redisUrl } : {},
|
|
131
|
+
attached: ["DATABASE_URL", ...(redis && !redisUrl && secrets.has("REDIS_URL") ? ["REDIS_URL"] : [])],
|
|
132
|
+
});
|
|
133
|
+
// The image sets NODE_ENV and fly.toml the port: the rest are secrets, staged for the deploy.
|
|
134
|
+
const { NODE_ENV: _node, PORT: _port, ...values } = plan.set;
|
|
135
|
+
await fly(flyBin, ["secrets", "import", "--app", app, "--stage"], root, { input: flySecretsInput(values) });
|
|
136
|
+
progress.stop(`Fly.io is ready: ${url}`);
|
|
137
|
+
if (launched)
|
|
138
|
+
log.info(`Commit fly.toml: fly launch wrote the app's name ("${app}") and region into it.`);
|
|
139
|
+
if (plan.unset.length) {
|
|
140
|
+
log.warn(`Not set: ${plan.unset.join(", ")}. The app starts without them, but the features that use them fail until they are set: pass --env-file, or run fly secrets set.`);
|
|
141
|
+
if (process.stdin.isTTY && !args.yes) {
|
|
142
|
+
const proceed = await confirm({ message: "Deploy anyway?", initialValue: true });
|
|
143
|
+
if (isCancel(proceed) || !proceed) {
|
|
144
|
+
cancel("Nothing was deployed. Set them, then run deploy again: it reuses everything.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
log.step("Deploying with fly deploy (build logs follow; the migrations run as the release command)");
|
|
150
|
+
try {
|
|
151
|
+
// Next.js compiles the public URL into the build, so it goes in as a build argument too.
|
|
152
|
+
await fly(flyBin, ["deploy", "--app", app, "--remote-only", "--build-arg", `NEXT_PUBLIC_APP_URL=${url}`], root, { inherit: true });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw new Error(`The deploy did not finish. Read the logs with \`fly logs --app ${app}\`, fix the cause, and run deploy again: it reuses what exists.`);
|
|
156
|
+
}
|
|
157
|
+
note([
|
|
158
|
+
`${bold("URL")} ${url}`,
|
|
159
|
+
`${bold("App")} ${app}`,
|
|
160
|
+
...(plan.unset.length ? ["", `${bold("Not set")} ${plan.unset.join(", ")}`] : []),
|
|
161
|
+
].join("\n"), "Deployed to Fly.io");
|
|
162
|
+
outro([
|
|
163
|
+
`${dim("Logs")} fly logs --app ${app}`,
|
|
164
|
+
`${dim("Status")} fly status --app ${app}`,
|
|
165
|
+
`${dim("Again")} ${cli} deploy ${dim("(reuses the app; --env-file to add secrets)")}`,
|
|
166
|
+
].join("\n"));
|
|
167
|
+
}
|
package/dist/deploy.js
CHANGED
|
@@ -12,6 +12,7 @@ import { UsageError } from "./cli/args.js";
|
|
|
12
12
|
import { bold, dim } from "./cli/ui.js";
|
|
13
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
14
|
import { readProject } from "./core/project.js";
|
|
15
|
+
import { deployToFly } from "./deploy-fly.js";
|
|
15
16
|
import { cliCommand } from "./modify.js";
|
|
16
17
|
import { resolvePnpm } from "./steps/install.js";
|
|
17
18
|
import { pathExists } from "./utils/fs.js";
|
|
@@ -132,8 +133,10 @@ export async function deployProject(args) {
|
|
|
132
133
|
const project = await readProject(process.cwd());
|
|
133
134
|
const cli = await cliCommand(project);
|
|
134
135
|
const platform = project.modules.deploy ?? "docker";
|
|
136
|
+
if (platform === "fly")
|
|
137
|
+
return await deployToFly(args, project, cli);
|
|
135
138
|
if (platform !== "railway") {
|
|
136
|
-
throw new UsageError(`deploy sets up Railway for now, and this project deploys with "${platform}". Switch with \`${cli} add deploy railway
|
|
139
|
+
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}.`);
|
|
137
140
|
}
|
|
138
141
|
const service = serviceName(project.identity.packageName);
|
|
139
142
|
const databases = databasesFor(project.modules);
|
package/package.json
CHANGED