@open-mercato/cli 0.6.4-develop.4382.1.6b4f656b77 → 0.6.4
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/.turbo/turbo-build.log +1 -1
- package/dist/agentic/guides/core.customers.md +1 -1
- package/dist/agentic/shared/AGENTS.md.template +56 -33
- package/dist/agentic/shared/ai/skills/{auto-continue-pr → om-auto-continue-pr}/SKILL.md +19 -19
- package/dist/agentic/shared/ai/skills/{auto-create-pr → om-auto-continue-pr}/STANDALONE.md +1 -1
- package/dist/agentic/shared/ai/skills/{auto-continue-pr-loop → om-auto-continue-pr-loop}/SKILL.md +32 -32
- package/dist/agentic/shared/ai/skills/{auto-create-pr → om-auto-create-pr}/SKILL.md +24 -24
- package/dist/agentic/shared/ai/skills/{auto-fix-github → om-auto-create-pr}/STANDALONE.md +1 -1
- package/dist/agentic/shared/ai/skills/{auto-create-pr-loop → om-auto-create-pr-loop}/SKILL.md +40 -40
- package/dist/agentic/shared/ai/skills/{auto-fix-github → om-auto-fix-github}/SKILL.md +3 -3
- package/dist/agentic/shared/ai/skills/{auto-continue-pr → om-auto-fix-github}/STANDALONE.md +1 -1
- package/dist/agentic/shared/ai/skills/{auto-review-pr → om-auto-review-pr}/SKILL.md +5 -5
- package/dist/agentic/shared/ai/skills/{auto-review-pr → om-auto-review-pr}/STANDALONE.md +1 -1
- package/dist/agentic/shared/ai/skills/{auto-upgrade-0.4.10-to-0.5.0 → om-auto-upgrade-0.4.10-to-0.5.0}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{backend-ui-design → om-backend-ui-design}/SKILL.md +2 -2
- package/dist/agentic/shared/ai/skills/{code-review → om-code-review}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{code-review → om-code-review}/references/review-checklist.md +3 -0
- package/dist/agentic/shared/ai/skills/{data-model-design → om-data-model-design}/SKILL.md +4 -2
- package/dist/agentic/shared/ai/skills/{eject-and-customize → om-eject-and-customize}/SKILL.md +2 -2
- package/dist/agentic/shared/ai/skills/{implement-spec → om-implement-spec}/SKILL.md +6 -6
- package/dist/agentic/shared/ai/skills/{integration-builder → om-integration-builder}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{integration-tests → om-integration-tests}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/SKILL.md +5 -3
- package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/references/naming-conventions.md +7 -8
- package/dist/agentic/shared/ai/skills/om-prepare-issue/SKILL.md +202 -0
- package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/SKILL.md +6 -6
- package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/references/spec-checklist.md +3 -3
- package/dist/agentic/shared/ai/skills/{system-extension → om-system-extension}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{trim-unused-modules → om-trim-unused-modules}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{troubleshooter → om-troubleshooter}/SKILL.md +1 -1
- package/dist/bin.js +1 -0
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +1 -1
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-setup.js +52 -51
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.js +7 -0
- package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.js.map +7 -0
- package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.js +63 -0
- package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.js.map +7 -0
- package/dist/lib/deploy/railway/env.js +98 -0
- package/dist/lib/deploy/railway/env.js.map +7 -0
- package/dist/lib/deploy/railway/graphql-client.js +99 -0
- package/dist/lib/deploy/railway/graphql-client.js.map +7 -0
- package/dist/lib/deploy/railway/index.js +1115 -0
- package/dist/lib/deploy/railway/index.js.map +7 -0
- package/dist/lib/deploy/railway/operations.js +249 -0
- package/dist/lib/deploy/railway/operations.js.map +7 -0
- package/dist/lib/deploy/railway/options.js +155 -0
- package/dist/lib/deploy/railway/options.js.map +7 -0
- package/dist/lib/deploy/railway/railway-cli.js +61 -0
- package/dist/lib/deploy/railway/railway-cli.js.map +7 -0
- package/dist/lib/deploy/railway/redaction.js +88 -0
- package/dist/lib/deploy/railway/redaction.js.map +7 -0
- package/dist/lib/deploy/railway/source.js +128 -0
- package/dist/lib/deploy/railway/source.js.map +7 -0
- package/dist/lib/deploy/railway/state.js +41 -0
- package/dist/lib/deploy/railway/state.js.map +7 -0
- package/dist/lib/deploy/railway/token.js +46 -0
- package/dist/lib/deploy/railway/token.js.map +7 -0
- package/dist/lib/deploy/railway/types.js +1 -0
- package/dist/lib/deploy/railway/types.js.map +7 -0
- package/dist/lib/generators/openapi.js +4 -0
- package/dist/lib/generators/openapi.js.map +2 -2
- package/dist/lib/testing/integration.js +57 -26
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/lib/testing/runtime-utils.js +16 -8
- package/dist/lib/testing/runtime-utils.js.map +2 -2
- package/dist/mercato.js +23 -8
- package/dist/mercato.js.map +2 -2
- package/package.json +11 -12
- package/src/bin.ts +1 -0
- package/src/lib/__integration__/TC-INT-008.spec.ts +1 -1
- package/src/lib/agentic-setup.ts +52 -51
- package/src/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.ts +3 -0
- package/src/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.ts +71 -0
- package/src/lib/deploy/railway/__tests__/dry-run.test.ts +61 -0
- package/src/lib/deploy/railway/__tests__/env.test.ts +55 -0
- package/src/lib/deploy/railway/__tests__/graphql-client.test.ts +80 -0
- package/src/lib/deploy/railway/__tests__/operations.test.ts +21 -0
- package/src/lib/deploy/railway/__tests__/options.test.ts +78 -0
- package/src/lib/deploy/railway/__tests__/redaction.test.ts +62 -0
- package/src/lib/deploy/railway/__tests__/source.test.ts +130 -0
- package/src/lib/deploy/railway/__tests__/state.test.ts +24 -0
- package/src/lib/deploy/railway/__tests__/token.test.ts +39 -0
- package/src/lib/deploy/railway/env.ts +112 -0
- package/src/lib/deploy/railway/graphql-client.ts +128 -0
- package/src/lib/deploy/railway/index.ts +1454 -0
- package/src/lib/deploy/railway/operations.ts +248 -0
- package/src/lib/deploy/railway/options.ts +167 -0
- package/src/lib/deploy/railway/railway-cli.ts +65 -0
- package/src/lib/deploy/railway/redaction.ts +91 -0
- package/src/lib/deploy/railway/source.ts +143 -0
- package/src/lib/deploy/railway/state.ts +45 -0
- package/src/lib/deploy/railway/token.ts +49 -0
- package/src/lib/deploy/railway/types.ts +87 -0
- package/src/lib/generators/openapi.ts +3 -0
- package/src/lib/testing/__tests__/integration.test.ts +79 -0
- package/src/lib/testing/__tests__/runtime-utils.test.ts +45 -1
- package/src/lib/testing/integration.ts +74 -28
- package/src/lib/testing/runtime-utils.ts +17 -8
- package/src/mercato.ts +33 -8
- /package/dist/agentic/shared/ai/skills/{backend-ui-design → om-backend-ui-design}/references/ui-components.md +0 -0
- /package/dist/agentic/shared/ai/skills/{data-model-design → om-data-model-design}/references/mikro-orm-cheatsheet.md +0 -0
- /package/dist/agentic/shared/ai/skills/{integration-builder → om-integration-builder}/references/adapter-contracts.md +0 -0
- /package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/references/navigation-patterns.md +0 -0
- /package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/references/spec-template.md +0 -0
- /package/dist/agentic/shared/ai/skills/{system-extension → om-system-extension}/references/extension-contracts.md +0 -0
- /package/dist/agentic/shared/ai/skills/{troubleshooter → om-troubleshooter}/references/diagnostic-commands.md +0 -0
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
+
import { satisfies } from "semver";
|
|
6
|
+
import {
|
|
7
|
+
computeRailwayVariables,
|
|
8
|
+
formatVariablePlan,
|
|
9
|
+
generateProtectedSecrets,
|
|
10
|
+
parseEnvFile,
|
|
11
|
+
resolveEnvFile,
|
|
12
|
+
writeGeneratedSecrets
|
|
13
|
+
} from "./env.js";
|
|
14
|
+
import { createRailwayGraphqlClient, RailwayGraphqlError } from "./graphql-client.js";
|
|
15
|
+
import { railwayOperations } from "./operations.js";
|
|
16
|
+
import { parseRailwayDeployOptions, railwayDeployHelp } from "./options.js";
|
|
17
|
+
import { collectSensitiveStructuredValues, redactText } from "./redaction.js";
|
|
18
|
+
import { uploadLocalSource } from "./railway-cli.js";
|
|
19
|
+
import {
|
|
20
|
+
assertLocalUploadSafe,
|
|
21
|
+
resolveRailwaySource
|
|
22
|
+
} from "./source.js";
|
|
23
|
+
import {
|
|
24
|
+
createRailwayState,
|
|
25
|
+
loadRailwayState,
|
|
26
|
+
railwayStatePath,
|
|
27
|
+
saveRailwayState
|
|
28
|
+
} from "./state.js";
|
|
29
|
+
import {
|
|
30
|
+
railwayTokenConfigPath,
|
|
31
|
+
resolveRailwayToken,
|
|
32
|
+
writeCachedRailwayToken
|
|
33
|
+
} from "./token.js";
|
|
34
|
+
const SUCCESS_STATUSES = /* @__PURE__ */ new Set(["SUCCESS", "COMPLETED"]);
|
|
35
|
+
const FAILURE_STATUSES = /* @__PURE__ */ new Set(["FAILED", "CRASHED", "REMOVED"]);
|
|
36
|
+
function isNotFoundError(error) {
|
|
37
|
+
return error instanceof RailwayGraphqlError && (error.status === 404 || error.code === "NOT_FOUND" || error.code?.endsWith("_NOT_FOUND") === true);
|
|
38
|
+
}
|
|
39
|
+
function isAmbiguousMutationError(error) {
|
|
40
|
+
if (!(error instanceof RailwayGraphqlError)) return true;
|
|
41
|
+
return error.status === void 0 || error.status >= 500 || error.code === "INTERNAL_ERROR";
|
|
42
|
+
}
|
|
43
|
+
function createPrompt() {
|
|
44
|
+
const readline = createInterface({ input, output });
|
|
45
|
+
return {
|
|
46
|
+
ask: (question) => readline.question(question),
|
|
47
|
+
close: () => readline.close()
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function nodes(connection) {
|
|
51
|
+
return (connection?.edges ?? []).map((edge) => edge?.node).filter((node) => node !== null && node !== void 0);
|
|
52
|
+
}
|
|
53
|
+
function wait(milliseconds) {
|
|
54
|
+
return new Promise((resolveWait) => setTimeout(resolveWait, milliseconds));
|
|
55
|
+
}
|
|
56
|
+
function normalizeProjectName(value) {
|
|
57
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 48).replace(/-+$/g, "");
|
|
58
|
+
if (!normalized) throw new Error("Unable to derive a valid Railway project name.");
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
function readAppPackage(cwd) {
|
|
62
|
+
const packagePath = resolve(cwd, "package.json");
|
|
63
|
+
if (!existsSync(packagePath)) throw new Error("Run this command from an Open Mercato app root.");
|
|
64
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
65
|
+
if (!parsed || typeof parsed !== "object") throw new Error("Invalid package.json.");
|
|
66
|
+
const record = parsed;
|
|
67
|
+
const dependencies = {
|
|
68
|
+
...record.dependencies && typeof record.dependencies === "object" ? record.dependencies : {},
|
|
69
|
+
...record.devDependencies && typeof record.devDependencies === "object" ? record.devDependencies : {}
|
|
70
|
+
};
|
|
71
|
+
if (!Object.keys(dependencies).some((key) => key.startsWith("@open-mercato/"))) {
|
|
72
|
+
throw new Error("The current package does not declare Open Mercato dependencies.");
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: typeof record.name === "string" && record.name.trim() ? record.name : basename(cwd),
|
|
76
|
+
dependencies,
|
|
77
|
+
nodeEngine: record.engines && typeof record.engines === "object" && typeof record.engines.node === "string" ? record.engines.node : void 0
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function assertSupportedNodeVersion(nodeEngine) {
|
|
81
|
+
if (!nodeEngine) return;
|
|
82
|
+
if (!satisfies(process.version, nodeEngine)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Node ${process.version} does not satisfy this app's required version "${nodeEngine}".`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function ensureEnvironmentState(state, name) {
|
|
89
|
+
state.environments[name] ??= {};
|
|
90
|
+
return state.environments[name];
|
|
91
|
+
}
|
|
92
|
+
function persistState(path, state) {
|
|
93
|
+
saveRailwayState(path, state);
|
|
94
|
+
}
|
|
95
|
+
async function resolveTokenInteractively(options, prompt) {
|
|
96
|
+
const configPath = railwayTokenConfigPath();
|
|
97
|
+
const resolved = resolveRailwayToken({ flagToken: options.token, configPath });
|
|
98
|
+
if (resolved.token) return { token: resolved.token, cached: resolved.source === "cache" };
|
|
99
|
+
if (options.nonInteractive || !prompt) {
|
|
100
|
+
const compatibilityHint = process.env.RAILWAY_TOKEN ? " RAILWAY_TOKEN is project-scoped; use RAILWAY_API_TOKEN for account/workspace operations." : "";
|
|
101
|
+
throw new Error(`Railway Account token is required.${compatibilityHint}`);
|
|
102
|
+
}
|
|
103
|
+
console.log("Create a token at https://railway.com/account/tokens (Account Settings -> Tokens).");
|
|
104
|
+
const token = (await prompt.ask("Railway Account token: ")).trim();
|
|
105
|
+
if (!token) throw new Error("Railway Account token is required.");
|
|
106
|
+
const persist = (await prompt.ask("Cache this token in ~/.config/open-mercato/railway.json? [y/N] ")).trim().toLowerCase();
|
|
107
|
+
if (persist === "y" || persist === "yes") {
|
|
108
|
+
writeCachedRailwayToken(configPath, token);
|
|
109
|
+
return { token, cached: true };
|
|
110
|
+
}
|
|
111
|
+
return { token, cached: false };
|
|
112
|
+
}
|
|
113
|
+
async function resolveWorkspace(client, preferredWorkspaceId) {
|
|
114
|
+
let data;
|
|
115
|
+
try {
|
|
116
|
+
data = await client.request(railwayOperations.me);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error instanceof RailwayGraphqlError && (error.status === 401 || error.status === 403 || error.code === "UNAUTHORIZED")) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"Token rejected. Generate a Railway Account token at https://railway.com/account/tokens."
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
const workspaces = data.me.workspaces ?? [];
|
|
126
|
+
const workspace = preferredWorkspaceId ? workspaces.find((candidate) => candidate.id === preferredWorkspaceId) : workspaces[0];
|
|
127
|
+
if (preferredWorkspaceId && !workspace) {
|
|
128
|
+
throw new Error("The recorded Railway workspace is no longer accessible to this Account token.");
|
|
129
|
+
}
|
|
130
|
+
if (!workspace) throw new Error("The Railway account has no accessible workspace.");
|
|
131
|
+
return { userId: data.me.id, workspaceId: workspace.id };
|
|
132
|
+
}
|
|
133
|
+
async function findProjectByName(client, workspaceId, projectName) {
|
|
134
|
+
const data = await client.request(railwayOperations.projects, { workspaceId });
|
|
135
|
+
return nodes(data.projects).find((project) => project.name === projectName) ?? null;
|
|
136
|
+
}
|
|
137
|
+
async function ensureProject(input2) {
|
|
138
|
+
if (input2.state.projectId) {
|
|
139
|
+
try {
|
|
140
|
+
const data = await input2.client.request(
|
|
141
|
+
railwayOperations.project,
|
|
142
|
+
{ id: input2.state.projectId }
|
|
143
|
+
);
|
|
144
|
+
if (data.project.workspaceId && data.project.workspaceId !== input2.workspaceId) {
|
|
145
|
+
throw new Error("Recorded Railway project belongs to a different workspace.");
|
|
146
|
+
}
|
|
147
|
+
if (data.project.name !== input2.state.projectName) {
|
|
148
|
+
if (!input2.options.forceRename) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Recorded Railway project is named "${data.project.name}". Use --force-rename to rename it.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
await input2.client.request(railwayOperations.projectUpdate, {
|
|
154
|
+
id: data.project.id,
|
|
155
|
+
input: { name: input2.state.projectName }
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return data.project.id;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!isNotFoundError(error)) throw error;
|
|
161
|
+
input2.state.projectId = void 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const existing = await findProjectByName(
|
|
165
|
+
input2.client,
|
|
166
|
+
input2.workspaceId,
|
|
167
|
+
input2.state.projectName
|
|
168
|
+
);
|
|
169
|
+
if (existing) {
|
|
170
|
+
input2.state.projectId = existing.id;
|
|
171
|
+
persistState(input2.statePath, input2.state);
|
|
172
|
+
return existing.id;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const data = await input2.client.request(railwayOperations.projectCreate, {
|
|
176
|
+
input: {
|
|
177
|
+
name: input2.state.projectName,
|
|
178
|
+
description: "Open Mercato deployment managed by mercato deploy railway",
|
|
179
|
+
workspaceId: input2.workspaceId
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
input2.state.projectId = data.projectCreate.id;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
const discovered = await findProjectByName(
|
|
185
|
+
input2.client,
|
|
186
|
+
input2.workspaceId,
|
|
187
|
+
input2.state.projectName
|
|
188
|
+
);
|
|
189
|
+
if (!discovered) throw error;
|
|
190
|
+
input2.state.projectId = discovered.id;
|
|
191
|
+
}
|
|
192
|
+
persistState(input2.statePath, input2.state);
|
|
193
|
+
return input2.state.projectId;
|
|
194
|
+
}
|
|
195
|
+
async function ensureEnvironment(input2) {
|
|
196
|
+
if (input2.environmentState.environmentId) {
|
|
197
|
+
try {
|
|
198
|
+
const data2 = await input2.client.request(
|
|
199
|
+
railwayOperations.environment,
|
|
200
|
+
{ id: input2.environmentState.environmentId, projectId: input2.projectId }
|
|
201
|
+
);
|
|
202
|
+
return data2.environment.id;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (!isNotFoundError(error)) throw error;
|
|
205
|
+
input2.environmentState.environmentId = void 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const data = await input2.client.request(railwayOperations.environments, { projectId: input2.projectId });
|
|
209
|
+
const existing = nodes(data.environments).find((environment) => environment.name === input2.name);
|
|
210
|
+
if (existing) {
|
|
211
|
+
input2.environmentState.environmentId = existing.id;
|
|
212
|
+
input2.persist();
|
|
213
|
+
return existing.id;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const created = await input2.client.request(
|
|
217
|
+
railwayOperations.environmentCreate,
|
|
218
|
+
{ input: { projectId: input2.projectId, name: input2.name } }
|
|
219
|
+
);
|
|
220
|
+
input2.environmentState.environmentId = created.environmentCreate.id;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const retryLookup = await input2.client.request(railwayOperations.environments, { projectId: input2.projectId });
|
|
223
|
+
const discovered = nodes(retryLookup.environments).find((environment) => environment.name === input2.name);
|
|
224
|
+
if (!discovered) throw error;
|
|
225
|
+
input2.environmentState.environmentId = discovered.id;
|
|
226
|
+
}
|
|
227
|
+
input2.persist();
|
|
228
|
+
return input2.environmentState.environmentId;
|
|
229
|
+
}
|
|
230
|
+
async function listServices(client, projectId) {
|
|
231
|
+
const data = await client.request(railwayOperations.services, { projectId });
|
|
232
|
+
return nodes(data.project.services);
|
|
233
|
+
}
|
|
234
|
+
async function waitForService(client, projectId, name, timeoutMs = 18e4) {
|
|
235
|
+
const deadline = Date.now() + timeoutMs;
|
|
236
|
+
while (Date.now() < deadline) {
|
|
237
|
+
const service = (await listServices(client, projectId)).find((candidate) => candidate.name === name);
|
|
238
|
+
if (service) return service;
|
|
239
|
+
await wait(5e3);
|
|
240
|
+
}
|
|
241
|
+
throw new Error(`Timed out waiting for Railway service "${name}". Re-run the command to resume.`);
|
|
242
|
+
}
|
|
243
|
+
async function ensureDatabaseReady(input2) {
|
|
244
|
+
const previous = await latestDeployment(
|
|
245
|
+
input2.client,
|
|
246
|
+
input2.projectId,
|
|
247
|
+
input2.environmentId,
|
|
248
|
+
input2.serviceId
|
|
249
|
+
);
|
|
250
|
+
if (previous && SUCCESS_STATUSES.has(previous.status)) return;
|
|
251
|
+
if (previous && !FAILURE_STATUSES.has(previous.status)) {
|
|
252
|
+
await waitForDeployment({
|
|
253
|
+
client: input2.client,
|
|
254
|
+
deploymentId: previous.id,
|
|
255
|
+
timeoutSeconds: 180,
|
|
256
|
+
prefix: input2.name,
|
|
257
|
+
secrets: []
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
let triggerError;
|
|
262
|
+
try {
|
|
263
|
+
await input2.client.request(railwayOperations.deploy, {
|
|
264
|
+
environmentId: input2.environmentId,
|
|
265
|
+
serviceId: input2.serviceId,
|
|
266
|
+
commitSha: null
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (!isAmbiguousMutationError(error)) throw error;
|
|
270
|
+
triggerError = error;
|
|
271
|
+
}
|
|
272
|
+
const deadline = Date.now() + 3e4;
|
|
273
|
+
while (Date.now() < deadline) {
|
|
274
|
+
const current = await latestDeployment(
|
|
275
|
+
input2.client,
|
|
276
|
+
input2.projectId,
|
|
277
|
+
input2.environmentId,
|
|
278
|
+
input2.serviceId
|
|
279
|
+
);
|
|
280
|
+
if (current && current.id !== previous?.id) {
|
|
281
|
+
await waitForDeployment({
|
|
282
|
+
client: input2.client,
|
|
283
|
+
deploymentId: current.id,
|
|
284
|
+
timeoutSeconds: 180,
|
|
285
|
+
prefix: input2.name,
|
|
286
|
+
secrets: []
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
await wait(2e3);
|
|
291
|
+
}
|
|
292
|
+
if (triggerError) throw triggerError;
|
|
293
|
+
throw new Error(`Railway did not expose a ${input2.name} deployment in this environment.`);
|
|
294
|
+
}
|
|
295
|
+
async function ensureDatabase(input2) {
|
|
296
|
+
if (input2.currentId) {
|
|
297
|
+
try {
|
|
298
|
+
const data = await input2.client.request(
|
|
299
|
+
railwayOperations.service,
|
|
300
|
+
{ id: input2.currentId }
|
|
301
|
+
);
|
|
302
|
+
if (data.service.projectId !== input2.projectId) {
|
|
303
|
+
throw new Error(`Recorded ${input2.name} service belongs to a different Railway project.`);
|
|
304
|
+
}
|
|
305
|
+
await ensureDatabaseReady({
|
|
306
|
+
client: input2.client,
|
|
307
|
+
projectId: input2.projectId,
|
|
308
|
+
environmentId: input2.environmentId,
|
|
309
|
+
serviceId: data.service.id,
|
|
310
|
+
name: input2.name
|
|
311
|
+
});
|
|
312
|
+
return data.service.id;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (!isNotFoundError(error)) throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const existing = (await listServices(input2.client, input2.projectId)).find((service) => service.name === input2.name);
|
|
318
|
+
if (existing) {
|
|
319
|
+
await ensureDatabaseReady({
|
|
320
|
+
client: input2.client,
|
|
321
|
+
projectId: input2.projectId,
|
|
322
|
+
environmentId: input2.environmentId,
|
|
323
|
+
serviceId: existing.id,
|
|
324
|
+
name: input2.name
|
|
325
|
+
});
|
|
326
|
+
input2.onResolved(existing.id);
|
|
327
|
+
return existing.id;
|
|
328
|
+
}
|
|
329
|
+
const templateData = await input2.client.request(railwayOperations.template, { code: input2.code });
|
|
330
|
+
if (!templateData.template.serializedConfig) {
|
|
331
|
+
throw new Error(`Railway template "${input2.code}" does not expose a deployable configuration.`);
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await input2.client.request(railwayOperations.templateDeploy, {
|
|
335
|
+
input: {
|
|
336
|
+
projectId: input2.projectId,
|
|
337
|
+
environmentId: input2.environmentId,
|
|
338
|
+
workspaceId: input2.workspaceId,
|
|
339
|
+
templateId: templateData.template.id,
|
|
340
|
+
serializedConfig: templateData.template.serializedConfig
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const discovered = (await listServices(input2.client, input2.projectId)).find((service) => service.name === input2.name);
|
|
345
|
+
if (!discovered) throw error;
|
|
346
|
+
}
|
|
347
|
+
const created = await waitForService(input2.client, input2.projectId, input2.name);
|
|
348
|
+
const readinessDeadline = Date.now() + 18e4;
|
|
349
|
+
let ready = false;
|
|
350
|
+
while (Date.now() < readinessDeadline) {
|
|
351
|
+
const deployment = await latestDeployment(
|
|
352
|
+
input2.client,
|
|
353
|
+
input2.projectId,
|
|
354
|
+
input2.environmentId,
|
|
355
|
+
created.id
|
|
356
|
+
);
|
|
357
|
+
if (deployment && SUCCESS_STATUSES.has(deployment.status)) {
|
|
358
|
+
ready = true;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
if (deployment && FAILURE_STATUSES.has(deployment.status)) {
|
|
362
|
+
throw new Error(`${input2.name} provisioning failed with status ${deployment.status}.`);
|
|
363
|
+
}
|
|
364
|
+
await wait(5e3);
|
|
365
|
+
}
|
|
366
|
+
if (!ready) {
|
|
367
|
+
throw new Error(`Timed out waiting for ${input2.name} to become ready. Re-run the command to resume.`);
|
|
368
|
+
}
|
|
369
|
+
input2.onResolved(created.id);
|
|
370
|
+
return created.id;
|
|
371
|
+
}
|
|
372
|
+
async function ensureService(input2) {
|
|
373
|
+
let serviceId;
|
|
374
|
+
let createdService = false;
|
|
375
|
+
if (input2.currentId) {
|
|
376
|
+
try {
|
|
377
|
+
const data = await input2.client.request(
|
|
378
|
+
railwayOperations.service,
|
|
379
|
+
{ id: input2.currentId }
|
|
380
|
+
);
|
|
381
|
+
if (data.service.projectId !== input2.projectId) {
|
|
382
|
+
throw new Error(`Recorded ${input2.name} service belongs to a different Railway project.`);
|
|
383
|
+
}
|
|
384
|
+
serviceId = data.service.id;
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (!isNotFoundError(error)) throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!serviceId) {
|
|
390
|
+
const existing = (await listServices(input2.client, input2.projectId)).find((service) => service.name === input2.name);
|
|
391
|
+
serviceId = existing?.id;
|
|
392
|
+
}
|
|
393
|
+
if (!serviceId) {
|
|
394
|
+
const serviceInput = {
|
|
395
|
+
projectId: input2.projectId,
|
|
396
|
+
name: input2.name
|
|
397
|
+
};
|
|
398
|
+
if (input2.source.mode === "git") {
|
|
399
|
+
serviceInput.source = { repo: input2.source.repo };
|
|
400
|
+
serviceInput.branch = input2.source.branch;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const created = await input2.client.request(
|
|
404
|
+
railwayOperations.serviceCreate,
|
|
405
|
+
{ input: serviceInput }
|
|
406
|
+
);
|
|
407
|
+
serviceId = created.serviceCreate.id;
|
|
408
|
+
createdService = true;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
const discovered = (await listServices(input2.client, input2.projectId)).find((service) => service.name === input2.name);
|
|
411
|
+
if (!discovered) throw error;
|
|
412
|
+
serviceId = discovered.id;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (input2.source.mode === "git" && !createdService) {
|
|
416
|
+
await input2.client.request(railwayOperations.serviceConnect, {
|
|
417
|
+
id: serviceId,
|
|
418
|
+
input: {
|
|
419
|
+
repo: input2.source.repo,
|
|
420
|
+
branch: input2.source.branch
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
} else if (input2.source.mode === "local" && input2.previousSource?.mode === "git") {
|
|
424
|
+
await input2.client.request(railwayOperations.serviceDisconnect, { id: serviceId });
|
|
425
|
+
}
|
|
426
|
+
if (input2.startCommand || input2.region || input2.railwayConfigFile) {
|
|
427
|
+
await input2.client.request(railwayOperations.serviceInstanceUpdate, {
|
|
428
|
+
environmentId: input2.environmentId,
|
|
429
|
+
serviceId,
|
|
430
|
+
input: {
|
|
431
|
+
...input2.startCommand ? { startCommand: input2.startCommand } : {},
|
|
432
|
+
...input2.region ? { region: input2.region } : {},
|
|
433
|
+
...input2.railwayConfigFile ? { railwayConfigFile: input2.railwayConfigFile } : {}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
input2.onResolved(serviceId);
|
|
438
|
+
return serviceId;
|
|
439
|
+
}
|
|
440
|
+
async function removeDisabledWorker(input2) {
|
|
441
|
+
if (!input2.workerServiceId) return;
|
|
442
|
+
try {
|
|
443
|
+
const data = await input2.client.request(railwayOperations.service, { id: input2.workerServiceId });
|
|
444
|
+
if (data.service.projectId !== input2.projectId || data.service.name !== "mercato-worker") {
|
|
445
|
+
throw new Error("Recorded worker service does not match the managed Railway project.");
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (isNotFoundError(error)) return;
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
await input2.client.request(railwayOperations.serviceDelete, {
|
|
453
|
+
id: input2.workerServiceId,
|
|
454
|
+
environmentId: input2.environmentId
|
|
455
|
+
});
|
|
456
|
+
} catch (error) {
|
|
457
|
+
try {
|
|
458
|
+
await input2.client.request(railwayOperations.service, { id: input2.workerServiceId });
|
|
459
|
+
} catch (lookupError) {
|
|
460
|
+
if (isNotFoundError(lookupError)) return;
|
|
461
|
+
throw lookupError;
|
|
462
|
+
}
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function upsertVariables(input2) {
|
|
467
|
+
await input2.client.request(railwayOperations.variableUpsert, {
|
|
468
|
+
input: {
|
|
469
|
+
projectId: input2.projectId,
|
|
470
|
+
environmentId: input2.environmentId,
|
|
471
|
+
serviceId: input2.serviceId,
|
|
472
|
+
variables: input2.variables,
|
|
473
|
+
replace: false,
|
|
474
|
+
skipDeploys: true
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
async function loadVariables(input2) {
|
|
479
|
+
const data = await input2.client.request(
|
|
480
|
+
railwayOperations.variables,
|
|
481
|
+
{
|
|
482
|
+
projectId: input2.projectId,
|
|
483
|
+
environmentId: input2.environmentId,
|
|
484
|
+
serviceId: input2.serviceId
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
if (!data.variables || typeof data.variables !== "object" || Array.isArray(data.variables)) return {};
|
|
488
|
+
return Object.fromEntries(
|
|
489
|
+
Object.entries(data.variables).filter((entry) => typeof entry[1] === "string")
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
async function latestDeployment(client, projectId, environmentId, serviceId) {
|
|
493
|
+
const data = await client.request(railwayOperations.deployments, { projectId, environmentId, serviceId });
|
|
494
|
+
return nodes(data.deployments)[0] ?? null;
|
|
495
|
+
}
|
|
496
|
+
async function printDeploymentLogs(client, deploymentId, prefix, secrets) {
|
|
497
|
+
for (const operation of [railwayOperations.buildLogs, railwayOperations.deploymentLogs]) {
|
|
498
|
+
try {
|
|
499
|
+
const data = await client.request(
|
|
500
|
+
operation,
|
|
501
|
+
{ deploymentId, limit: 100 }
|
|
502
|
+
);
|
|
503
|
+
const logKey = operation === railwayOperations.buildLogs ? "buildLogs" : "deploymentLogs";
|
|
504
|
+
for (const entry of data[logKey] ?? []) {
|
|
505
|
+
if (entry.message) console.log(`[${prefix}] ${redactText(entry.message, secrets)}`);
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function waitForDeployment(input2) {
|
|
512
|
+
const deadline = Date.now() + input2.timeoutSeconds * 1e3;
|
|
513
|
+
let lastStatus = "UNKNOWN";
|
|
514
|
+
while (Date.now() < deadline) {
|
|
515
|
+
const data = await input2.client.request(
|
|
516
|
+
railwayOperations.deployment,
|
|
517
|
+
{ id: input2.deploymentId }
|
|
518
|
+
);
|
|
519
|
+
lastStatus = data.deployment.status;
|
|
520
|
+
if (SUCCESS_STATUSES.has(lastStatus)) return data.deployment;
|
|
521
|
+
if (FAILURE_STATUSES.has(lastStatus)) {
|
|
522
|
+
await printDeploymentLogs(
|
|
523
|
+
input2.client,
|
|
524
|
+
input2.deploymentId,
|
|
525
|
+
input2.prefix,
|
|
526
|
+
input2.secrets
|
|
527
|
+
);
|
|
528
|
+
throw new Error(`${input2.prefix} deployment failed with status ${lastStatus}.`);
|
|
529
|
+
}
|
|
530
|
+
await wait(5e3);
|
|
531
|
+
}
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Timed out waiting for ${input2.prefix} deployment. Last status: ${lastStatus}. Re-run the command to resume.`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
async function deployService(input2) {
|
|
537
|
+
const previous = await latestDeployment(
|
|
538
|
+
input2.client,
|
|
539
|
+
input2.projectId,
|
|
540
|
+
input2.environmentId,
|
|
541
|
+
input2.serviceId
|
|
542
|
+
);
|
|
543
|
+
let deploymentId = null;
|
|
544
|
+
let triggerError;
|
|
545
|
+
try {
|
|
546
|
+
if (input2.source.mode === "git") {
|
|
547
|
+
await input2.client.request(railwayOperations.deploy, {
|
|
548
|
+
environmentId: input2.environmentId,
|
|
549
|
+
serviceId: input2.serviceId,
|
|
550
|
+
commitSha: input2.source.commitSha
|
|
551
|
+
});
|
|
552
|
+
} else {
|
|
553
|
+
deploymentId = uploadLocalSource({
|
|
554
|
+
cwd: input2.cwd,
|
|
555
|
+
projectId: input2.projectId,
|
|
556
|
+
environmentId: input2.environmentId,
|
|
557
|
+
serviceId: input2.serviceId,
|
|
558
|
+
serviceName: input2.serviceName,
|
|
559
|
+
token: input2.token
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (!isAmbiguousMutationError(error)) throw error;
|
|
564
|
+
triggerError = error;
|
|
565
|
+
}
|
|
566
|
+
if (!deploymentId) {
|
|
567
|
+
const deadline = Date.now() + 3e4;
|
|
568
|
+
while (Date.now() < deadline) {
|
|
569
|
+
const current = await latestDeployment(
|
|
570
|
+
input2.client,
|
|
571
|
+
input2.projectId,
|
|
572
|
+
input2.environmentId,
|
|
573
|
+
input2.serviceId
|
|
574
|
+
);
|
|
575
|
+
if (current && current.id !== previous?.id) {
|
|
576
|
+
deploymentId = current.id;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
await wait(2e3);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (!deploymentId) {
|
|
583
|
+
if (triggerError) throw triggerError;
|
|
584
|
+
throw new Error(`Railway did not expose a new deployment for ${input2.serviceName}.`);
|
|
585
|
+
}
|
|
586
|
+
return waitForDeployment({
|
|
587
|
+
client: input2.client,
|
|
588
|
+
deploymentId,
|
|
589
|
+
timeoutSeconds: input2.timeoutSeconds,
|
|
590
|
+
prefix: input2.serviceName,
|
|
591
|
+
secrets: [input2.token, ...input2.secrets]
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function collectSensitiveValues(variables) {
|
|
595
|
+
return collectSensitiveStructuredValues(variables);
|
|
596
|
+
}
|
|
597
|
+
async function ensureDomain(input2) {
|
|
598
|
+
const existingData = await input2.client.request(railwayOperations.domains, {
|
|
599
|
+
projectId: input2.projectId,
|
|
600
|
+
environmentId: input2.environmentId,
|
|
601
|
+
serviceId: input2.serviceId
|
|
602
|
+
});
|
|
603
|
+
if (!input2.customDomain && input2.currentDomainId) {
|
|
604
|
+
const existingCustomDomain = existingData.domains.customDomains?.find(
|
|
605
|
+
(candidate) => candidate.id === input2.currentDomainId
|
|
606
|
+
);
|
|
607
|
+
if (existingCustomDomain) {
|
|
608
|
+
return {
|
|
609
|
+
id: existingCustomDomain.id,
|
|
610
|
+
url: `https://${existingCustomDomain.domain}`
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const existingRecordedServiceDomain = existingData.domains.serviceDomains?.find(
|
|
614
|
+
(candidate) => candidate.id === input2.currentDomainId
|
|
615
|
+
);
|
|
616
|
+
if (existingRecordedServiceDomain) {
|
|
617
|
+
return {
|
|
618
|
+
id: existingRecordedServiceDomain.id,
|
|
619
|
+
url: `https://${existingRecordedServiceDomain.domain}`
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (input2.customDomain) {
|
|
624
|
+
const existingDomain = existingData.domains.customDomains?.find(
|
|
625
|
+
(candidate) => candidate.domain === input2.customDomain
|
|
626
|
+
);
|
|
627
|
+
let domain;
|
|
628
|
+
if (existingDomain) {
|
|
629
|
+
domain = existingDomain;
|
|
630
|
+
} else {
|
|
631
|
+
try {
|
|
632
|
+
const data = await input2.client.request(
|
|
633
|
+
railwayOperations.customDomainCreate,
|
|
634
|
+
{
|
|
635
|
+
input: {
|
|
636
|
+
projectId: input2.projectId,
|
|
637
|
+
environmentId: input2.environmentId,
|
|
638
|
+
serviceId: input2.serviceId,
|
|
639
|
+
domain: input2.customDomain,
|
|
640
|
+
targetPort: 3e3
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
);
|
|
644
|
+
domain = data.customDomainCreate;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
const discovered = await input2.client.request(railwayOperations.domains, {
|
|
647
|
+
projectId: input2.projectId,
|
|
648
|
+
environmentId: input2.environmentId,
|
|
649
|
+
serviceId: input2.serviceId
|
|
650
|
+
});
|
|
651
|
+
const created = discovered.domains.customDomains?.find(
|
|
652
|
+
(candidate) => candidate.domain === input2.customDomain
|
|
653
|
+
);
|
|
654
|
+
if (!created) throw error;
|
|
655
|
+
domain = created;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const records = domain.status?.dnsRecords ?? [];
|
|
659
|
+
if (records.length > 0) {
|
|
660
|
+
console.log("Configure these DNS records:");
|
|
661
|
+
for (const record of records) {
|
|
662
|
+
console.log(` ${record.recordType ?? "DNS"} ${record.hostlabel ?? "@"} -> ${record.requiredValue ?? ""}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (input2.waitForVerification && !domain.status?.verified) {
|
|
666
|
+
const deadline = Date.now() + 3e5;
|
|
667
|
+
while (Date.now() < deadline) {
|
|
668
|
+
await wait(1e4);
|
|
669
|
+
const refreshed = await input2.client.request(
|
|
670
|
+
railwayOperations.customDomain,
|
|
671
|
+
{ id: domain.id, projectId: input2.projectId }
|
|
672
|
+
);
|
|
673
|
+
domain = refreshed.customDomain;
|
|
674
|
+
if (domain.status?.verified) break;
|
|
675
|
+
}
|
|
676
|
+
if (!domain.status?.verified) {
|
|
677
|
+
console.warn("DNS not yet propagated. The deployment succeeded; re-run later to verify the domain.");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
id: domain.id,
|
|
682
|
+
url: `https://${domain.domain}`
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const existingServiceDomain = existingData.domains.serviceDomains?.[0];
|
|
686
|
+
if (existingServiceDomain) {
|
|
687
|
+
return {
|
|
688
|
+
id: existingServiceDomain.id,
|
|
689
|
+
url: `https://${existingServiceDomain.domain}`
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
let createdDomain;
|
|
693
|
+
try {
|
|
694
|
+
const data = await input2.client.request(railwayOperations.serviceDomainCreate, {
|
|
695
|
+
input: {
|
|
696
|
+
environmentId: input2.environmentId,
|
|
697
|
+
serviceId: input2.serviceId,
|
|
698
|
+
targetPort: 3e3
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
createdDomain = data.serviceDomainCreate;
|
|
702
|
+
} catch (error) {
|
|
703
|
+
const discovered = await input2.client.request(railwayOperations.domains, {
|
|
704
|
+
projectId: input2.projectId,
|
|
705
|
+
environmentId: input2.environmentId,
|
|
706
|
+
serviceId: input2.serviceId
|
|
707
|
+
});
|
|
708
|
+
const created = discovered.domains.serviceDomains?.[0];
|
|
709
|
+
if (!created) throw error;
|
|
710
|
+
createdDomain = created;
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
id: createdDomain.id,
|
|
714
|
+
url: `https://${createdDomain.domain}`
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
async function cleanupProject(input2) {
|
|
718
|
+
if (!input2.state.projectId) throw new Error("The Railway state file does not contain a project ID.");
|
|
719
|
+
if (input2.state.workspaceId && input2.state.workspaceId !== input2.workspaceId) {
|
|
720
|
+
throw new Error("Recorded Railway project belongs to a different workspace.");
|
|
721
|
+
}
|
|
722
|
+
const projectData = await input2.client.request(railwayOperations.project, { id: input2.state.projectId });
|
|
723
|
+
if (projectData.project.name !== input2.state.projectName || projectData.project.workspaceId && projectData.project.workspaceId !== input2.workspaceId) {
|
|
724
|
+
throw new Error("Recorded Railway project identity does not match the cleanup target.");
|
|
725
|
+
}
|
|
726
|
+
if (!input2.options.yes) {
|
|
727
|
+
if (input2.options.nonInteractive || !input2.prompt) {
|
|
728
|
+
throw new Error("--cleanup requires --yes in non-interactive mode.");
|
|
729
|
+
}
|
|
730
|
+
console.log(`Project: ${input2.state.projectName} (${input2.state.projectId})`);
|
|
731
|
+
for (const [name, environment] of Object.entries(input2.state.environments)) {
|
|
732
|
+
console.log(` ${name}: ${environment.appUrl ?? "no recorded URL"}`);
|
|
733
|
+
}
|
|
734
|
+
const confirmation = await input2.prompt.ask(`Type "${input2.state.projectName}" to delete this project: `);
|
|
735
|
+
if (confirmation.trim() !== input2.state.projectName) throw new Error("Cleanup cancelled.");
|
|
736
|
+
}
|
|
737
|
+
await input2.client.request(railwayOperations.projectDelete, { id: input2.state.projectId });
|
|
738
|
+
if (existsSync(input2.statePath)) unlinkSync(input2.statePath);
|
|
739
|
+
console.log(`Deleted Railway project ${input2.state.projectName}.`);
|
|
740
|
+
}
|
|
741
|
+
function printDryRun(input2) {
|
|
742
|
+
console.log("Railway deployment plan (dry run)");
|
|
743
|
+
console.log(` Project: ${input2.state.projectName}`);
|
|
744
|
+
console.log(` Environment: ${input2.options.environment}`);
|
|
745
|
+
console.log(` Source: ${input2.source.mode} (${input2.source.reason})`);
|
|
746
|
+
console.log(` Env file: ${input2.envFile}`);
|
|
747
|
+
console.log(" Operations:");
|
|
748
|
+
console.log(" - validate Railway account and workspace");
|
|
749
|
+
console.log(" - create or reuse project and environment");
|
|
750
|
+
console.log(" - provision Postgres and Redis from Railway templates");
|
|
751
|
+
console.log(` - create or reuse ${input2.options.service}`);
|
|
752
|
+
if (input2.options.worker) console.log(" - create or reuse mercato-worker");
|
|
753
|
+
if (!input2.options.worker && input2.state.environments[input2.options.environment]?.workerServiceId) {
|
|
754
|
+
console.log(" - delete the recorded managed mercato-worker service");
|
|
755
|
+
}
|
|
756
|
+
console.log(" - upsert service variables");
|
|
757
|
+
console.log(` - deploy via ${input2.source.mode === "git" ? "Railway Git source" : "railway up"}`);
|
|
758
|
+
console.log(" - create a Railway or custom domain and redeploy the app");
|
|
759
|
+
if (input2.options.volume) console.log(` - create volume mounted at ${input2.options.volume}`);
|
|
760
|
+
console.log(" App variables:");
|
|
761
|
+
for (const line of formatVariablePlan(input2.appVariables, input2.options.token)) console.log(` ${line}`);
|
|
762
|
+
if (input2.workerVariables) {
|
|
763
|
+
console.log(" Worker variables:");
|
|
764
|
+
for (const line of formatVariablePlan(input2.workerVariables, input2.options.token)) console.log(` ${line}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async function runRailwayDeploy(args, dependencies = {}) {
|
|
768
|
+
const options = parseRailwayDeployOptions(args);
|
|
769
|
+
if (options.help) {
|
|
770
|
+
console.log(railwayDeployHelp());
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const cwd = dependencies.cwd ?? process.cwd();
|
|
774
|
+
const appPackage = readAppPackage(cwd);
|
|
775
|
+
assertSupportedNodeVersion(appPackage.nodeEngine);
|
|
776
|
+
const statePath = railwayStatePath(cwd, options.track);
|
|
777
|
+
const loadedState = loadRailwayState(statePath);
|
|
778
|
+
const requestedName = normalizeProjectName(options.project || appPackage.name);
|
|
779
|
+
const state = loadedState ?? createRailwayState(requestedName, process.env.npm_package_version || "unknown");
|
|
780
|
+
if (options.cleanup && !state.projectId) {
|
|
781
|
+
throw new Error("The Railway state file does not contain a project ID.");
|
|
782
|
+
}
|
|
783
|
+
if (loadedState && options.project && requestedName !== loadedState.projectName) {
|
|
784
|
+
if (!options.forceRename) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`State records project "${loadedState.projectName}". Pass --force-rename to use "${requestedName}".`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
state.projectName = requestedName;
|
|
790
|
+
}
|
|
791
|
+
const environmentState = ensureEnvironmentState(state, options.environment);
|
|
792
|
+
const source = options.cleanup ? { mode: "local", reason: "cleanup does not deploy source" } : (dependencies.resolveSource ?? resolveRailwaySource)(options.source, cwd);
|
|
793
|
+
if (options.cleanup && options.dryRun) {
|
|
794
|
+
console.log("Railway cleanup plan (dry run)");
|
|
795
|
+
console.log(` Project: ${state.projectName} (${state.projectId})`);
|
|
796
|
+
console.log(` State: ${statePath}`);
|
|
797
|
+
console.log(" Operation: projectDelete");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (!options.cleanup && !options.dryRun) {
|
|
801
|
+
console.log(`Source: ${source.mode} (${source.reason})`);
|
|
802
|
+
}
|
|
803
|
+
if (!options.cleanup && source.mode === "local") assertLocalUploadSafe(cwd);
|
|
804
|
+
const envFile = options.cleanup ? "" : resolveEnvFile(cwd, options.envFile);
|
|
805
|
+
const localEnv = options.cleanup ? {} : parseEnvFile(readFileSync(envFile, "utf8"));
|
|
806
|
+
let protectedSecrets = options.dryRun ? {
|
|
807
|
+
AUTH_SECRET: "generated-at-deploy",
|
|
808
|
+
JWT_SECRET: "generated-at-deploy",
|
|
809
|
+
TENANT_DATA_ENCRYPTION_FALLBACK_KEY: "generated-at-deploy"
|
|
810
|
+
} : generateProtectedSecrets(localEnv);
|
|
811
|
+
let appVariables = computeRailwayVariables({
|
|
812
|
+
env: localEnv,
|
|
813
|
+
role: "app",
|
|
814
|
+
workerEnabled: options.worker,
|
|
815
|
+
protectedSecrets,
|
|
816
|
+
railwayToken: options.token || process.env.RAILWAY_API_TOKEN,
|
|
817
|
+
allowedSecretKeys: options.allowedSecretKeys
|
|
818
|
+
});
|
|
819
|
+
let workerVariables = options.worker ? computeRailwayVariables({
|
|
820
|
+
env: localEnv,
|
|
821
|
+
role: "worker",
|
|
822
|
+
workerEnabled: true,
|
|
823
|
+
protectedSecrets,
|
|
824
|
+
railwayToken: options.token || process.env.RAILWAY_API_TOKEN,
|
|
825
|
+
allowedSecretKeys: options.allowedSecretKeys
|
|
826
|
+
}) : void 0;
|
|
827
|
+
if (options.dryRun) {
|
|
828
|
+
printDryRun({ state, options, source, envFile, appVariables, workerVariables });
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const ownsPrompt = dependencies.prompt === void 0;
|
|
832
|
+
const prompt = dependencies.prompt === void 0 ? !options.nonInteractive && process.stdin.isTTY ? createPrompt() : null : dependencies.prompt;
|
|
833
|
+
try {
|
|
834
|
+
const tokenResult = await resolveTokenInteractively(options, prompt);
|
|
835
|
+
const client = createRailwayGraphqlClient({
|
|
836
|
+
token: tokenResult.token,
|
|
837
|
+
verbose: options.verbose
|
|
838
|
+
});
|
|
839
|
+
console.log("Step 1: Authenticate with Railway");
|
|
840
|
+
const { workspaceId } = await resolveWorkspace(client, state.workspaceId);
|
|
841
|
+
if (options.cleanup) {
|
|
842
|
+
await cleanupProject({ client, state, statePath, options, prompt, workspaceId });
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
state.workspaceId = workspaceId;
|
|
846
|
+
persistState(statePath, state);
|
|
847
|
+
console.log("Step 2: Resolve workspace");
|
|
848
|
+
console.log(`Step 3: Create or look up project "${state.projectName}"`);
|
|
849
|
+
const projectId = await ensureProject({ client, state, statePath, workspaceId, options });
|
|
850
|
+
console.log(`Step 4: Create or look up environment "${options.environment}"`);
|
|
851
|
+
const environmentId = await ensureEnvironment({
|
|
852
|
+
client,
|
|
853
|
+
projectId,
|
|
854
|
+
name: options.environment,
|
|
855
|
+
environmentState,
|
|
856
|
+
persist: () => persistState(statePath, state)
|
|
857
|
+
});
|
|
858
|
+
if (options.region) {
|
|
859
|
+
const regionData = await client.request(
|
|
860
|
+
railwayOperations.regions,
|
|
861
|
+
{ projectId }
|
|
862
|
+
);
|
|
863
|
+
if (!regionData.regions.some(
|
|
864
|
+
(region) => region.id === options.region || region.region === options.region
|
|
865
|
+
)) {
|
|
866
|
+
throw new Error(`Unknown Railway region "${options.region}".`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
console.log("Step 5: Provision managed Postgres and Redis");
|
|
870
|
+
await ensureDatabase({
|
|
871
|
+
client,
|
|
872
|
+
projectId,
|
|
873
|
+
environmentId,
|
|
874
|
+
workspaceId,
|
|
875
|
+
name: "Postgres",
|
|
876
|
+
code: "postgres",
|
|
877
|
+
currentId: environmentState.postgresServiceId,
|
|
878
|
+
onResolved: (id) => {
|
|
879
|
+
environmentState.postgresServiceId = id;
|
|
880
|
+
persistState(statePath, state);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
await ensureDatabase({
|
|
884
|
+
client,
|
|
885
|
+
projectId,
|
|
886
|
+
environmentId,
|
|
887
|
+
workspaceId,
|
|
888
|
+
name: "Redis",
|
|
889
|
+
code: "redis",
|
|
890
|
+
currentId: environmentState.redisServiceId,
|
|
891
|
+
onResolved: (id) => {
|
|
892
|
+
environmentState.redisServiceId = id;
|
|
893
|
+
persistState(statePath, state);
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
console.log("Step 6: Create app and worker services");
|
|
897
|
+
const previousSource = environmentState.source;
|
|
898
|
+
const appServiceId = await ensureService({
|
|
899
|
+
client,
|
|
900
|
+
projectId,
|
|
901
|
+
environmentId,
|
|
902
|
+
name: options.service,
|
|
903
|
+
source,
|
|
904
|
+
currentId: environmentState.appServiceId,
|
|
905
|
+
previousSource,
|
|
906
|
+
startCommand: "sh ./scripts/railway-start.sh",
|
|
907
|
+
region: options.region,
|
|
908
|
+
railwayConfigFile: "railway.toml",
|
|
909
|
+
onResolved: (id) => {
|
|
910
|
+
environmentState.appServiceId = id;
|
|
911
|
+
environmentState.source = source;
|
|
912
|
+
persistState(statePath, state);
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
const workerServiceId = options.worker ? await ensureService({
|
|
916
|
+
client,
|
|
917
|
+
projectId,
|
|
918
|
+
environmentId,
|
|
919
|
+
name: "mercato-worker",
|
|
920
|
+
source,
|
|
921
|
+
currentId: environmentState.workerServiceId,
|
|
922
|
+
previousSource,
|
|
923
|
+
startCommand: "sh ./scripts/railway-worker.sh",
|
|
924
|
+
region: options.region,
|
|
925
|
+
railwayConfigFile: "railway.worker.toml",
|
|
926
|
+
onResolved: (id) => {
|
|
927
|
+
environmentState.workerServiceId = id;
|
|
928
|
+
persistState(statePath, state);
|
|
929
|
+
}
|
|
930
|
+
}) : void 0;
|
|
931
|
+
if (!options.worker && environmentState.workerServiceId) {
|
|
932
|
+
await removeDisabledWorker({
|
|
933
|
+
client,
|
|
934
|
+
projectId,
|
|
935
|
+
environmentId,
|
|
936
|
+
workerServiceId: environmentState.workerServiceId
|
|
937
|
+
});
|
|
938
|
+
environmentState.workerServiceId = void 0;
|
|
939
|
+
if (environmentState.lastDeployIds) {
|
|
940
|
+
delete environmentState.lastDeployIds.worker;
|
|
941
|
+
}
|
|
942
|
+
persistState(statePath, state);
|
|
943
|
+
}
|
|
944
|
+
if (options.volume) {
|
|
945
|
+
if (environmentState.volumeId) {
|
|
946
|
+
const volumeData = await client.request(railwayOperations.volumes, { projectId });
|
|
947
|
+
const recordedVolume = nodes(volumeData.project.volumes).find(
|
|
948
|
+
(volume) => volume.id === environmentState.volumeId && nodes(volume.volumeInstances).some(
|
|
949
|
+
(instance) => instance.environmentId === environmentId && instance.serviceId === appServiceId && instance.mountPath === options.volume
|
|
950
|
+
)
|
|
951
|
+
);
|
|
952
|
+
if (!recordedVolume) environmentState.volumeId = void 0;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (options.volume && !environmentState.volumeId) {
|
|
956
|
+
try {
|
|
957
|
+
const volumeData = await client.request(
|
|
958
|
+
railwayOperations.volumeCreate,
|
|
959
|
+
{
|
|
960
|
+
input: {
|
|
961
|
+
projectId,
|
|
962
|
+
environmentId,
|
|
963
|
+
serviceId: appServiceId,
|
|
964
|
+
mountPath: options.volume
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
);
|
|
968
|
+
environmentState.volumeId = volumeData.volumeCreate.id;
|
|
969
|
+
} catch (error) {
|
|
970
|
+
const volumeData = await client.request(railwayOperations.volumes, { projectId });
|
|
971
|
+
const discovered = nodes(volumeData.project.volumes).find(
|
|
972
|
+
(volume) => nodes(volume.volumeInstances).some(
|
|
973
|
+
(instance) => instance.environmentId === environmentId && instance.serviceId === appServiceId && instance.mountPath === options.volume
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
if (!discovered) throw error;
|
|
977
|
+
environmentState.volumeId = discovered.id;
|
|
978
|
+
}
|
|
979
|
+
persistState(statePath, state);
|
|
980
|
+
}
|
|
981
|
+
if (!options.volume) {
|
|
982
|
+
console.warn(
|
|
983
|
+
"Attachments uploaded to this deployment will be lost on redeploy. Re-run with --volume /app/storage to enable persistent storage."
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
console.log("Step 7: Compute and upload environment variables");
|
|
987
|
+
const existingVariables = await loadVariables({
|
|
988
|
+
client,
|
|
989
|
+
projectId,
|
|
990
|
+
environmentId,
|
|
991
|
+
serviceId: appServiceId
|
|
992
|
+
});
|
|
993
|
+
protectedSecrets = generateProtectedSecrets({ ...localEnv, ...existingVariables });
|
|
994
|
+
appVariables = computeRailwayVariables({
|
|
995
|
+
env: localEnv,
|
|
996
|
+
role: "app",
|
|
997
|
+
workerEnabled: options.worker,
|
|
998
|
+
protectedSecrets,
|
|
999
|
+
railwayToken: tokenResult.token,
|
|
1000
|
+
allowedSecretKeys: options.allowedSecretKeys
|
|
1001
|
+
});
|
|
1002
|
+
workerVariables = options.worker ? computeRailwayVariables({
|
|
1003
|
+
env: localEnv,
|
|
1004
|
+
role: "worker",
|
|
1005
|
+
workerEnabled: true,
|
|
1006
|
+
protectedSecrets,
|
|
1007
|
+
railwayToken: tokenResult.token,
|
|
1008
|
+
allowedSecretKeys: options.allowedSecretKeys
|
|
1009
|
+
}) : void 0;
|
|
1010
|
+
await upsertVariables({ client, projectId, environmentId, serviceId: appServiceId, variables: appVariables });
|
|
1011
|
+
if (workerServiceId && workerVariables) {
|
|
1012
|
+
await upsertVariables({
|
|
1013
|
+
client,
|
|
1014
|
+
projectId,
|
|
1015
|
+
environmentId,
|
|
1016
|
+
serviceId: workerServiceId,
|
|
1017
|
+
variables: workerVariables
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
if (options.writeEnv) writeGeneratedSecrets(envFile, protectedSecrets);
|
|
1021
|
+
console.log("Step 8: Trigger deployment and monitor status");
|
|
1022
|
+
const appDeployment = await deployService({
|
|
1023
|
+
client,
|
|
1024
|
+
cwd,
|
|
1025
|
+
source,
|
|
1026
|
+
projectId,
|
|
1027
|
+
environmentId,
|
|
1028
|
+
serviceId: appServiceId,
|
|
1029
|
+
serviceName: options.service,
|
|
1030
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
1031
|
+
token: tokenResult.token,
|
|
1032
|
+
secrets: collectSensitiveValues(appVariables)
|
|
1033
|
+
});
|
|
1034
|
+
environmentState.lastDeployIds ??= {};
|
|
1035
|
+
environmentState.lastDeployIds.app = appDeployment.id;
|
|
1036
|
+
persistState(statePath, state);
|
|
1037
|
+
if (workerServiceId) {
|
|
1038
|
+
const workerDeployment = await deployService({
|
|
1039
|
+
client,
|
|
1040
|
+
cwd,
|
|
1041
|
+
source,
|
|
1042
|
+
projectId,
|
|
1043
|
+
environmentId,
|
|
1044
|
+
serviceId: workerServiceId,
|
|
1045
|
+
serviceName: "mercato-worker",
|
|
1046
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
1047
|
+
token: tokenResult.token,
|
|
1048
|
+
secrets: collectSensitiveValues(workerVariables ?? {})
|
|
1049
|
+
});
|
|
1050
|
+
environmentState.lastDeployIds.worker = workerDeployment.id;
|
|
1051
|
+
persistState(statePath, state);
|
|
1052
|
+
}
|
|
1053
|
+
console.log("Step 9: Provision public domain");
|
|
1054
|
+
const domain = await ensureDomain({
|
|
1055
|
+
client,
|
|
1056
|
+
projectId,
|
|
1057
|
+
environmentId,
|
|
1058
|
+
serviceId: appServiceId,
|
|
1059
|
+
customDomain: options.domain,
|
|
1060
|
+
waitForVerification: options.waitDomain,
|
|
1061
|
+
currentDomainId: environmentState.domainId
|
|
1062
|
+
});
|
|
1063
|
+
environmentState.domainId = domain.id;
|
|
1064
|
+
environmentState.appUrl = domain.url;
|
|
1065
|
+
persistState(statePath, state);
|
|
1066
|
+
const finalAppVariables = { ...appVariables, APP_URL: domain.url, NEXT_PUBLIC_APP_URL: domain.url };
|
|
1067
|
+
await upsertVariables({
|
|
1068
|
+
client,
|
|
1069
|
+
projectId,
|
|
1070
|
+
environmentId,
|
|
1071
|
+
serviceId: appServiceId,
|
|
1072
|
+
variables: finalAppVariables
|
|
1073
|
+
});
|
|
1074
|
+
const finalDeployment = await deployService({
|
|
1075
|
+
client,
|
|
1076
|
+
cwd,
|
|
1077
|
+
source,
|
|
1078
|
+
projectId,
|
|
1079
|
+
environmentId,
|
|
1080
|
+
serviceId: appServiceId,
|
|
1081
|
+
serviceName: options.service,
|
|
1082
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
1083
|
+
token: tokenResult.token,
|
|
1084
|
+
secrets: collectSensitiveValues(finalAppVariables)
|
|
1085
|
+
});
|
|
1086
|
+
environmentState.lastDeployIds.app = finalDeployment.id;
|
|
1087
|
+
persistState(statePath, state);
|
|
1088
|
+
console.log("Step 10: Deployment complete");
|
|
1089
|
+
console.log("");
|
|
1090
|
+
console.log("Open Mercato deployed to Railway");
|
|
1091
|
+
console.log(` Project: https://railway.com/project/${projectId}`);
|
|
1092
|
+
console.log(` Environment: ${options.environment}`);
|
|
1093
|
+
console.log(` Source: ${source.mode}`);
|
|
1094
|
+
console.log(` App URL: ${domain.url}`);
|
|
1095
|
+
console.log(` Health: ${domain.url}/api/healthz`);
|
|
1096
|
+
if (workerServiceId) console.log(" Worker: mercato-worker (running)");
|
|
1097
|
+
console.log(` State: ${statePath}`);
|
|
1098
|
+
if (tokenResult.cached) console.log(` Token cache: ${railwayTokenConfigPath()}`);
|
|
1099
|
+
console.log("");
|
|
1100
|
+
console.log(`DEPLOY_URL=${domain.url}`);
|
|
1101
|
+
} finally {
|
|
1102
|
+
if (ownsPrompt) prompt?.close();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
export {
|
|
1106
|
+
computeRailwayVariables,
|
|
1107
|
+
formatVariablePlan,
|
|
1108
|
+
generateProtectedSecrets,
|
|
1109
|
+
parseEnvFile,
|
|
1110
|
+
parseRailwayDeployOptions,
|
|
1111
|
+
railwayDeployHelp,
|
|
1112
|
+
resolveEnvFile,
|
|
1113
|
+
runRailwayDeploy
|
|
1114
|
+
};
|
|
1115
|
+
//# sourceMappingURL=index.js.map
|