gencow 0.1.257 → 0.1.259
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/lib/app-command.mjs +2 -0
- package/lib/cli-command-runner.mjs +30 -5
- package/lib/cloud-targets.mjs +34 -5
- package/lib/db-command.mjs +20 -5
- package/lib/db-push-command.mjs +7 -1
- package/lib/deploy-codegen-failure.mjs +17 -2
- package/lib/deploy-prod-target.mjs +144 -0
- package/lib/deploy-runtime.mjs +54 -155
- package/lib/deploy-static-fullstack-runtime.mjs +16 -7
- package/lib/files-command.mjs +8 -4
- package/lib/jobs-command.mjs +14 -8
- package/lib/static-artifact-manifest.mjs +47 -6
- package/lib/static-command.mjs +14 -0
- package/lib/static-deploy-artifact-preflight.mjs +85 -0
- package/lib/static-deploy-command.mjs +5 -50
- package/package.json +4 -4
- package/runtime/server.mjs +92 -133
package/lib/app-command.mjs
CHANGED
|
@@ -488,6 +488,8 @@ ${dashboardLine}
|
|
|
488
488
|
if (delData.stopErrorCode) {
|
|
489
489
|
warnImpl(`App stop needs administrator review. Code: ${delData.stopErrorCode}. Handoff ID: ${delData.handoffId}`);
|
|
490
490
|
}
|
|
491
|
+
// The handoff changes ownership only; the app keeps serving until an administrator processes it.
|
|
492
|
+
warnImpl(`Until an administrator processes the deletion, the app's public URL can keep responding. Request ID: ${requestId}. This request is already recorded; do not repeat the delete.`);
|
|
491
493
|
return 0;
|
|
492
494
|
}
|
|
493
495
|
|
|
@@ -83,10 +83,16 @@ const SAFE_LOCAL_ERROR_CODES = new Map([
|
|
|
83
83
|
[
|
|
84
84
|
"CODEGEN_SOURCE_LOAD_FAILED",
|
|
85
85
|
{
|
|
86
|
-
message: "Deploy stopped before upload: a backend source file
|
|
86
|
+
message: "Deploy stopped before upload: a backend source file could not be loaded.",
|
|
87
87
|
stage: "codegen",
|
|
88
|
-
action:
|
|
89
|
-
|
|
88
|
+
action: (caught) => {
|
|
89
|
+
const cause =
|
|
90
|
+
"the top-level code that throws during module load, or an import path that does not resolve (check the path and that the imported package is installed)";
|
|
91
|
+
if (safeSourceLocation(caught)) {
|
|
92
|
+
return `Retryable: yes, after the source at the location above is fixed (${cause}) — run \`npx gencow@latest codegen\` to confirm it passes, then rerun your deploy command.`;
|
|
93
|
+
}
|
|
94
|
+
return `Retryable: yes, after ${cause} is fixed — run \`npx gencow@latest codegen\` to confirm it passes, then rerun your deploy command. The failing location is not available; check the backend files that run code at import time and the paths they import.`;
|
|
95
|
+
},
|
|
90
96
|
},
|
|
91
97
|
],
|
|
92
98
|
[
|
|
@@ -111,16 +117,35 @@ const SAFE_LOCAL_ERROR_CODES = new Map([
|
|
|
111
117
|
"Retryable: no, until `npx gencow@latest codegen` passes locally — run it, fix what it reports, then rerun your deploy command.",
|
|
112
118
|
},
|
|
113
119
|
],
|
|
120
|
+
[
|
|
121
|
+
"STATIC_DIR_NOT_FOUND",
|
|
122
|
+
{
|
|
123
|
+
message: (caught) => {
|
|
124
|
+
const dir = safeDisplayDir(caught);
|
|
125
|
+
return dir
|
|
126
|
+
? `Static deploy stopped before upload: the static directory "${dir}" does not exist or is not a directory.`
|
|
127
|
+
: "Static deploy stopped before upload: the static directory does not exist or is not a directory.";
|
|
128
|
+
},
|
|
129
|
+
stage: "preflight",
|
|
130
|
+
action: (caught) => {
|
|
131
|
+
const dir = safeDisplayDir(caught) ?? "<static-dir>";
|
|
132
|
+
if (caught.staticDirSource === "config") {
|
|
133
|
+
return `Retryable: yes, after ${dir} exists as a directory — run \`ls -ld ${dir}\` to confirm, or correct the client static.dir setting in your Gencow config, then rerun the same command.`;
|
|
134
|
+
}
|
|
135
|
+
return `Retryable: yes, after ${dir} exists as a directory — run \`ls -ld ${dir}\` to confirm, then rerun the same command with a correct path, or omit the directory argument to auto-detect (dist, out, build, .next/out).`;
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
114
139
|
[
|
|
115
140
|
"STATIC_ARTIFACT_INDEX_EMPTY",
|
|
116
141
|
{
|
|
117
142
|
message:
|
|
118
|
-
"Static deploy stopped before upload: the build output's index.html is empty or contains only whitespace.",
|
|
143
|
+
"Static deploy stopped before upload: the build output's index.html is empty or contains only whitespace, a BOM, or HTML comments.",
|
|
119
144
|
stage: "artifact",
|
|
120
145
|
action: (caught) => {
|
|
121
146
|
const dir = safeDisplayDir(caught);
|
|
122
147
|
const target = dir ?? "<build-output-dir>";
|
|
123
|
-
return `Retryable: yes, after the build writes an index.html with real content into ${target} — run \`ls -l ${target}/index.html\` to confirm its size is above 0 bytes and that it
|
|
148
|
+
return `Retryable: yes, after the build writes an index.html with real content into ${target} — run \`ls -l ${target}/index.html\` to confirm its size is above 0 bytes and that it holds more than whitespace or HTML comments, then rerun \`gencow static ${target}\`.`;
|
|
124
149
|
},
|
|
125
150
|
},
|
|
126
151
|
],
|
package/lib/cloud-targets.mjs
CHANGED
|
@@ -33,6 +33,28 @@ export function readProjectAppTargets(cwd = process.cwd()) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Single decision for "which prod app does --prod (+ optional --app) target".
|
|
37
|
+
// acceptExplicitWithoutProdApp preserves call sites that trust an explicit app
|
|
38
|
+
// when the project has no prodApp binding (legacy metadata); others fail closed.
|
|
39
|
+
export function resolveProdAppTarget({ explicitAppId = null, prodApp = null, acceptExplicitWithoutProdApp = false }) {
|
|
40
|
+
if (!prodApp) {
|
|
41
|
+
if (explicitAppId && acceptExplicitWithoutProdApp) return { appId: explicitAppId };
|
|
42
|
+
return { appId: null, reason: "no_prod_app" };
|
|
43
|
+
}
|
|
44
|
+
if (explicitAppId && explicitAppId !== prodApp) {
|
|
45
|
+
return { appId: null, reason: "explicit_app_not_prod_app", prodApp };
|
|
46
|
+
}
|
|
47
|
+
return { appId: prodApp };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Shared human message for a failed prod target; falls back to the call site's own text.
|
|
51
|
+
export function describeProdTargetFailure(result, fallbackMessage) {
|
|
52
|
+
if (result?.reason === "explicit_app_not_prod_app") {
|
|
53
|
+
return `\`--app\` with \`--prod\` must name this project's prod app: ${result.prodApp}. Run again with --app ${result.prodApp} or without --app.`;
|
|
54
|
+
}
|
|
55
|
+
return fallbackMessage;
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
export function resolveCloudAppTarget(restArgs, options = {}) {
|
|
37
59
|
const {
|
|
38
60
|
cwd = process.cwd(),
|
|
@@ -59,17 +81,24 @@ export function resolveCloudAppTarget(restArgs, options = {}) {
|
|
|
59
81
|
if (projectTargets.invalid) {
|
|
60
82
|
return { error: "MIGRATION_PROJECT_CONFIG_INVALID" };
|
|
61
83
|
}
|
|
84
|
+
// The project's own prod app id names the prod environment even without --prod.
|
|
85
|
+
if (explicitApp && envTarget === "dev" && projectTargets.prodApp && appId === projectTargets.prodApp) {
|
|
86
|
+
envTarget = "prod";
|
|
87
|
+
}
|
|
62
88
|
if (!appId) {
|
|
63
89
|
appId = envTarget === "prod" ? projectTargets.prodApp : projectTargets.appId;
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
if (envTarget === "prod") {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
const prodTarget = resolveProdAppTarget({
|
|
94
|
+
explicitAppId: explicitApp ? appId : null,
|
|
95
|
+
prodApp: projectTargets.prodApp,
|
|
96
|
+
acceptExplicitWithoutProdApp: true,
|
|
97
|
+
});
|
|
98
|
+
if (!prodTarget.appId) {
|
|
99
|
+
return { error: describeProdTargetFailure(prodTarget, missingProdMessage) };
|
|
72
100
|
}
|
|
101
|
+
appId = prodTarget.appId;
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
if (!appId) {
|
package/lib/db-command.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { createDbCheckCommand, createDbPushCommand } from "./db-push-command.mjs
|
|
|
17
17
|
import { logEnvFileOperationWithType } from "./backend-env-resolver.mjs";
|
|
18
18
|
import { runDrizzleGenerateWithFallback } from "./drizzle-generate.mjs";
|
|
19
19
|
import { BOLD, CYAN, DIM, GREEN, RED, RESET, error, info, log, success, warn } from "./output.mjs";
|
|
20
|
+
import { describeProdTargetFailure, resolveProdAppTarget } from "./cloud-targets.mjs";
|
|
20
21
|
import { platformFetch, requireCreds } from "./platform-client.mjs";
|
|
21
22
|
import { readProjectMetadata } from "./project-context.mjs";
|
|
22
23
|
|
|
@@ -35,9 +36,17 @@ export function resolveDbCloudTarget(args, gencowJson = null) {
|
|
|
35
36
|
if (gencowJson) {
|
|
36
37
|
platformUrl = gencowJson.platformUrl || null;
|
|
37
38
|
if (isProd) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const prodTarget = resolveProdAppTarget({
|
|
40
|
+
explicitAppId,
|
|
41
|
+
prodApp: gencowJson.prodApp || null,
|
|
42
|
+
acceptExplicitWithoutProdApp: true,
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
appId: prodTarget.appId,
|
|
46
|
+
isProd,
|
|
47
|
+
platformUrl,
|
|
48
|
+
prodTargetFailure: prodTarget.appId ? undefined : prodTarget,
|
|
49
|
+
};
|
|
41
50
|
}
|
|
42
51
|
return { appId: explicitAppId || gencowJson.appId || gencowJson.appName || null, isProd, platformUrl };
|
|
43
52
|
}
|
|
@@ -369,10 +378,16 @@ export function createDbCommands({
|
|
|
369
378
|
readFileSyncImpl,
|
|
370
379
|
resolvePathImpl,
|
|
371
380
|
});
|
|
372
|
-
const
|
|
381
|
+
const target = resolveDbCloudTarget(args, gencowJson);
|
|
382
|
+
const { appId, isProd, platformUrl } = target;
|
|
373
383
|
|
|
374
384
|
if (isProd && !appId) {
|
|
375
|
-
errorImpl(
|
|
385
|
+
errorImpl(
|
|
386
|
+
describeProdTargetFailure(
|
|
387
|
+
target.prodTargetFailure,
|
|
388
|
+
"Prod app not found. Run 'gencow deploy' first to create a prod app.",
|
|
389
|
+
),
|
|
390
|
+
);
|
|
376
391
|
return 1;
|
|
377
392
|
}
|
|
378
393
|
|
package/lib/db-push-command.mjs
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
resolveConfirmationMode,
|
|
54
54
|
} from "./confirm-gate.mjs";
|
|
55
55
|
import { emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
|
|
56
|
+
import { describeProdTargetFailure } from "./cloud-targets.mjs";
|
|
56
57
|
import { readProjectMetadata } from "./project-context.mjs";
|
|
57
58
|
|
|
58
59
|
function clientCorrelationId() {
|
|
@@ -246,7 +247,12 @@ function createDbMigrationCommand(deps, command) {
|
|
|
246
247
|
buildClientUserFixableDiagnostic("MIGRATION_APP_TARGET_MISSING", clientCorrelationId()),
|
|
247
248
|
);
|
|
248
249
|
} else if (isProd) {
|
|
249
|
-
deps.errorImpl(
|
|
250
|
+
deps.errorImpl(
|
|
251
|
+
describeProdTargetFailure(
|
|
252
|
+
target.prodTargetFailure,
|
|
253
|
+
"Prod app not found. Run 'gencow deploy' first to create a prod app.",
|
|
254
|
+
),
|
|
255
|
+
);
|
|
250
256
|
} else {
|
|
251
257
|
deps.errorImpl("Cloud app not found. Make sure gencow.json exists with an appId.");
|
|
252
258
|
deps.infoImpl(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
|
|
@@ -33,6 +33,18 @@ function toProjectRelativePath(rawPath, cwd, processCwd) {
|
|
|
33
33
|
return normalized;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// esbuild prints one diagnostic per line (`<file>:<line>:<col>: ERROR: <text>`) and the
|
|
37
|
+
// Bun loader prints `Could not resolve`/`Cannot find module` for an import path that does
|
|
38
|
+
// not exist. Those are load failures: the source is well-formed, its import is not.
|
|
39
|
+
const DIAGNOSTIC_LINE_PATTERN = /ERROR: |Could not resolve "|Cannot find (?:module|package)\b/u;
|
|
40
|
+
const UNRESOLVED_MODULE_PATTERN = /Could not resolve "|Cannot find (?:module|package)\b/u;
|
|
41
|
+
|
|
42
|
+
// The first diagnostic decides the class, matching how the first location is reported.
|
|
43
|
+
function isUnresolvedModuleFailure(message) {
|
|
44
|
+
const firstDiagnostic = message.split("\n").find((line) => DIAGNOSTIC_LINE_PATTERN.test(line)) ?? message;
|
|
45
|
+
return UNRESOLVED_MODULE_PATTERN.test(firstDiagnostic);
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
// Returns the first project-relative source location found in a raw message, or null.
|
|
37
49
|
// Absolute paths, stacks and the raw text are never returned.
|
|
38
50
|
function findProjectSourceLocation(message, { cwd, processCwd }) {
|
|
@@ -53,11 +65,14 @@ export function classifyDeployCodegenFailure(error, { cwd, backendRoot = "gencow
|
|
|
53
65
|
const message = (error instanceof Error ? error.message : String(error ?? "")).slice(0, MAX_CLASSIFIED_MESSAGE_LENGTH);
|
|
54
66
|
if (message.includes("Transpile error:")) {
|
|
55
67
|
const location = findProjectSourceLocation(message, { cwd, processCwd });
|
|
68
|
+
const backendLocation = location && location.sourceFile.startsWith(`${backendRoot}/`) ? location : null;
|
|
69
|
+
if (isUnresolvedModuleFailure(message)) {
|
|
70
|
+
return { code: CODEGEN_FAILURE_CODES.SOURCE_LOAD_FAILED, ...(backendLocation ?? {}) };
|
|
71
|
+
}
|
|
56
72
|
if (location) {
|
|
57
|
-
const inBackend = location.sourceFile.startsWith(`${backendRoot}/`);
|
|
58
73
|
return {
|
|
59
74
|
code: CODEGEN_FAILURE_CODES.SOURCE_SYNTAX_ERROR,
|
|
60
|
-
...(
|
|
75
|
+
...(backendLocation ?? {}),
|
|
61
76
|
};
|
|
62
77
|
}
|
|
63
78
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
|
|
3
|
+
import { CONFIRMATION_MODE, resolveConfirmationMode } from "./confirm-gate.mjs";
|
|
4
|
+
import { confirmFirstProductionDeploy } from "./deploy-first-prod-confirm.mjs";
|
|
5
|
+
import { reconcileProductionAppTarget } from "./deploy-app-target.mjs";
|
|
6
|
+
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
7
|
+
import { BOLD, DIM, GREEN, RED, RESET, error, info, log, success } from "./output.mjs";
|
|
8
|
+
import { prodCreateFailureMessage } from "./prod-create-failure.mjs";
|
|
9
|
+
|
|
10
|
+
export async function ensureProductionDeployTarget({
|
|
11
|
+
appId,
|
|
12
|
+
displayName,
|
|
13
|
+
createCloudAppIfNeeded,
|
|
14
|
+
createInterfaceImpl,
|
|
15
|
+
creds,
|
|
16
|
+
deployArgs,
|
|
17
|
+
gencowJsonPath,
|
|
18
|
+
platformFetchImpl,
|
|
19
|
+
processEnv = process.env,
|
|
20
|
+
readFileSyncImpl = readFileSync,
|
|
21
|
+
setTimeoutImpl = setTimeout,
|
|
22
|
+
successImpl = success,
|
|
23
|
+
errorImpl = error,
|
|
24
|
+
infoImpl = info,
|
|
25
|
+
logImpl = log,
|
|
26
|
+
writeFileSyncImpl = writeFileSync,
|
|
27
|
+
exitImpl = (code) => process.exit(code),
|
|
28
|
+
prodAppId = null,
|
|
29
|
+
region = null,
|
|
30
|
+
prepareLocalSource = null,
|
|
31
|
+
}) {
|
|
32
|
+
const confirmMode = resolveConfirmationMode({
|
|
33
|
+
allowCiBypass: true,
|
|
34
|
+
args: deployArgs,
|
|
35
|
+
createInterfaceImpl,
|
|
36
|
+
processEnv,
|
|
37
|
+
});
|
|
38
|
+
// Without a recorded prod app, the first create always needs confirmation, so a
|
|
39
|
+
// non-interactive run fails before any platform lookup or create request.
|
|
40
|
+
if (!prodAppId && confirmMode === CONFIRMATION_MODE.NON_INTERACTIVE) {
|
|
41
|
+
await confirmFirstProductionDeploy({ appId, errorImpl, infoImpl, logImpl, mode: confirmMode, region });
|
|
42
|
+
exitImpl(1);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const reconciledTarget = await reconcileProductionAppTarget({
|
|
46
|
+
appId,
|
|
47
|
+
prodAppId,
|
|
48
|
+
creds,
|
|
49
|
+
displayName,
|
|
50
|
+
gencowJsonPath,
|
|
51
|
+
platformFetchImpl,
|
|
52
|
+
createCloudAppIfNeeded,
|
|
53
|
+
infoImpl,
|
|
54
|
+
errorImpl,
|
|
55
|
+
exitImpl,
|
|
56
|
+
});
|
|
57
|
+
if (!reconciledTarget) return null;
|
|
58
|
+
const productionParentAppId = reconciledTarget.appId;
|
|
59
|
+
const existingProdAppId = reconciledTarget.prodAppId;
|
|
60
|
+
if (existingProdAppId) return existingProdAppId;
|
|
61
|
+
if (!productionParentAppId) return null;
|
|
62
|
+
|
|
63
|
+
const proceed = await confirmFirstProductionDeploy({
|
|
64
|
+
appId: productionParentAppId,
|
|
65
|
+
createInterfaceImpl,
|
|
66
|
+
errorImpl,
|
|
67
|
+
infoImpl,
|
|
68
|
+
logImpl,
|
|
69
|
+
mode: confirmMode,
|
|
70
|
+
region,
|
|
71
|
+
});
|
|
72
|
+
if (!proceed) {
|
|
73
|
+
exitImpl(1);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Local source validation must pass before any remote create: a rejected source
|
|
78
|
+
// (for example a codegen failure) must not leave an empty production app behind.
|
|
79
|
+
// The callback renders its own diagnosis and returns false when it aborts.
|
|
80
|
+
if (prepareLocalSource && !(await prepareLocalSource())) return null;
|
|
81
|
+
|
|
82
|
+
infoImpl("Creating prod app...");
|
|
83
|
+
const createProdRes = await platformFetchImpl(
|
|
84
|
+
creds,
|
|
85
|
+
`/platform/apps/${productionParentAppId}/create-prod`,
|
|
86
|
+
{
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify(region ? { region } : {}),
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (!createProdRes.ok) {
|
|
94
|
+
const errData = await readJsonObjectResponse(createProdRes);
|
|
95
|
+
if (createProdRes.status === 403) {
|
|
96
|
+
logImpl("");
|
|
97
|
+
logImpl(` ${RED}⛔ Production Deploy — Startup or Enterprise required${RESET}`);
|
|
98
|
+
logImpl("");
|
|
99
|
+
logImpl(` ${DIM}gencow deploy --prod is for production deployment.${RESET}`);
|
|
100
|
+
logImpl("");
|
|
101
|
+
logImpl(` ${BOLD}💡 Dev environment deploy:${RESET}`);
|
|
102
|
+
logImpl(` ${GREEN}gencow deploy${RESET} ${DIM}← Deploy backend to dev${RESET}`);
|
|
103
|
+
logImpl(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
|
|
104
|
+
logImpl(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
|
|
105
|
+
logImpl("");
|
|
106
|
+
logImpl(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
|
|
107
|
+
logImpl(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Startup${RESET}`);
|
|
108
|
+
logImpl("");
|
|
109
|
+
} else {
|
|
110
|
+
errorImpl(prodCreateFailureMessage(errData, createProdRes));
|
|
111
|
+
}
|
|
112
|
+
exitImpl(1);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const createProdData = await readJsonObjectResponse(createProdRes);
|
|
117
|
+
if (!createProdData) {
|
|
118
|
+
errorImpl("Prod app creation failed: server returned an invalid JSON response");
|
|
119
|
+
exitImpl(1);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const nextProdAppId = createProdData.prodApp;
|
|
123
|
+
const gencowJson = existsSync(gencowJsonPath) ? JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8")) : {};
|
|
124
|
+
gencowJson.prodApp = nextProdAppId;
|
|
125
|
+
writeFileSyncImpl(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
|
|
126
|
+
|
|
127
|
+
if (createProdData.alreadyExists) {
|
|
128
|
+
infoImpl(`Prod app verified: ${nextProdAppId}`);
|
|
129
|
+
if (region && createProdData.region && createProdData.region !== region) {
|
|
130
|
+
infoImpl(
|
|
131
|
+
`Region unchanged: existing prod app is in ${createProdData.region}; --region only applies to first prod creation.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
successImpl(`Prod app created: ${nextProdAppId}`);
|
|
136
|
+
infoImpl(`URL: ${createProdData.url}`);
|
|
137
|
+
if (createProdData.region) {
|
|
138
|
+
infoImpl(`Region: ${createProdData.region} (${createProdData.regionSource || "selected"})`);
|
|
139
|
+
}
|
|
140
|
+
await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return nextProdAppId;
|
|
144
|
+
}
|
package/lib/deploy-runtime.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
2
2
|
import { basename, resolve } from "path";
|
|
3
3
|
import { readProjectDisplayName } from "./cli-project-runtime.mjs";
|
|
4
|
-
import { BOLD, CYAN, DIM,
|
|
4
|
+
import { BOLD, CYAN, DIM, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
|
|
5
5
|
import { createDeployPackageRuntime } from "./deploy-package-runtime.mjs";
|
|
6
6
|
import {
|
|
7
7
|
getCliInvocationSelection,
|
|
@@ -16,11 +16,9 @@ import {
|
|
|
16
16
|
runStaticDeployFlow,
|
|
17
17
|
} from "./deploy-static-fullstack-runtime.mjs";
|
|
18
18
|
import { detectStaticDeployBackend, preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
|
|
19
|
-
import {
|
|
19
|
+
import { formatCommandError } from "./cli-command-runner.mjs";
|
|
20
20
|
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
21
|
-
import {
|
|
22
|
-
import { CONFIRMATION_MODE, resolveConfirmationMode } from "./confirm-gate.mjs";
|
|
23
|
-
import { confirmFirstProductionDeploy } from "./deploy-first-prod-confirm.mjs";
|
|
21
|
+
import { ensureProductionDeployTarget } from "./deploy-prod-target.mjs";
|
|
24
22
|
|
|
25
23
|
export function parseDeployArgs(deployArgs) {
|
|
26
24
|
const parsed = {
|
|
@@ -185,134 +183,12 @@ function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, lo
|
|
|
185
183
|
infoImpl(" gencow static [dir] Deploy static files only");
|
|
186
184
|
}
|
|
187
185
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
deployArgs,
|
|
195
|
-
gencowJsonPath,
|
|
196
|
-
platformFetchImpl,
|
|
197
|
-
processEnv = process.env,
|
|
198
|
-
readFileSyncImpl = readFileSync,
|
|
199
|
-
setTimeoutImpl = setTimeout,
|
|
200
|
-
successImpl = success,
|
|
201
|
-
errorImpl = error,
|
|
202
|
-
infoImpl = info,
|
|
203
|
-
logImpl = log,
|
|
204
|
-
writeFileSyncImpl = writeFileSync,
|
|
205
|
-
exitImpl = (code) => process.exit(code),
|
|
206
|
-
prodAppId = null,
|
|
207
|
-
region = null,
|
|
208
|
-
}) {
|
|
209
|
-
const confirmMode = resolveConfirmationMode({
|
|
210
|
-
allowCiBypass: true,
|
|
211
|
-
args: deployArgs,
|
|
212
|
-
createInterfaceImpl,
|
|
213
|
-
processEnv,
|
|
214
|
-
});
|
|
215
|
-
// Without a recorded prod app, the first create always needs confirmation, so a
|
|
216
|
-
// non-interactive run fails before any platform lookup or create request.
|
|
217
|
-
if (!prodAppId && confirmMode === CONFIRMATION_MODE.NON_INTERACTIVE) {
|
|
218
|
-
await confirmFirstProductionDeploy({ appId, errorImpl, infoImpl, logImpl, mode: confirmMode, region });
|
|
219
|
-
exitImpl(1);
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
const reconciledTarget = await reconcileProductionAppTarget({
|
|
223
|
-
appId,
|
|
224
|
-
prodAppId,
|
|
225
|
-
creds,
|
|
226
|
-
displayName,
|
|
227
|
-
gencowJsonPath,
|
|
228
|
-
platformFetchImpl,
|
|
229
|
-
createCloudAppIfNeeded,
|
|
230
|
-
infoImpl,
|
|
231
|
-
errorImpl,
|
|
232
|
-
exitImpl,
|
|
233
|
-
});
|
|
234
|
-
if (!reconciledTarget) return null;
|
|
235
|
-
const productionParentAppId = reconciledTarget.appId;
|
|
236
|
-
const existingProdAppId = reconciledTarget.prodAppId;
|
|
237
|
-
if (existingProdAppId) return existingProdAppId;
|
|
238
|
-
if (!productionParentAppId) return null;
|
|
239
|
-
|
|
240
|
-
const proceed = await confirmFirstProductionDeploy({
|
|
241
|
-
appId: productionParentAppId,
|
|
242
|
-
createInterfaceImpl,
|
|
243
|
-
errorImpl,
|
|
244
|
-
infoImpl,
|
|
245
|
-
logImpl,
|
|
246
|
-
mode: confirmMode,
|
|
247
|
-
region,
|
|
248
|
-
});
|
|
249
|
-
if (!proceed) {
|
|
250
|
-
exitImpl(1);
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
infoImpl("Creating prod app...");
|
|
255
|
-
const createProdRes = await platformFetchImpl(
|
|
256
|
-
creds,
|
|
257
|
-
`/platform/apps/${productionParentAppId}/create-prod`,
|
|
258
|
-
{
|
|
259
|
-
method: "POST",
|
|
260
|
-
headers: { "Content-Type": "application/json" },
|
|
261
|
-
body: JSON.stringify(region ? { region } : {}),
|
|
262
|
-
},
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
if (!createProdRes.ok) {
|
|
266
|
-
const errData = await readJsonObjectResponse(createProdRes);
|
|
267
|
-
if (createProdRes.status === 403) {
|
|
268
|
-
logImpl("");
|
|
269
|
-
logImpl(` ${RED}⛔ Production Deploy — Startup or Enterprise required${RESET}`);
|
|
270
|
-
logImpl("");
|
|
271
|
-
logImpl(` ${DIM}gencow deploy --prod is for production deployment.${RESET}`);
|
|
272
|
-
logImpl("");
|
|
273
|
-
logImpl(` ${BOLD}💡 Dev environment deploy:${RESET}`);
|
|
274
|
-
logImpl(` ${GREEN}gencow deploy${RESET} ${DIM}← Deploy backend to dev${RESET}`);
|
|
275
|
-
logImpl(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
|
|
276
|
-
logImpl(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
|
|
277
|
-
logImpl("");
|
|
278
|
-
logImpl(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
|
|
279
|
-
logImpl(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Startup${RESET}`);
|
|
280
|
-
logImpl("");
|
|
281
|
-
} else {
|
|
282
|
-
errorImpl(prodCreateFailureMessage(errData, createProdRes));
|
|
283
|
-
}
|
|
284
|
-
exitImpl(1);
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const createProdData = await readJsonObjectResponse(createProdRes);
|
|
289
|
-
if (!createProdData) {
|
|
290
|
-
errorImpl("Prod app creation failed: server returned an invalid JSON response");
|
|
291
|
-
exitImpl(1);
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
const nextProdAppId = createProdData.prodApp;
|
|
295
|
-
const gencowJson = existsSync(gencowJsonPath) ? JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8")) : {};
|
|
296
|
-
gencowJson.prodApp = nextProdAppId;
|
|
297
|
-
writeFileSyncImpl(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
|
|
298
|
-
|
|
299
|
-
if (createProdData.alreadyExists) {
|
|
300
|
-
infoImpl(`Prod app verified: ${nextProdAppId}`);
|
|
301
|
-
if (region && createProdData.region && createProdData.region !== region) {
|
|
302
|
-
infoImpl(
|
|
303
|
-
`Region unchanged: existing prod app is in ${createProdData.region}; --region only applies to first prod creation.`,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
} else {
|
|
307
|
-
successImpl(`Prod app created: ${nextProdAppId}`);
|
|
308
|
-
infoImpl(`URL: ${createProdData.url}`);
|
|
309
|
-
if (createProdData.region) {
|
|
310
|
-
infoImpl(`Region: ${createProdData.region} (${createProdData.regionSource || "selected"})`);
|
|
311
|
-
}
|
|
312
|
-
await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return nextProdAppId;
|
|
186
|
+
// Returns the prepared backend source, or null when the capability check rejected it
|
|
187
|
+
// (the rejection is already rendered and exit is already requested).
|
|
188
|
+
async function prepareBackendSourceForProdCreate(runtime) {
|
|
189
|
+
if ((await runtime.preflightBackendCapability?.()) === false) return null;
|
|
190
|
+
const generatorVersion = await runtime.prepareDeployProjectSource?.({ existingBundleMode: false });
|
|
191
|
+
return { runtime, generatorVersion };
|
|
316
192
|
}
|
|
317
193
|
|
|
318
194
|
async function runRollbackDeploy({
|
|
@@ -528,12 +404,15 @@ export function createDeployCommand({
|
|
|
528
404
|
try {
|
|
529
405
|
staticArtifactPreflight = preflightStaticDeployArtifactImpl({
|
|
530
406
|
staticDirArg: resolvedStaticDirArg,
|
|
407
|
+
// Display text is what the user typed or the client config says, never the resolved path.
|
|
408
|
+
staticDirDisplay: parsed.staticDirArg ?? selectedClient?.static?.dir ?? null,
|
|
409
|
+
staticDirSource: parsed.staticDirArg ? "argument" : "config",
|
|
531
410
|
cwd: staticArtifactCwd,
|
|
532
411
|
existsSyncImpl,
|
|
533
412
|
resolvePathImpl,
|
|
534
413
|
});
|
|
535
414
|
} catch (manifestError) {
|
|
536
|
-
errorImpl(
|
|
415
|
+
errorImpl(formatCommandError(manifestError));
|
|
537
416
|
exitImpl(1);
|
|
538
417
|
return;
|
|
539
418
|
}
|
|
@@ -614,28 +493,46 @@ export function createDeployCommand({
|
|
|
614
493
|
return;
|
|
615
494
|
}
|
|
616
495
|
|
|
496
|
+
// Backend source preparation (capability check, migration generation, codegen) runs
|
|
497
|
+
// before the production app is created, so a rejected source creates no remote app.
|
|
498
|
+
// It is prepared once here and handed to deployBackendPackage/runStaticDeployFlow.
|
|
499
|
+
let preparedSource = null;
|
|
617
500
|
if (parsed.envTarget === "prod" && appId && !parsed.existingBundleMode && !parsed.resumeRequested) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
501
|
+
// A static-only deploy has no backend source to prepare.
|
|
502
|
+
const preparesBackendSource = !parsed.staticDeploy || hasDetectedBackend;
|
|
503
|
+
let prodTarget = null;
|
|
504
|
+
try {
|
|
505
|
+
prodTarget = await ensureProductionDeployTarget({
|
|
506
|
+
appId: rootAppId,
|
|
507
|
+
displayName,
|
|
508
|
+
createCloudAppIfNeeded: backendDeployRuntime.createCloudAppIfNeeded,
|
|
509
|
+
createInterfaceImpl,
|
|
510
|
+
creds,
|
|
511
|
+
deployArgs,
|
|
512
|
+
gencowJsonPath,
|
|
513
|
+
platformFetchImpl,
|
|
514
|
+
processEnv,
|
|
515
|
+
readFileSyncImpl,
|
|
516
|
+
setTimeoutImpl,
|
|
517
|
+
successImpl,
|
|
518
|
+
errorImpl,
|
|
519
|
+
infoImpl,
|
|
520
|
+
logImpl,
|
|
521
|
+
writeFileSyncImpl,
|
|
522
|
+
exitImpl,
|
|
523
|
+
prodAppId,
|
|
524
|
+
region: parsed.region,
|
|
525
|
+
prepareLocalSource: preparesBackendSource
|
|
526
|
+
? async () => {
|
|
527
|
+
preparedSource = await prepareBackendSourceForProdCreate(backendDeployRuntime);
|
|
528
|
+
return preparedSource !== null;
|
|
529
|
+
}
|
|
530
|
+
: null,
|
|
531
|
+
});
|
|
532
|
+
} finally {
|
|
533
|
+
// Prepared codegen output is temporary; drop it when no production app was created.
|
|
534
|
+
if (!prodTarget) backendDeployRuntime.cleanupPreparedDeploySource?.();
|
|
535
|
+
}
|
|
639
536
|
if (!prodTarget) return;
|
|
640
537
|
appId = prodTarget;
|
|
641
538
|
infoImpl(`Deploy target: ${CYAN}${appId}${RESET} (production)`);
|
|
@@ -649,6 +546,7 @@ export function createDeployCommand({
|
|
|
649
546
|
if (parsed.staticDeploy) {
|
|
650
547
|
await runStaticDeployFlow({
|
|
651
548
|
appId,
|
|
549
|
+
preparedSource,
|
|
652
550
|
backendRoot: backendProjectRoot,
|
|
653
551
|
creds,
|
|
654
552
|
displayName,
|
|
@@ -736,6 +634,7 @@ export function createDeployCommand({
|
|
|
736
634
|
|
|
737
635
|
await deployBackendPackage({
|
|
738
636
|
activePackageRuntime,
|
|
637
|
+
preparedSource,
|
|
739
638
|
appId,
|
|
740
639
|
creds,
|
|
741
640
|
displayName,
|