gencow 0.1.244 → 0.1.246
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 -2
- package/lib/deploy-admission-boundary.mjs +52 -0
- package/lib/deploy-app-creation.mjs +71 -0
- package/lib/deploy-app-target.mjs +182 -0
- package/lib/deploy-failure-diagnostic.mjs +26 -0
- package/lib/deploy-package-runtime.mjs +31 -42
- package/lib/deploy-runtime.mjs +41 -12
- package/lib/deploy-static-fullstack-runtime.mjs +17 -9
- package/lib/deployment-admission-retry.mjs +8 -3
- package/lib/static-deploy-command.mjs +139 -46
- package/lib/static-deploy-receipt-store.mjs +100 -0
- package/package.json +4 -3
- package/runtime/server.mjs +2802 -1903
- package/runtime/tooling.mjs +9 -9
package/lib/app-command.mjs
CHANGED
|
@@ -164,7 +164,7 @@ async function confirmDelete(name) {
|
|
|
164
164
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
165
165
|
const answer = await new Promise((resolveAnswer) =>
|
|
166
166
|
rl.question(
|
|
167
|
-
`\n ${RED}!${RESET} Remove "${BOLD}${name}${RESET}" from your account? An administrator will stop it and permanently delete its data.
|
|
167
|
+
`\n ${RED}!${RESET} Remove "${BOLD}${name}${RESET}" from your account? An administrator will stop it and permanently delete its data. Redeploying this project can create a new app ID with the same display name.\n Type the app name to confirm: `,
|
|
168
168
|
resolveAnswer,
|
|
169
169
|
),
|
|
170
170
|
);
|
|
@@ -408,7 +408,7 @@ ${dashboardLine}
|
|
|
408
408
|
saveCredsImpl({ ...currentCreds, currentApp: undefined });
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
successImpl(`App "${name}" removed from your account. An administrator will permanently delete it.
|
|
411
|
+
successImpl(`App "${name}" removed from your account. An administrator will stop it and permanently delete it. Redeploying this project can create a new app ID with the same display name.`);
|
|
412
412
|
if (delData.stopErrorCode) {
|
|
413
413
|
warnImpl(`App stop needs administrator review. Code: ${delData.stopErrorCode}. Handoff ID: ${delData.handoffId}`);
|
|
414
414
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const DEFAULT_ADMISSION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
2
|
+
|
|
3
|
+
function pendingResponse({ requestId, appName, resumeCommand = null }) {
|
|
4
|
+
const response = new Response(
|
|
5
|
+
JSON.stringify({
|
|
6
|
+
error: "Deployment admission is still running. The server request was not cancelled.",
|
|
7
|
+
code: "PLATFORM_DEPLOYMENT_ADMISSION_PENDING",
|
|
8
|
+
stage: "admission",
|
|
9
|
+
retryClass: "retryable-infra",
|
|
10
|
+
userAction: "VIEW_STATUS",
|
|
11
|
+
requestId,
|
|
12
|
+
appName,
|
|
13
|
+
statusCommand: "gencow deploy status",
|
|
14
|
+
resumeCommand,
|
|
15
|
+
serverOperationContinues: "UNKNOWN",
|
|
16
|
+
databaseState: "UNKNOWN",
|
|
17
|
+
incumbentServing: "UNKNOWN",
|
|
18
|
+
operationState: "UNKNOWN",
|
|
19
|
+
}),
|
|
20
|
+
{ status: 503, headers: { "Content-Type": "application/json", "Retry-After": "5" } },
|
|
21
|
+
);
|
|
22
|
+
// The caller must surface this state without replaying the same admission
|
|
23
|
+
// request as a new deployment. Keep this marker private to the in-process
|
|
24
|
+
// response path; the public body carries the stable diagnostic contract.
|
|
25
|
+
response.deploymentAdmissionPending = true;
|
|
26
|
+
return response;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function postDeploymentAdmissionWithTimeout({
|
|
30
|
+
send,
|
|
31
|
+
requestId = null,
|
|
32
|
+
appName,
|
|
33
|
+
resumeCommand = requestId ? `gencow deploy --resume ${requestId}` : null,
|
|
34
|
+
timeoutMs = DEFAULT_ADMISSION_TIMEOUT_MS,
|
|
35
|
+
setTimeoutImpl = setTimeout,
|
|
36
|
+
clearTimeoutImpl = clearTimeout,
|
|
37
|
+
}) {
|
|
38
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new Error("DEPLOYMENT_ADMISSION_TIMEOUT_INVALID");
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
let timer;
|
|
41
|
+
const deadline = new Promise((resolve) => {
|
|
42
|
+
timer = setTimeoutImpl(() => {
|
|
43
|
+
resolve(pendingResponse({ requestId, appName, resumeCommand }));
|
|
44
|
+
}, timeoutMs);
|
|
45
|
+
timer?.unref?.();
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
return await Promise.race([send({ signal: controller.signal }), deadline]);
|
|
49
|
+
} finally {
|
|
50
|
+
if (timer !== undefined) clearTimeoutImpl(timer);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { resolveCreatedAppResponse } from "./app-create-response.mjs";
|
|
2
|
+
import { resolveDeployAppId } from "./deploy-app-target.mjs";
|
|
3
|
+
import { writeProjectMetadata } from "./deploy-project-metadata.mjs";
|
|
4
|
+
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
5
|
+
import { DIM, RESET } from "./output.mjs";
|
|
6
|
+
|
|
7
|
+
export function createCloudAppIfNeeded({
|
|
8
|
+
platformFetchImpl,
|
|
9
|
+
rpcMutationImpl,
|
|
10
|
+
errorImpl,
|
|
11
|
+
infoImpl,
|
|
12
|
+
exitImpl,
|
|
13
|
+
successImpl,
|
|
14
|
+
setTimeoutImpl,
|
|
15
|
+
writeFileSyncImpl,
|
|
16
|
+
}) {
|
|
17
|
+
return async function createAppIfNeeded({
|
|
18
|
+
creds,
|
|
19
|
+
appId,
|
|
20
|
+
displayName,
|
|
21
|
+
envTarget = "dev",
|
|
22
|
+
gencowJsonPath,
|
|
23
|
+
allowMissingAppReplacement = true,
|
|
24
|
+
}) {
|
|
25
|
+
const resolvedAppId = await resolveDeployAppId({
|
|
26
|
+
creds,
|
|
27
|
+
appId,
|
|
28
|
+
displayName,
|
|
29
|
+
environment: envTarget,
|
|
30
|
+
platformFetchImpl,
|
|
31
|
+
allowMissingAppReplacement,
|
|
32
|
+
infoImpl,
|
|
33
|
+
errorImpl,
|
|
34
|
+
exitImpl,
|
|
35
|
+
});
|
|
36
|
+
if (resolvedAppId === false) return null;
|
|
37
|
+
if (resolvedAppId) return resolvedAppId;
|
|
38
|
+
|
|
39
|
+
infoImpl("Creating app (auto-generating ID)...");
|
|
40
|
+
const createRes = await rpcMutationImpl(creds, "apps.create", { name: displayName });
|
|
41
|
+
if (!createRes.ok) {
|
|
42
|
+
const createErr = await readJsonObjectResponse(createRes);
|
|
43
|
+
errorImpl(`App creation failed: ${createErr?.error || createRes.statusText}`);
|
|
44
|
+
exitImpl(1);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const createData = await readJsonObjectResponse(createRes);
|
|
49
|
+
let nextAppId;
|
|
50
|
+
try {
|
|
51
|
+
nextAppId = resolveCreatedAppResponse(createData, { platformUrl: creds.platformUrl }).appId;
|
|
52
|
+
} catch (caught) {
|
|
53
|
+
errorImpl(`App creation failed: ${caught.message}`);
|
|
54
|
+
exitImpl(1);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
successImpl(`App "${nextAppId}" created. Waiting for provisioning...`);
|
|
59
|
+
writeProjectMetadata({
|
|
60
|
+
appId: nextAppId,
|
|
61
|
+
displayName,
|
|
62
|
+
envTarget,
|
|
63
|
+
platformUrl: creds.platformUrl,
|
|
64
|
+
gencowJsonPath,
|
|
65
|
+
writeFileSyncImpl,
|
|
66
|
+
});
|
|
67
|
+
infoImpl(`${DIM}gencow.json created (appId: ${nextAppId})${RESET}`);
|
|
68
|
+
await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
|
|
69
|
+
return nextAppId;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
2
|
+
|
|
3
|
+
const PUBLIC_LOOKUP_ERROR_CODES = new Set([
|
|
4
|
+
"ACCOUNT_DELETED",
|
|
5
|
+
"ACCOUNT_DELETION_PENDING",
|
|
6
|
+
"ACCOUNT_SUSPENDED",
|
|
7
|
+
"APP_RELEASE_AUTH_REQUIRED",
|
|
8
|
+
"APP_RELEASE_CAPABILITY_UNAVAILABLE",
|
|
9
|
+
"APP_RELEASE_ENVIRONMENT_INVALID",
|
|
10
|
+
"APP_RELEASE_POLICY_READ_UNAVAILABLE",
|
|
11
|
+
]);
|
|
12
|
+
const SAFE_CORRELATION_ID = /^[A-Za-z0-9_-]{8,128}$/u;
|
|
13
|
+
|
|
14
|
+
function publicLookupFailure(response, body) {
|
|
15
|
+
const code = PUBLIC_LOOKUP_ERROR_CODES.has(body?.code)
|
|
16
|
+
? body.code
|
|
17
|
+
: response.status === 401
|
|
18
|
+
? "APP_RELEASE_AUTH_REQUIRED"
|
|
19
|
+
: "APP_DEPLOY_TARGET_LOOKUP_FAILED";
|
|
20
|
+
return {
|
|
21
|
+
kind: "error",
|
|
22
|
+
code,
|
|
23
|
+
status: response.status,
|
|
24
|
+
retryable: typeof body?.retryable === "boolean" ? body.retryable : response.status >= 500,
|
|
25
|
+
...(typeof body?.correlationId === "string" && SAFE_CORRELATION_ID.test(body.correlationId)
|
|
26
|
+
? { correlationId: body.correlationId }
|
|
27
|
+
: {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatLookupFailure(failure, target) {
|
|
32
|
+
const code = failure?.kind === "error" ? failure.code : "APP_DEPLOY_TARGET_LOOKUP_FAILED";
|
|
33
|
+
const lines = [
|
|
34
|
+
`${code}: Could not verify the configured ${target} app. No replacement app was created and gencow.json was unchanged.`,
|
|
35
|
+
];
|
|
36
|
+
if (failure?.status) lines.push(`HTTP status: ${failure.status}`);
|
|
37
|
+
if (typeof failure?.retryable === "boolean") lines.push(`Retryable: ${failure.retryable ? "yes" : "no"}`);
|
|
38
|
+
if (failure?.correlationId) lines.push(`Correlation ID: ${failure.correlationId}`);
|
|
39
|
+
if (code === "APP_RELEASE_AUTH_REQUIRED") {
|
|
40
|
+
lines.push("Action: run gencow login, or replace/unset the environment credential, then retry.");
|
|
41
|
+
} else if (code === "ACCOUNT_SUSPENDED" || code === "ACCOUNT_DELETION_PENDING" || code === "ACCOUNT_DELETED") {
|
|
42
|
+
lines.push("Action: resolve the account status in Platform before retrying.");
|
|
43
|
+
} else if (failure?.retryable) {
|
|
44
|
+
lines.push("Action: wait for Platform release capability to recover, then retry.");
|
|
45
|
+
} else {
|
|
46
|
+
lines.push("Action: check that the credential can access this app, then retry.");
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function lookupOwnedApp({ creds, appId, environment, platformFetchImpl }) {
|
|
52
|
+
try {
|
|
53
|
+
const response = await platformFetchImpl(
|
|
54
|
+
creds,
|
|
55
|
+
`/platform/apps/${appId}/release-capability?environment=${environment}`,
|
|
56
|
+
{ method: "GET" },
|
|
57
|
+
);
|
|
58
|
+
const body = await readJsonObjectResponse(response);
|
|
59
|
+
if (response.status === 404) {
|
|
60
|
+
if (body?.code === "APP_RELEASE_APP_NOT_FOUND") return "missing";
|
|
61
|
+
return body?.code ? publicLookupFailure(response, body) : "unsupported";
|
|
62
|
+
}
|
|
63
|
+
if (response.status === 409 && body?.code === "APP_RELEASE_VERSIONS_DISABLED") return "found";
|
|
64
|
+
if (!response.ok) return publicLookupFailure(response, body);
|
|
65
|
+
return body?.protocol === "app-release-v1" && typeof body.enabled === "boolean" ? "found" : "error";
|
|
66
|
+
} catch {
|
|
67
|
+
return {
|
|
68
|
+
kind: "error",
|
|
69
|
+
code: "APP_RELEASE_CAPABILITY_UNAVAILABLE",
|
|
70
|
+
retryable: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function resolveDeployAppId({
|
|
76
|
+
creds,
|
|
77
|
+
appId,
|
|
78
|
+
displayName,
|
|
79
|
+
environment = "dev",
|
|
80
|
+
platformFetchImpl,
|
|
81
|
+
allowMissingAppReplacement,
|
|
82
|
+
infoImpl,
|
|
83
|
+
errorImpl,
|
|
84
|
+
exitImpl,
|
|
85
|
+
}) {
|
|
86
|
+
if (!appId || !platformFetchImpl) return appId;
|
|
87
|
+
const lookup = await lookupOwnedApp({ creds, appId, environment, platformFetchImpl });
|
|
88
|
+
if (lookup === "found" || lookup === "unsupported") return appId;
|
|
89
|
+
if (lookup?.kind === "error" || lookup === "error") {
|
|
90
|
+
errorImpl(formatLookupFailure(lookup, `"${appId}"`));
|
|
91
|
+
exitImpl(1);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!allowMissingAppReplacement) {
|
|
95
|
+
errorImpl(
|
|
96
|
+
`APP_NOT_FOUND: The existing app ID "${appId}" is unavailable. This deployment requires the original app; restore access before retrying. No replacement app was created.`,
|
|
97
|
+
);
|
|
98
|
+
exitImpl(1);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
infoImpl(`Configured app ID is no longer available. Creating a new app named "${displayName}"...`);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function reconcileProductionAppTarget({
|
|
106
|
+
appId,
|
|
107
|
+
prodAppId,
|
|
108
|
+
creds,
|
|
109
|
+
displayName,
|
|
110
|
+
gencowJsonPath,
|
|
111
|
+
platformFetchImpl,
|
|
112
|
+
createCloudAppIfNeeded,
|
|
113
|
+
infoImpl,
|
|
114
|
+
errorImpl,
|
|
115
|
+
exitImpl,
|
|
116
|
+
}) {
|
|
117
|
+
let productionParentAppId = appId;
|
|
118
|
+
let existingProdAppId = prodAppId;
|
|
119
|
+
if (!platformFetchImpl || (!productionParentAppId && !existingProdAppId)) {
|
|
120
|
+
return { appId: productionParentAppId, prodAppId: existingProdAppId };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const failLookup = (failure, target) => {
|
|
124
|
+
errorImpl(formatLookupFailure(failure, target));
|
|
125
|
+
exitImpl(1);
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (existingProdAppId) {
|
|
130
|
+
const prodState = await lookupOwnedApp({
|
|
131
|
+
creds,
|
|
132
|
+
appId: existingProdAppId,
|
|
133
|
+
environment: "prod",
|
|
134
|
+
platformFetchImpl,
|
|
135
|
+
});
|
|
136
|
+
if (prodState?.kind === "error" || prodState === "error") return failLookup(prodState, "production");
|
|
137
|
+
if (prodState === "found" || prodState === "unsupported") {
|
|
138
|
+
return { appId: productionParentAppId, prodAppId: existingProdAppId };
|
|
139
|
+
}
|
|
140
|
+
infoImpl(
|
|
141
|
+
`Configured production app ID is no longer available. Creating a production app for "${displayName}"...`,
|
|
142
|
+
);
|
|
143
|
+
existingProdAppId = null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const parentState = productionParentAppId
|
|
147
|
+
? await lookupOwnedApp({
|
|
148
|
+
creds,
|
|
149
|
+
appId: productionParentAppId,
|
|
150
|
+
environment: "dev",
|
|
151
|
+
platformFetchImpl,
|
|
152
|
+
})
|
|
153
|
+
: "missing";
|
|
154
|
+
if (productionParentAppId && (parentState?.kind === "error" || parentState === "error")) {
|
|
155
|
+
return failLookup(parentState, "root");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (productionParentAppId && parentState === "missing") {
|
|
159
|
+
infoImpl(`Configured app ID is no longer available. Creating a new app named "${displayName}"...`);
|
|
160
|
+
productionParentAppId = await createCloudAppIfNeeded({
|
|
161
|
+
creds,
|
|
162
|
+
appId: null,
|
|
163
|
+
displayName,
|
|
164
|
+
envTarget: "dev",
|
|
165
|
+
gencowJsonPath,
|
|
166
|
+
});
|
|
167
|
+
if (!productionParentAppId) return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!productionParentAppId) {
|
|
171
|
+
productionParentAppId = await createCloudAppIfNeeded({
|
|
172
|
+
creds,
|
|
173
|
+
appId: null,
|
|
174
|
+
displayName,
|
|
175
|
+
envTarget: "dev",
|
|
176
|
+
gencowJsonPath,
|
|
177
|
+
});
|
|
178
|
+
if (!productionParentAppId) return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { appId: productionParentAppId, prodAppId: existingProdAppId };
|
|
182
|
+
}
|
|
@@ -310,6 +310,32 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText, opt
|
|
|
310
310
|
Number.isSafeInteger(Number(contract.operationId))
|
|
311
311
|
? `gencow deploy status --deployment ${contract.operationId}`
|
|
312
312
|
: "gencow deploy status";
|
|
313
|
+
if (contract.code === "PLATFORM_DEPLOYMENT_ADMISSION_PENDING") {
|
|
314
|
+
lines.push(
|
|
315
|
+
`Database state: ${safeText(responseBody.databaseState, 32) ?? "UNKNOWN"}`,
|
|
316
|
+
`Incumbent serving: ${safeText(responseBody.incumbentServing, 32) ?? "UNKNOWN"}`,
|
|
317
|
+
`Operation state: ${safeText(responseBody.operationState, 32) ?? "UNKNOWN"}`,
|
|
318
|
+
);
|
|
319
|
+
const requestId = safeText(responseBody.requestId, 128);
|
|
320
|
+
if (requestId && /^[A-Za-z0-9_-]{8,128}$/u.test(requestId)) lines.push(`Request ID: ${requestId}`);
|
|
321
|
+
lines.push("Server outcome: unknown; inspect the existing request before submitting another deployment.");
|
|
322
|
+
const resumeCommand = safeText(responseBody.resumeCommand, 240);
|
|
323
|
+
const resumeMatch = resumeCommand?.match(
|
|
324
|
+
/^gencow deploy (?:--resume [A-Za-z0-9_-]{8,128}|--static(?: --prod)? --app ([A-Za-z0-9_-]{1,128})(?: --client [A-Za-z0-9_-]{1,128})? --resume [A-Za-z0-9_-]{8,128})$/u,
|
|
325
|
+
);
|
|
326
|
+
const resumeToken = resumeCommand?.match(/--resume ([A-Za-z0-9_-]{8,128})$/u)?.[1];
|
|
327
|
+
const resumeAppName = resumeMatch?.[1];
|
|
328
|
+
const expectedResumeApp = options.expectedAppName ?? responseBody.appName;
|
|
329
|
+
if (
|
|
330
|
+
resumeMatch &&
|
|
331
|
+
requestId &&
|
|
332
|
+
resumeToken === requestId &&
|
|
333
|
+
(!resumeAppName || !expectedResumeApp || resumeAppName === expectedResumeApp)
|
|
334
|
+
) {
|
|
335
|
+
lines.push(`Next command: ${resumeCommand}`);
|
|
336
|
+
return lines;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
313
339
|
lines.push(`Next command: ${selfService?.action.command ?? exactStatusCommand}`);
|
|
314
340
|
if (contract.blockingOperationId && responseBody.retryReadinessReason === "ACTIVE_OWNER" && selfService) {
|
|
315
341
|
lines.push(`Then check retry gate: ${exactStatusCommand}`);
|
|
@@ -3,9 +3,8 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
4
4
|
import { dirname, resolve } from "path";
|
|
5
5
|
import { fetchAppDiagnosticLogsViaRest, normalizeDiagnosticLogLines } from "./app-diagnostics.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { createCloudAppIfNeeded as createCloudAppIfNeededImpl } from "./deploy-app-creation.mjs";
|
|
7
7
|
import { resolveConfiguredAuditorImplementation } from "./auditor-mode.mjs";
|
|
8
|
-
import { writeProjectMetadata } from "./deploy-project-metadata.mjs";
|
|
9
8
|
import {
|
|
10
9
|
loadDeployMigrationGenerationContext,
|
|
11
10
|
prepareDeployCodegenArtifacts,
|
|
@@ -21,6 +20,7 @@ import { buildDeployClaimFromArchive } from "./deploy-package-claim.mjs";
|
|
|
21
20
|
import { renderDeployFailureDiagnosticLines } from "./deploy-failure-diagnostic.mjs";
|
|
22
21
|
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
23
22
|
import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs";
|
|
23
|
+
import { postDeploymentAdmissionWithTimeout } from "./deploy-admission-boundary.mjs";
|
|
24
24
|
import { submitDeploymentWithIdempotentRetry } from "./deployment-admission-retry.mjs";
|
|
25
25
|
import { acquireDeployReceipt, releaseDeployReceipt } from "./deploy-receipt-store.mjs";
|
|
26
26
|
import { CYAN, DIM, RED, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
|
|
@@ -155,6 +155,7 @@ export function createDeployPackageRuntime({
|
|
|
155
155
|
} = {}) {
|
|
156
156
|
let migrationProjectConfig = null;
|
|
157
157
|
let preparedDeployCodegenArtifacts = null;
|
|
158
|
+
let lastDeployOutcome = "idle";
|
|
158
159
|
function cleanupPreparedDeploySource() {
|
|
159
160
|
preparedDeployCodegenArtifacts?.cleanup?.();
|
|
160
161
|
preparedDeployCodegenArtifacts = null;
|
|
@@ -557,41 +558,16 @@ export function createDeployPackageRuntime({
|
|
|
557
558
|
return { bundleBuffer, bundleSize, deployClaim, tmpBundle };
|
|
558
559
|
}
|
|
559
560
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const createData = await readJsonObjectResponse(createRes);
|
|
573
|
-
let nextAppId;
|
|
574
|
-
try {
|
|
575
|
-
nextAppId = resolveCreatedAppResponse(createData, { platformUrl: creds.platformUrl }).appId;
|
|
576
|
-
} catch (caught) {
|
|
577
|
-
errorImpl(`App creation failed: ${caught.message}`);
|
|
578
|
-
exitImpl(1);
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
successImpl(`App "${nextAppId}" created. Waiting for provisioning...`);
|
|
583
|
-
writeProjectMetadata({
|
|
584
|
-
appId: nextAppId,
|
|
585
|
-
displayName,
|
|
586
|
-
envTarget,
|
|
587
|
-
platformUrl: creds.platformUrl,
|
|
588
|
-
gencowJsonPath,
|
|
589
|
-
writeFileSyncImpl,
|
|
590
|
-
});
|
|
591
|
-
infoImpl(`${DIM}gencow.json created (appId: ${nextAppId})${RESET}`);
|
|
592
|
-
await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
|
|
593
|
-
return nextAppId;
|
|
594
|
-
}
|
|
561
|
+
const createCloudAppIfNeeded = createCloudAppIfNeededImpl({
|
|
562
|
+
platformFetchImpl,
|
|
563
|
+
rpcMutationImpl,
|
|
564
|
+
errorImpl,
|
|
565
|
+
infoImpl,
|
|
566
|
+
exitImpl,
|
|
567
|
+
successImpl,
|
|
568
|
+
setTimeoutImpl,
|
|
569
|
+
writeFileSyncImpl,
|
|
570
|
+
});
|
|
595
571
|
|
|
596
572
|
function startSpinner(label) {
|
|
597
573
|
const spinnerFrames = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
@@ -622,6 +598,7 @@ export function createDeployPackageRuntime({
|
|
|
622
598
|
releaseAttemptId = null,
|
|
623
599
|
clientName = null,
|
|
624
600
|
}) {
|
|
601
|
+
lastDeployOutcome = "pending";
|
|
625
602
|
const cwd = cwdImpl();
|
|
626
603
|
if (deployClaim?.claim?.migrationBundle?.manifestSha256) {
|
|
627
604
|
emitMigrationResponsibilityNotice(warnImpl);
|
|
@@ -638,7 +615,7 @@ export function createDeployPackageRuntime({
|
|
|
638
615
|
: null;
|
|
639
616
|
let activeBundleBuffer = receipt?.bundleBuffer ?? bundleBuffer;
|
|
640
617
|
let activeDeployClaim = receipt?.deployClaim ?? deployClaim;
|
|
641
|
-
const deployOnce = async (targetAppId) =>
|
|
618
|
+
const deployOnce = async (targetAppId, signal) =>
|
|
642
619
|
platformFetchImpl(creds, `/platform/apps/${targetAppId}/deploy?env=${envTarget}`, {
|
|
643
620
|
method: "POST",
|
|
644
621
|
headers: {
|
|
@@ -659,6 +636,7 @@ export function createDeployPackageRuntime({
|
|
|
659
636
|
: {}),
|
|
660
637
|
},
|
|
661
638
|
body: activeBundleBuffer,
|
|
639
|
+
signal,
|
|
662
640
|
});
|
|
663
641
|
const pollAcceptedDeployment = (initialResponse) =>
|
|
664
642
|
pollAcceptedDeploymentOperation({
|
|
@@ -669,11 +647,15 @@ export function createDeployPackageRuntime({
|
|
|
669
647
|
setTimeoutImpl,
|
|
670
648
|
});
|
|
671
649
|
const submitDeployAdmission = () => {
|
|
672
|
-
// A legacy request without a deploy claim has no stable pre-receipt
|
|
673
|
-
// identity. Never retry that request automatically.
|
|
674
|
-
if (!activeDeployClaim) return deployOnce(targetAppId);
|
|
675
650
|
return submitDeploymentWithIdempotentRetry({
|
|
676
|
-
submit: () =>
|
|
651
|
+
submit: () =>
|
|
652
|
+
postDeploymentAdmissionWithTimeout({
|
|
653
|
+
requestId: activeDeployClaim?.claim?.requestId ?? explicitRequestId,
|
|
654
|
+
appName: targetAppId,
|
|
655
|
+
send: ({ signal }) => deployOnce(targetAppId, signal),
|
|
656
|
+
}),
|
|
657
|
+
retryOnResponse: Boolean(activeDeployClaim),
|
|
658
|
+
retryOnTransport: Boolean(activeDeployClaim),
|
|
677
659
|
sleepImpl: (ms) => new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, ms)),
|
|
678
660
|
});
|
|
679
661
|
};
|
|
@@ -705,6 +687,9 @@ export function createDeployPackageRuntime({
|
|
|
705
687
|
if (deployRes.status === 202) {
|
|
706
688
|
deployRes = await pollAcceptedDeployment(deployRes);
|
|
707
689
|
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
lastDeployOutcome = "pending";
|
|
692
|
+
throw error;
|
|
708
693
|
} finally {
|
|
709
694
|
stopSpinner(spinner);
|
|
710
695
|
}
|
|
@@ -753,6 +738,7 @@ export function createDeployPackageRuntime({
|
|
|
753
738
|
errData?.code === "DEPLOY_REQUEST_IN_PROGRESS" ||
|
|
754
739
|
errData?.code === "CONTROL_PLANE_OBSERVATION_INCOMPLETE" ||
|
|
755
740
|
errData?.retryClass === "retryable-infra";
|
|
741
|
+
lastDeployOutcome = retryableResponse ? "pending" : "failed";
|
|
756
742
|
if (receipt && !retryableResponse) releaseDeployReceipt(receipt);
|
|
757
743
|
const migrationDiagnostic = isActionableMigrationDiagnostic(errData?.diagnostic)
|
|
758
744
|
? normalizeRetiredMigrationGateDiagnostic(extractActionableMigrationDiagnostic(errData.diagnostic))
|
|
@@ -777,6 +763,7 @@ export function createDeployPackageRuntime({
|
|
|
777
763
|
|
|
778
764
|
const deployData = await readJsonObjectResponse(deployRes);
|
|
779
765
|
if (!deployData) {
|
|
766
|
+
lastDeployOutcome = "unknown";
|
|
780
767
|
errorImpl(
|
|
781
768
|
`Deploy failed: no terminal deployment receipt was returned after packaging.\nCode: CLI_DEPLOY_TERMINAL_RECEIPT_MISSING\nStage: controller\nHTTP status: ${deployRes?.status ?? "unknown"}\nNext action: RETRY`,
|
|
782
769
|
);
|
|
@@ -784,6 +771,7 @@ export function createDeployPackageRuntime({
|
|
|
784
771
|
return null;
|
|
785
772
|
}
|
|
786
773
|
emitMigrationAdvisories(deployData?.warnings, warnImpl);
|
|
774
|
+
lastDeployOutcome = "completed";
|
|
787
775
|
if (receipt) releaseDeployReceipt(receipt);
|
|
788
776
|
const deployElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
789
777
|
logImpl("");
|
|
@@ -842,5 +830,6 @@ export function createDeployPackageRuntime({
|
|
|
842
830
|
packageDeployProject,
|
|
843
831
|
createCloudAppIfNeeded,
|
|
844
832
|
deployBundleWithRetry,
|
|
833
|
+
getLastDeployOutcome: () => lastDeployOutcome,
|
|
845
834
|
};
|
|
846
835
|
}
|
package/lib/deploy-runtime.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./deploy-static-fullstack-runtime.mjs";
|
|
18
18
|
import { detectStaticDeployBackend, preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
|
|
19
19
|
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
20
|
+
import { reconcileProductionAppTarget } from "./deploy-app-target.mjs";
|
|
20
21
|
|
|
21
22
|
export function parseDeployArgs(deployArgs) {
|
|
22
23
|
const parsed = {
|
|
@@ -27,10 +28,12 @@ export function parseDeployArgs(deployArgs) {
|
|
|
27
28
|
isRollback: false,
|
|
28
29
|
region: null,
|
|
29
30
|
requestId: null,
|
|
31
|
+
resumeRequested: false,
|
|
30
32
|
staticDeploy: false,
|
|
31
33
|
staticDirArg: null,
|
|
32
34
|
unknownArg: null,
|
|
33
35
|
};
|
|
36
|
+
Object.defineProperty(parsed, "resumeRequested", { value: false, writable: true });
|
|
34
37
|
|
|
35
38
|
for (let index = 0; index < deployArgs.length; index += 1) {
|
|
36
39
|
const arg = deployArgs[index];
|
|
@@ -46,6 +49,7 @@ export function parseDeployArgs(deployArgs) {
|
|
|
46
49
|
break;
|
|
47
50
|
}
|
|
48
51
|
parsed.requestId = value;
|
|
52
|
+
if (arg === "--resume") parsed.resumeRequested = true;
|
|
49
53
|
index += 1;
|
|
50
54
|
} else if (arg.startsWith("--request-id=") || arg.startsWith("--resume=")) {
|
|
51
55
|
const value = arg.slice(arg.indexOf("=") + 1);
|
|
@@ -54,6 +58,7 @@ export function parseDeployArgs(deployArgs) {
|
|
|
54
58
|
break;
|
|
55
59
|
}
|
|
56
60
|
parsed.requestId = value;
|
|
61
|
+
if (arg.startsWith("--resume=")) parsed.resumeRequested = true;
|
|
57
62
|
} else if (arg === "--region") {
|
|
58
63
|
const value = deployArgs[index + 1];
|
|
59
64
|
if (!value || value.startsWith("-")) {
|
|
@@ -192,6 +197,8 @@ async function confirmFirstProductionDeploy({ appId, createInterfaceImpl, infoIm
|
|
|
192
197
|
|
|
193
198
|
async function ensureProductionDeployTarget({
|
|
194
199
|
appId,
|
|
200
|
+
displayName,
|
|
201
|
+
createCloudAppIfNeeded,
|
|
195
202
|
createInterfaceImpl,
|
|
196
203
|
creds,
|
|
197
204
|
deployArgs,
|
|
@@ -209,14 +216,28 @@ async function ensureProductionDeployTarget({
|
|
|
209
216
|
prodAppId = null,
|
|
210
217
|
region = null,
|
|
211
218
|
}) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
const reconciledTarget = await reconcileProductionAppTarget({
|
|
220
|
+
appId,
|
|
221
|
+
prodAppId,
|
|
222
|
+
creds,
|
|
223
|
+
displayName,
|
|
224
|
+
gencowJsonPath,
|
|
225
|
+
platformFetchImpl,
|
|
226
|
+
createCloudAppIfNeeded,
|
|
227
|
+
infoImpl,
|
|
228
|
+
errorImpl,
|
|
229
|
+
exitImpl,
|
|
230
|
+
});
|
|
231
|
+
if (!reconciledTarget) return null;
|
|
232
|
+
const productionParentAppId = reconciledTarget.appId;
|
|
233
|
+
const existingProdAppId = reconciledTarget.prodAppId;
|
|
234
|
+
if (existingProdAppId) return existingProdAppId;
|
|
235
|
+
if (!productionParentAppId) return null;
|
|
215
236
|
|
|
216
237
|
const ciMode = processEnv.CI === "true" || deployArgs.includes("--yes");
|
|
217
238
|
if (!ciMode) {
|
|
218
239
|
const confirmed = await confirmFirstProductionDeploy({
|
|
219
|
-
appId,
|
|
240
|
+
appId: productionParentAppId,
|
|
220
241
|
createInterfaceImpl,
|
|
221
242
|
infoImpl,
|
|
222
243
|
logImpl,
|
|
@@ -225,11 +246,15 @@ async function ensureProductionDeployTarget({
|
|
|
225
246
|
}
|
|
226
247
|
|
|
227
248
|
infoImpl("Creating prod app...");
|
|
228
|
-
const createProdRes = await platformFetchImpl(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
249
|
+
const createProdRes = await platformFetchImpl(
|
|
250
|
+
creds,
|
|
251
|
+
`/platform/apps/${productionParentAppId}/create-prod`,
|
|
252
|
+
{
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: { "Content-Type": "application/json" },
|
|
255
|
+
body: JSON.stringify(region ? { region } : {}),
|
|
256
|
+
},
|
|
257
|
+
);
|
|
233
258
|
|
|
234
259
|
if (!createProdRes.ok) {
|
|
235
260
|
const errData = await readJsonObjectResponse(createProdRes);
|
|
@@ -497,7 +522,7 @@ export function createDeployCommand({
|
|
|
497
522
|
}
|
|
498
523
|
|
|
499
524
|
let staticArtifactPreflight = null;
|
|
500
|
-
if (parsed.staticDeploy) {
|
|
525
|
+
if (parsed.staticDeploy && !parsed.resumeRequested) {
|
|
501
526
|
try {
|
|
502
527
|
staticArtifactPreflight = preflightStaticDeployArtifactImpl({
|
|
503
528
|
staticDirArg: resolvedStaticDirArg,
|
|
@@ -526,6 +551,7 @@ export function createDeployCommand({
|
|
|
526
551
|
resolvePathImpl,
|
|
527
552
|
});
|
|
528
553
|
let { appId, displayName, gencowJsonPath, prodAppId, projectDir } = context;
|
|
554
|
+
const rootAppId = appId;
|
|
529
555
|
appId = resolveEnvironmentDeployTarget({ appId, envTarget: parsed.envTarget, prodAppId });
|
|
530
556
|
const backendProjectRoot = projectDir ?? projectRoot;
|
|
531
557
|
const projectConfig = loadConfigImpl
|
|
@@ -586,9 +612,11 @@ export function createDeployCommand({
|
|
|
586
612
|
return;
|
|
587
613
|
}
|
|
588
614
|
|
|
589
|
-
if (parsed.envTarget === "prod" && appId && !parsed.existingBundleMode) {
|
|
615
|
+
if (parsed.envTarget === "prod" && appId && !parsed.existingBundleMode && !parsed.resumeRequested) {
|
|
590
616
|
const prodTarget = await ensureProductionDeployTarget({
|
|
591
|
-
appId,
|
|
617
|
+
appId: rootAppId,
|
|
618
|
+
displayName,
|
|
619
|
+
createCloudAppIfNeeded: backendDeployRuntime.createCloudAppIfNeeded,
|
|
592
620
|
createInterfaceImpl,
|
|
593
621
|
creds,
|
|
594
622
|
deployArgs,
|
|
@@ -625,6 +653,7 @@ export function createDeployCommand({
|
|
|
625
653
|
envTarget: parsed.envTarget,
|
|
626
654
|
forceDeploy: parsed.forceDeploy,
|
|
627
655
|
requestId: parsed.requestId,
|
|
656
|
+
resumeStaticOnly: parsed.resumeRequested,
|
|
628
657
|
gencowJsonPath,
|
|
629
658
|
hasDetectedBackend,
|
|
630
659
|
selectedClient,
|