gencow 0.1.245 → 0.1.247
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/bin/gencow.mjs +1 -0
- package/core/index.js +15 -2
- package/lib/cli-dev-runtime.mjs +5 -5
- package/lib/cli-project-runtime.mjs +3 -2
- package/lib/cli-version-check.mjs +2 -1
- package/lib/codegen-command.mjs +12 -8
- package/lib/deploy-admission-boundary.mjs +52 -0
- package/lib/deploy-command.mjs +98 -1
- package/lib/deploy-failure-diagnostic.mjs +127 -5
- package/lib/deploy-package-runtime.mjs +41 -15
- package/lib/deploy-receipt-store.mjs +146 -0
- package/lib/deploy-runtime.mjs +13 -3
- package/lib/deploy-static-fullstack-runtime.mjs +92 -38
- package/lib/deployment-admission-retry.mjs +8 -3
- package/lib/dev-local-command.mjs +14 -3
- package/lib/static-deploy-command.mjs +139 -46
- package/lib/static-deploy-receipt-store.mjs +100 -0
- package/package.json +2 -2
- package/runtime/config.mjs +6 -3
- package/runtime/server.mjs +26963 -23319
- package/runtime/tooling.mjs +84 -19
package/bin/gencow.mjs
CHANGED
package/core/index.js
CHANGED
|
@@ -314,17 +314,30 @@ function buildWakeAppBootFailedResult(error) {
|
|
|
314
314
|
const publicMessage = typeof metadata.publicMessage === "string" && metadata.publicMessage.length <= 200 ? metadata.publicMessage : message;
|
|
315
315
|
const reasonCode = typeof metadata.reasonCode === "string" && /^[a-z][a-z0-9_]{2,80}$/.test(metadata.reasonCode) ? metadata.reasonCode : void 0;
|
|
316
316
|
const correlationId = typeof metadata.correlationId === "string" && /^rt_[A-Za-z0-9_-]{8,80}$/.test(metadata.correlationId) ? metadata.correlationId : void 0;
|
|
317
|
+
const safeCode = (value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,99}$/.test(value) ? value : void 0;
|
|
318
|
+
const operationId = typeof metadata.operationId === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(metadata.operationId) ? metadata.operationId : void 0;
|
|
319
|
+
const operationState = metadata.operationState === "completed" || metadata.operationState === "failed" ? metadata.operationState : void 0;
|
|
317
320
|
return {
|
|
318
321
|
ok: false,
|
|
319
322
|
status: "boot_failed",
|
|
320
323
|
error: publicMessage,
|
|
324
|
+
...typeof metadata.hint === "string" && metadata.hint.length <= 500 ? { hint: metadata.hint } : {},
|
|
325
|
+
...typeof metadata.stage === "string" && /^[a-z][a-z0-9_]{2,40}$/.test(metadata.stage) ? { stage: metadata.stage } : {},
|
|
326
|
+
...typeof metadata.nextAction === "string" && metadata.nextAction.length <= 200 ? { nextAction: metadata.nextAction } : {},
|
|
327
|
+
...typeof metadata.stateChanged === "boolean" ? { stateChanged: metadata.stateChanged } : {},
|
|
328
|
+
...Number.isSafeInteger(metadata.retryAfterSec) && Number(metadata.retryAfterSec) > 0 ? { retryAfterSec: Number(metadata.retryAfterSec) } : {},
|
|
329
|
+
...typeof metadata.retryCondition === "string" && metadata.retryCondition.length <= 500 ? { retryCondition: metadata.retryCondition } : {},
|
|
321
330
|
...reasonCode ? { reasonCode } : {},
|
|
322
331
|
...typeof metadata.retryable === "boolean" ? { retryable: metadata.retryable } : {},
|
|
323
|
-
...correlationId ? { correlationId } : {}
|
|
332
|
+
...correlationId ? { correlationId } : {},
|
|
333
|
+
...operationId ? { operationId } : {},
|
|
334
|
+
...operationState ? { operationState } : {},
|
|
335
|
+
...metadata.terminalResultCode === null ? { terminalResultCode: null } : safeCode(metadata.terminalResultCode) ? { terminalResultCode: safeCode(metadata.terminalResultCode) } : {},
|
|
336
|
+
...metadata.lastFailureCode === null ? { lastFailureCode: null } : safeCode(metadata.lastFailureCode) ? { lastFailureCode: safeCode(metadata.lastFailureCode) } : {}
|
|
324
337
|
};
|
|
325
338
|
}
|
|
326
339
|
function isWakeAppDeferredResult(result) {
|
|
327
|
-
return !result.ok && (result.status === "capacity_rejected" || result.status === "queue_timeout");
|
|
340
|
+
return !result.ok && (result.status === "capacity_rejected" || result.status === "queue_timeout" || result.status === "convergence_pending");
|
|
328
341
|
}
|
|
329
342
|
|
|
330
343
|
// ../core/src/platform-capacity-profile.ts
|
package/lib/cli-dev-runtime.mjs
CHANGED
|
@@ -158,11 +158,11 @@ export function createApiCodegenRuntime(options = {}) {
|
|
|
158
158
|
const absoluteFunctions = resolvePathImpl(cwd, backendRoot);
|
|
159
159
|
const publishDir = resolveCodegenPublishDir(cwd, config);
|
|
160
160
|
const artifactsDir = resolveCodegenArtifactsDir(cwd);
|
|
161
|
-
const apiTsPath = resolvePathImpl(publishDir, "api.ts");
|
|
162
|
-
const publishRel = relative(cwd, publishDir).replace(/\\/g, "/") || ".";
|
|
163
|
-
const previousApi = captureApiSnapshot({ apiTsPath, existsSyncImpl, readFileSyncImpl });
|
|
161
|
+
const apiTsPath = publishDir === false ? null : resolvePathImpl(publishDir, "api.ts");
|
|
162
|
+
const publishRel = publishDir === false ? null : relative(cwd, publishDir).replace(/\\/g, "/") || ".";
|
|
163
|
+
const previousApi = publishDir === false ? null : captureApiSnapshot({ apiTsPath, existsSyncImpl, readFileSyncImpl });
|
|
164
164
|
|
|
165
|
-
infoImpl("Generating client API types...");
|
|
165
|
+
infoImpl(publishDir === false ? "Client generation disabled; generating server artifacts..." : "Generating client API types...");
|
|
166
166
|
try {
|
|
167
167
|
const { bundled } = await runCodegenWithReporting({
|
|
168
168
|
config,
|
|
@@ -209,7 +209,7 @@ export function createApiCodegenRuntime(options = {}) {
|
|
|
209
209
|
} else {
|
|
210
210
|
errorImpl(`API Codegen failed: ${caught.message}`);
|
|
211
211
|
}
|
|
212
|
-
try {
|
|
212
|
+
if (publishDir !== false) try {
|
|
213
213
|
writeCodegenFailureStub({
|
|
214
214
|
apiTsPath,
|
|
215
215
|
existsSyncImpl,
|
|
@@ -193,7 +193,7 @@ function parseTsConfigViaRegex(src) {
|
|
|
193
193
|
if (port !== null) result.port = port;
|
|
194
194
|
|
|
195
195
|
const codegen = {};
|
|
196
|
-
const clientOutDir = extractFrom(codegenBlock, "clientOutDir");
|
|
196
|
+
const clientOutDir = extractFrom(codegenBlock, "clientOutDir") ?? extractBoolFrom(codegenBlock, "clientOutDir");
|
|
197
197
|
if (clientOutDir !== null) codegen.clientOutDir = clientOutDir;
|
|
198
198
|
const outDir = extractFrom(codegenBlock, "outDir");
|
|
199
199
|
if (outDir !== null) codegen.outDir = outDir;
|
|
@@ -401,7 +401,7 @@ export async function loadConfig(options = {}) {
|
|
|
401
401
|
warnImpl("[gencow] `functionsDir` is deprecated; use `rootDir` instead (same meaning).");
|
|
402
402
|
functionsDirDeprecationWarned = true;
|
|
403
403
|
}
|
|
404
|
-
if (trimPath(raw?.codegen?.clientOutDir) && trimPath(raw?.codegen?.outDir)) {
|
|
404
|
+
if ((raw?.codegen?.clientOutDir === false || trimPath(raw?.codegen?.clientOutDir)) && trimPath(raw?.codegen?.outDir)) {
|
|
405
405
|
warnImpl(
|
|
406
406
|
"[gencow] Both codegen output paths are set. Using `codegen.clientOutDir`; ignoring deprecated `codegen.outDir`.",
|
|
407
407
|
);
|
|
@@ -692,6 +692,7 @@ export function buildEnv(config, options = {}) {
|
|
|
692
692
|
NODE_PATH: nodePath,
|
|
693
693
|
PORT: String(processEnv.PORT || config.port),
|
|
694
694
|
GENCOW_FUNCTIONS: resolvePathImpl(cwd, config.rootDir),
|
|
695
|
+
GENCOW_APP_NAME: processEnv.GENCOW_APP_NAME || config.name || config.deploy?.app || "",
|
|
695
696
|
GENCOW_DB_URL: resolvePathImpl(cwd, config.db.url),
|
|
696
697
|
GENCOW_STORAGE: resolvePathImpl(cwd, config.storage),
|
|
697
698
|
GENCOW_MIGRATIONS: resolvePathImpl(cwd, config.rootDir, "migrations"),
|
|
@@ -94,6 +94,7 @@ export function formatStaleCliWarning({ currentVersion, latestVersion, command }
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export function maybeCheckCliVersion({
|
|
97
|
+
args = [],
|
|
97
98
|
cacheMaxAgeMs = DEFAULT_CACHE_MAX_AGE_MS,
|
|
98
99
|
command,
|
|
99
100
|
currentVersion,
|
|
@@ -112,7 +113,7 @@ export function maybeCheckCliVersion({
|
|
|
112
113
|
if (isEnabled(processEnv.GENCOW_SKIP_VERSION_CHECK)) {
|
|
113
114
|
return { status: "skipped" };
|
|
114
115
|
}
|
|
115
|
-
if (command === "help" || command === "version") {
|
|
116
|
+
if (command === "help" || command === "version" || args.some((arg) => arg === "--help" || arg === "-h")) {
|
|
116
117
|
return { status: "skipped" };
|
|
117
118
|
}
|
|
118
119
|
|
package/lib/codegen-command.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export { formatAuthSchemaConflictMessage } from "./auth-schema-provenance-guard.
|
|
|
16
16
|
|
|
17
17
|
export function resolveCodegenConfig(config = {}) {
|
|
18
18
|
const trim = (value) => (typeof value === "string" && value.trim().length > 0 ? value.trim() : null);
|
|
19
|
-
const outDir = trim(config.codegen?.clientOutDir) ?? trim(config.codegen?.outDir);
|
|
19
|
+
const outDir = config.codegen?.clientOutDir === false ? false : trim(config.codegen?.clientOutDir) ?? trim(config.codegen?.outDir);
|
|
20
20
|
const serverOutDir =
|
|
21
21
|
typeof config.codegen?.serverOutDir === "string" && config.codegen.serverOutDir.trim().length > 0
|
|
22
22
|
? config.codegen.serverOutDir.trim()
|
|
@@ -43,6 +43,7 @@ function getDeclaredCodegenClients(config = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function resolveCodegenPublishDir(cwd, config, options = {}) {
|
|
46
|
+
if (config?.codegen?.clientOutDir === false) return false;
|
|
46
47
|
const resolvePathImpl = options.resolvePathImpl ?? resolve;
|
|
47
48
|
const selectedClient = options.selectedTarget ?? options.selectedClient ?? null;
|
|
48
49
|
if (selectedClient) {
|
|
@@ -155,7 +156,7 @@ export function reportCodegenArtifacts({
|
|
|
155
156
|
successImpl,
|
|
156
157
|
}) {
|
|
157
158
|
const artifactsRel = relative(cwd, artifactsDir).replace(/\\/g, "/") || ".";
|
|
158
|
-
const publishRel = relative(cwd, publishDir).replace(/\\/g, "/") || ".";
|
|
159
|
+
const publishRel = publishDir === false ? null : relative(cwd, publishDir).replace(/\\/g, "/") || ".";
|
|
159
160
|
const serverRel = relative(cwd, serverPublishDir).replace(/\\/g, "/") || ".";
|
|
160
161
|
const DB_SCHEMA_GEN = "db-schema.gen.ts";
|
|
161
162
|
for (const artifact of artifacts) {
|
|
@@ -206,7 +207,8 @@ export async function runCodegenWithReporting({
|
|
|
206
207
|
rootDir: resolveBackendRootDir(config),
|
|
207
208
|
schema: config.schema,
|
|
208
209
|
outDir,
|
|
209
|
-
finalOutDir: publishDir,
|
|
210
|
+
finalOutDir: publishDir === false ? undefined : publishDir,
|
|
211
|
+
clientGeneration: publishDir !== false,
|
|
210
212
|
serverPublishDir,
|
|
211
213
|
cloudFeatures: config.cloudFeatures,
|
|
212
214
|
authSchema: codegenResolved.authSchema
|
|
@@ -254,6 +256,7 @@ export function createCodegenCommand(deps) {
|
|
|
254
256
|
deps.logImpl(
|
|
255
257
|
` Client: api.ts, db.d.ts, operations.d.ts → codegen.clientOutDir or clients[*].codegen.outDir.\n`,
|
|
256
258
|
);
|
|
259
|
+
deps.logImpl(` Set codegen.clientOutDir: false to disable client generation; --outdir overrides it for one run.\n`);
|
|
257
260
|
deps.logImpl(
|
|
258
261
|
` Server: schema-auth.gen.ts, db-schema.gen.ts → codegen.serverOutDir (default: <functionsDir>/generated).\n`,
|
|
259
262
|
);
|
|
@@ -301,13 +304,14 @@ export function createCodegenCommand(deps) {
|
|
|
301
304
|
explicitTarget = codegenArgs[i].slice("--target=".length) || null;
|
|
302
305
|
}
|
|
303
306
|
}
|
|
304
|
-
const
|
|
307
|
+
const clientDisabled = codegenResolved.clientOutDir === false && !outdir;
|
|
308
|
+
const declaredCodegenClients = codegenResolved.clientOutDir === false ? [] : getDeclaredCodegenClients(config);
|
|
305
309
|
const explicitSelection = explicitTarget ?? getCliInvocationSelection()?.target ?? null;
|
|
306
310
|
if (
|
|
307
311
|
Array.isArray(config?.clients) &&
|
|
308
312
|
config.clients.length > 0 &&
|
|
309
313
|
declaredCodegenClients.length === 0 &&
|
|
310
|
-
!outdir
|
|
314
|
+
!outdir && !clientDisabled
|
|
311
315
|
) {
|
|
312
316
|
deps.errorImpl(
|
|
313
317
|
"No declared codegen client found. Add clients[*].codegen.outDir or pass --outdir for a one-off output.",
|
|
@@ -315,7 +319,7 @@ export function createCodegenCommand(deps) {
|
|
|
315
319
|
deps.exitImpl(1);
|
|
316
320
|
return;
|
|
317
321
|
}
|
|
318
|
-
const targetSelection = selectCodegenClient(declaredCodegenClients, explicitSelection);
|
|
322
|
+
const targetSelection = codegenResolved.clientOutDir === false ? { selected: null, autoSelected: false } : selectCodegenClient(declaredCodegenClients, explicitSelection);
|
|
319
323
|
if (targetSelection.error) {
|
|
320
324
|
deps.errorImpl(targetSelection.error);
|
|
321
325
|
deps.exitImpl(1);
|
|
@@ -330,7 +334,7 @@ export function createCodegenCommand(deps) {
|
|
|
330
334
|
});
|
|
331
335
|
const serverPublishDir = resolveCodegenServerPublishDir(projectDir, config);
|
|
332
336
|
const artifactsRel = relative(projectDir, artifactsDir).replace(/\\/g, "/") || ".";
|
|
333
|
-
const publishRel = relative(projectDir, publishDir).replace(/\\/g, "/") || ".";
|
|
337
|
+
const publishRel = publishDir === false ? null : relative(projectDir, publishDir).replace(/\\/g, "/") || ".";
|
|
334
338
|
const serverRel = relative(projectDir, serverPublishDir).replace(/\\/g, "/") || ".";
|
|
335
339
|
|
|
336
340
|
deps.logImpl(`\n${deps.BOLD}${deps.CYAN}Gencow Codegen${deps.RESET}\n`);
|
|
@@ -341,7 +345,7 @@ export function createCodegenCommand(deps) {
|
|
|
341
345
|
deps.infoImpl(`Codegen client: ${selectedTarget.name}${selectionNote} → ${publishRel}/`);
|
|
342
346
|
}
|
|
343
347
|
deps.infoImpl(`Intermediate: ${artifactsRel}/manifest.json`);
|
|
344
|
-
deps.infoImpl(`Client types: ${publishRel}/ (api.ts, db.d.ts, operations.d.ts)`);
|
|
348
|
+
deps.infoImpl(publishDir === false ? "Client generation disabled (codegen.clientOutDir: false)" : `Client types: ${publishRel}/ (api.ts, db.d.ts, operations.d.ts)`);
|
|
345
349
|
deps.infoImpl(`Server schema: ${serverRel}/ (schema-auth.gen.ts, db-schema.gen.ts)`);
|
|
346
350
|
deps.logImpl("");
|
|
347
351
|
|
|
@@ -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
|
+
}
|
package/lib/deploy-command.mjs
CHANGED
|
@@ -27,6 +27,7 @@ export function renderDeployHelp() {
|
|
|
27
27
|
log(` gencow deploy`);
|
|
28
28
|
log(` gencow deploy status`);
|
|
29
29
|
log(` gencow deploy status --deployment <operation-id>`);
|
|
30
|
+
log(` gencow deploy status --release-attempt <attempt-id> [--prod]`);
|
|
30
31
|
log(` gencow deploy logs [-n 100] [--history]`);
|
|
31
32
|
log(` gencow deploy --static dist/`);
|
|
32
33
|
log(` gencow deploy --prod --region kr`);
|
|
@@ -45,12 +46,50 @@ export function parseDeployLogScope(deployArgs) {
|
|
|
45
46
|
return deployArgs.includes("--history") ? "history" : "current";
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function readReleaseAttemptStatusId(deployArgs) {
|
|
50
|
+
const indices = deployArgs.flatMap((arg, index) => (arg === "--release-attempt" ? [index] : []));
|
|
51
|
+
if (indices.length === 0) return null;
|
|
52
|
+
if (indices.length !== 1) return undefined;
|
|
53
|
+
const index = indices[0];
|
|
54
|
+
const value = deployArgs[index + 1];
|
|
55
|
+
if (
|
|
56
|
+
typeof value !== "string" ||
|
|
57
|
+
!/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u.test(value)
|
|
58
|
+
) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const targetArgs = [...deployArgs.slice(1, index), ...deployArgs.slice(index + 2)];
|
|
63
|
+
let appSelected = false;
|
|
64
|
+
let prodSelected = false;
|
|
65
|
+
for (let cursor = 0; cursor < targetArgs.length; cursor += 1) {
|
|
66
|
+
if (targetArgs[cursor] === "--prod" && !prodSelected) {
|
|
67
|
+
prodSelected = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if ((targetArgs[cursor] === "--app" || targetArgs[cursor] === "-a") && !appSelected) {
|
|
71
|
+
const appName = targetArgs[cursor + 1];
|
|
72
|
+
if (typeof appName !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(appName)) return undefined;
|
|
73
|
+
appSelected = true;
|
|
74
|
+
cursor += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
function readDeploymentStatusOperationId(deployArgs) {
|
|
49
83
|
const index = deployArgs.findIndex((arg) => arg === "--deployment");
|
|
50
84
|
if (index === -1) return null;
|
|
51
85
|
const value = deployArgs[index + 1];
|
|
86
|
+
// The public diagnostic contract emits `--prod` after the operation id for
|
|
87
|
+
// production-owned deployments. The operation endpoint is already scoped
|
|
88
|
+
// by the id, so accept that harmless target selector instead of rejecting
|
|
89
|
+
// the exact self-service command we just rendered.
|
|
90
|
+
const trailingArgs = deployArgs.slice(index + 2);
|
|
52
91
|
if (
|
|
53
|
-
|
|
92
|
+
trailingArgs.some((arg) => arg !== "--prod") ||
|
|
54
93
|
typeof value !== "string" ||
|
|
55
94
|
!/^[1-9][0-9]*$/u.test(value) ||
|
|
56
95
|
!Number.isSafeInteger(Number(value))
|
|
@@ -69,8 +108,11 @@ function logDeploymentOperationStatus(logImpl, infoImpl, body) {
|
|
|
69
108
|
infoImpl(`State: ${body.state ?? "unknown"}`);
|
|
70
109
|
if (body.code) infoImpl(`Code: ${body.code}`);
|
|
71
110
|
if (body.stage) infoImpl(`Stage: ${body.stage}`);
|
|
111
|
+
if (body.capability) infoImpl(`Capability: ${body.capability}`);
|
|
72
112
|
if (body.databaseState) infoImpl(`Database: ${body.databaseState}`);
|
|
73
113
|
if (body.servingState) infoImpl(`Serving: ${body.servingState}`);
|
|
114
|
+
if (body.retryClass) infoImpl(`Retry class: ${body.retryClass}`);
|
|
115
|
+
if (body.userAction) infoImpl(`Action: ${body.userAction}`);
|
|
74
116
|
if (body.retryPrecondition) infoImpl(`Before retry: ${body.retryPrecondition}`);
|
|
75
117
|
if (body.nextAction?.command) infoImpl(`Next: ${body.nextAction.command}`);
|
|
76
118
|
logImpl("");
|
|
@@ -94,6 +136,61 @@ export async function handleDeployReadonlySubcommand(params) {
|
|
|
94
136
|
}
|
|
95
137
|
|
|
96
138
|
const subcmd = deployArgs[0];
|
|
139
|
+
const releaseAttemptId = subcmd === "status" ? readReleaseAttemptStatusId(deployArgs) : null;
|
|
140
|
+
if (releaseAttemptId === undefined) {
|
|
141
|
+
errorImpl("APP_RELEASE_STATUS_ID_INVALID: use a valid --release-attempt UUID.");
|
|
142
|
+
exitImpl(1);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (releaseAttemptId !== null) {
|
|
146
|
+
const targetArgs = deployArgs
|
|
147
|
+
.slice(1)
|
|
148
|
+
.filter((arg, index, list) => arg !== "--release-attempt" && list[index - 1] !== "--release-attempt");
|
|
149
|
+
const target = resolveCloudAppTarget(targetArgs, {
|
|
150
|
+
cwd,
|
|
151
|
+
missingAppMessage: "App not found. Run from the project root with gencow.json.",
|
|
152
|
+
missingProdMessage: "No prod app yet. Run gencow deploy --prod first.",
|
|
153
|
+
});
|
|
154
|
+
if (target.error) {
|
|
155
|
+
errorImpl(target.error);
|
|
156
|
+
exitImpl(1);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
const { appId, envTarget } = target;
|
|
160
|
+
const res = await platformFetchImpl(
|
|
161
|
+
creds,
|
|
162
|
+
`/platform/apps/${encodeURIComponent(appId)}/release-attempts/${releaseAttemptId}?environment=${envTarget}`,
|
|
163
|
+
{ method: "GET" },
|
|
164
|
+
);
|
|
165
|
+
const decoded = await readJsonValueResponse(res);
|
|
166
|
+
const body = decoded.parsed && decoded.value && typeof decoded.value === "object" ? decoded.value : null;
|
|
167
|
+
if (!res.ok || !body || body.attemptId !== releaseAttemptId) {
|
|
168
|
+
for (const line of renderDeployFailureDiagnosticLines(body, res.statusText, { expectedAppName: appId }))
|
|
169
|
+
errorImpl(line);
|
|
170
|
+
exitImpl(1);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
logImpl("");
|
|
174
|
+
logImpl(" Release Attempt");
|
|
175
|
+
infoImpl(`Attempt: ${body.attemptId}`);
|
|
176
|
+
infoImpl(`Target: ${body.environment}`);
|
|
177
|
+
infoImpl(`State: ${body.state ?? "unknown"}`);
|
|
178
|
+
infoImpl(`Terminal: ${body.terminal === true ? "yes" : "no"}`);
|
|
179
|
+
if (Array.isArray(body.expectedComponents)) infoImpl(`Components: ${body.expectedComponents.join(", ")}`);
|
|
180
|
+
if (body.backendDeploymentId) infoImpl(`Backend deployment: ${body.backendDeploymentId}`);
|
|
181
|
+
if (body.staticDeploymentId) infoImpl(`Static deployment: ${body.staticDeploymentId}`);
|
|
182
|
+
if (body.failureCode)
|
|
183
|
+
infoImpl(`Failure: ${body.failureCode} (${body.failureStage ?? "unknown stage"})`);
|
|
184
|
+
infoImpl(
|
|
185
|
+
"Retry: not established by release attempt status; terminal state alone is not a retry gate.",
|
|
186
|
+
);
|
|
187
|
+
if (body.backendDeploymentId)
|
|
188
|
+
infoImpl(`Next status: gencow deploy status --deployment ${body.backendDeploymentId}`);
|
|
189
|
+
if (body.staticDeploymentId)
|
|
190
|
+
infoImpl(`Next status: gencow deploy status --deployment ${body.staticDeploymentId}`);
|
|
191
|
+
logImpl("");
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
97
194
|
const deploymentId = subcmd === "status" ? readDeploymentStatusOperationId(deployArgs) : null;
|
|
98
195
|
if (deploymentId === undefined) {
|
|
99
196
|
errorImpl("DEPLOYMENT_OPERATION_ID_INVALID: use a positive numeric --deployment value.");
|
|
@@ -27,6 +27,8 @@ const STRUCTURED_CONTRACT_FIELDS = [
|
|
|
27
27
|
"retryClass",
|
|
28
28
|
"userAction",
|
|
29
29
|
"correlationId",
|
|
30
|
+
"requestId",
|
|
31
|
+
"environment",
|
|
30
32
|
"operationId",
|
|
31
33
|
"appName",
|
|
32
34
|
];
|
|
@@ -48,7 +50,7 @@ const SELF_SERVICE_RETRY_POLICIES = new Set([
|
|
|
48
50
|
"AFTER_STATUS",
|
|
49
51
|
]);
|
|
50
52
|
const SAFE_SELF_SERVICE_COMMAND =
|
|
51
|
-
/^gencow (?:deploy status(?: --prod)?(?: --deployment [1-9][0-9]*)
|
|
53
|
+
/^gencow (?:(?:deploy status(?: --prod)?(?: --deployment [1-9][0-9]*| --release-attempt [a-f0-9-]{36})?|deploy status --app [a-z][a-z0-9]*(?:-[a-z0-9]+)*(?: --prod)? --release-attempt [a-f0-9-]{36}|deploy status --app [a-z][a-z0-9]*(?:-[a-z0-9]+)*(?: --prod)?)|(?:deploy --app [a-z][a-z0-9]*(?:-[a-z0-9]+)*(?: --prod)? --resume [A-Za-z0-9_-]{16,128})|(?:db:check(?: --prod)? --inspect-state)|whoami)$/u;
|
|
52
54
|
|
|
53
55
|
function safeText(value, maxLength) {
|
|
54
56
|
if (typeof value !== "string") return null;
|
|
@@ -66,6 +68,9 @@ function readDeployFailureContract(value, expectedAppName) {
|
|
|
66
68
|
const retryClass = safeText(value.retryClass, 32);
|
|
67
69
|
const userAction = safeText(value.userAction, 32);
|
|
68
70
|
const correlationId = value.correlationId === undefined ? null : safeText(value.correlationId, 128);
|
|
71
|
+
const requestId = value.requestId === undefined ? null : safeText(value.requestId, 128);
|
|
72
|
+
const environment = value.environment === "dev" || value.environment === "prod" ? value.environment : null;
|
|
73
|
+
const appName = value.appName === undefined ? null : safeText(value.appName, 128);
|
|
69
74
|
const operationId =
|
|
70
75
|
Number.isSafeInteger(value.operationId) && value.operationId > 0
|
|
71
76
|
? String(value.operationId)
|
|
@@ -88,9 +93,18 @@ function readDeployFailureContract(value, expectedAppName) {
|
|
|
88
93
|
!RETRY_CLASSES.has(retryClass) ||
|
|
89
94
|
!userAction ||
|
|
90
95
|
!USER_ACTIONS.has(userAction) ||
|
|
96
|
+
(code === "DEPLOYMENT_EXECUTOR_ADMISSION_RECONCILIATION_UNAVAILABLE" && userAction !== "VIEW_STATUS") ||
|
|
91
97
|
(value.correlationId !== undefined && !correlationId) ||
|
|
92
98
|
(correlationId && !/^[A-Za-z0-9_-]{8,128}$/u.test(correlationId)) ||
|
|
99
|
+
(requestId !== null && !/^[A-Za-z0-9_-]{16,128}$/u.test(requestId)) ||
|
|
100
|
+
(requestId !== null &&
|
|
101
|
+
value.correlationId !== requestId &&
|
|
102
|
+
value.code === "DEPLOYMENT_EXECUTOR_ADMISSION_RECONCILIATION_UNAVAILABLE") ||
|
|
103
|
+
(value.environment !== undefined && !environment) ||
|
|
104
|
+
(value.appName !== undefined && (!appName || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(appName))) ||
|
|
93
105
|
(expectedAppName && value.appName !== undefined && value.appName !== expectedAppName) ||
|
|
106
|
+
(code === "DEPLOYMENT_EXECUTOR_ADMISSION_OUTCOME_UNKNOWN" &&
|
|
107
|
+
(!requestId || !environment || !appName || !expectedAppName || appName !== expectedAppName)) ||
|
|
94
108
|
(value.blockingOperationId !== undefined && !blockingOperationId) ||
|
|
95
109
|
(value.recoveredOperationId !== undefined && !recoveredOperationId) ||
|
|
96
110
|
(recoveredOperationId &&
|
|
@@ -113,6 +127,9 @@ function readDeployFailureContract(value, expectedAppName) {
|
|
|
113
127
|
userAction:
|
|
114
128
|
userAction === "CONTACT_SUPPORT" || userAction === "OPERATOR_ACTION" ? "VIEW_STATUS" : userAction,
|
|
115
129
|
correlationId,
|
|
130
|
+
requestId,
|
|
131
|
+
environment,
|
|
132
|
+
appName,
|
|
116
133
|
operationId,
|
|
117
134
|
blockingOperationId,
|
|
118
135
|
recoveredOperationId,
|
|
@@ -120,7 +137,7 @@ function readDeployFailureContract(value, expectedAppName) {
|
|
|
120
137
|
};
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
function readSelfServiceDiagnostic(responseBody, contract) {
|
|
140
|
+
function readSelfServiceDiagnostic(responseBody, contract, expectedAppName) {
|
|
124
141
|
const value = responseBody?.diagnostic?.selfService;
|
|
125
142
|
const stateImpact = value?.stateImpact;
|
|
126
143
|
const retry = value?.retry;
|
|
@@ -131,6 +148,33 @@ function readSelfServiceDiagnostic(responseBody, contract) {
|
|
|
131
148
|
const statusCommandOperationId = /^gencow deploy status(?: --prod)? --deployment ([1-9][0-9]*)$/u.exec(
|
|
132
149
|
command ?? "",
|
|
133
150
|
)?.[1];
|
|
151
|
+
const statusCommandReleaseAttemptMatch =
|
|
152
|
+
/^gencow deploy status(?: --prod)? --release-attempt ([a-f0-9-]{36})$/u.exec(command ?? "") ??
|
|
153
|
+
/^gencow deploy status --app ([a-z][a-z0-9]*(?:-[a-z0-9]+)*)( --prod)? --release-attempt ([a-f0-9-]{36})$/u.exec(
|
|
154
|
+
command ?? "",
|
|
155
|
+
);
|
|
156
|
+
const statusCommandReleaseAttemptId =
|
|
157
|
+
statusCommandReleaseAttemptMatch?.[3] ?? statusCommandReleaseAttemptMatch?.[1];
|
|
158
|
+
const statusCommandReleaseAttemptAppName = statusCommandReleaseAttemptMatch?.[3]
|
|
159
|
+
? statusCommandReleaseAttemptMatch[1]
|
|
160
|
+
: null;
|
|
161
|
+
const statusCommandReleaseAttemptEnvironment = statusCommandReleaseAttemptMatch?.[3]
|
|
162
|
+
? statusCommandReleaseAttemptMatch[2]
|
|
163
|
+
? "prod"
|
|
164
|
+
: "dev"
|
|
165
|
+
: null;
|
|
166
|
+
const resumeCommandMatch =
|
|
167
|
+
/^gencow deploy --app ([a-z][a-z0-9]*(?:-[a-z0-9]+)*)( --prod)? --resume ([A-Za-z0-9_-]{16,128})$/u.exec(
|
|
168
|
+
command ?? "",
|
|
169
|
+
);
|
|
170
|
+
const resumeCommandAppName = resumeCommandMatch?.[1] ?? null;
|
|
171
|
+
const resumeCommandEnvironment = resumeCommandMatch?.[2] ? "prod" : "dev";
|
|
172
|
+
const resumeCommandRequestId = resumeCommandMatch?.[3] ?? null;
|
|
173
|
+
const scopedStatusCommandMatch =
|
|
174
|
+
/^gencow deploy status --app ([a-z][a-z0-9]*(?:-[a-z0-9]+)*)( --prod)?$/u.exec(command ?? "");
|
|
175
|
+
const scopedStatusCommandAppName = scopedStatusCommandMatch?.[1] ?? null;
|
|
176
|
+
const scopedStatusCommandEnvironment = scopedStatusCommandMatch?.[2] ? "prod" : "dev";
|
|
177
|
+
const requestId = value?.requestId === undefined ? null : safeText(value.requestId, 128);
|
|
134
178
|
const prerequisites = Array.isArray(action?.prerequisites)
|
|
135
179
|
? action.prerequisites.map((item) => safeText(item, 240))
|
|
136
180
|
: [];
|
|
@@ -160,6 +204,53 @@ function readSelfServiceDiagnostic(responseBody, contract) {
|
|
|
160
204
|
statusCommandOperationId !== contract.blockingOperationId &&
|
|
161
205
|
statusCommandOperationId !== contract.recoveredOperationId) ||
|
|
162
206
|
!Number.isSafeInteger(Number(statusCommandOperationId)))) ||
|
|
207
|
+
(statusCommandReleaseAttemptId &&
|
|
208
|
+
statusCommandReleaseAttemptId !== contract.operationId &&
|
|
209
|
+
`release-${statusCommandReleaseAttemptId}` !== contract.operationId) ||
|
|
210
|
+
(statusCommandReleaseAttemptAppName &&
|
|
211
|
+
(statusCommandReleaseAttemptAppName !== contract.appName ||
|
|
212
|
+
(expectedAppName && statusCommandReleaseAttemptAppName !== expectedAppName) ||
|
|
213
|
+
(contract.environment && statusCommandReleaseAttemptEnvironment !== contract.environment))) ||
|
|
214
|
+
(requestId !== null && !/^[A-Za-z0-9_-]{16,128}$/u.test(requestId)) ||
|
|
215
|
+
(contract.requestId !== null && requestId !== contract.requestId) ||
|
|
216
|
+
(resumeCommandRequestId !== null && contract.code !== "DEPLOYMENT_EXECUTOR_ADMISSION_OUTCOME_UNKNOWN") ||
|
|
217
|
+
(contract.code === "DEPLOYMENT_EXECUTOR_ADMISSION_RECONCILIATION_UNAVAILABLE" &&
|
|
218
|
+
(!requestId ||
|
|
219
|
+
contract.stage !== "admission" ||
|
|
220
|
+
contract.userAction !== "VIEW_STATUS" ||
|
|
221
|
+
contract.requestId !== requestId ||
|
|
222
|
+
contract.correlationId !== requestId ||
|
|
223
|
+
!contract.environment ||
|
|
224
|
+
!contract.appName ||
|
|
225
|
+
!scopedStatusCommandAppName ||
|
|
226
|
+
scopedStatusCommandAppName !== contract.appName ||
|
|
227
|
+
scopedStatusCommandEnvironment !== contract.environment ||
|
|
228
|
+
!expectedAppName ||
|
|
229
|
+
contract.appName !== expectedAppName ||
|
|
230
|
+
stateImpact.mutation !== "UNKNOWN" ||
|
|
231
|
+
stateImpact.incumbentServing !== "UNKNOWN" ||
|
|
232
|
+
stateImpact.operationState !== "BLOCKED" ||
|
|
233
|
+
retry.allowedNow !== false ||
|
|
234
|
+
retry.policy !== "AFTER_STATUS")) ||
|
|
235
|
+
(contract.code === "DEPLOYMENT_EXECUTOR_ADMISSION_OUTCOME_UNKNOWN" &&
|
|
236
|
+
(!requestId ||
|
|
237
|
+
contract.stage !== "admission" ||
|
|
238
|
+
contract.userAction !== "RETRY" ||
|
|
239
|
+
contract.requestId !== requestId ||
|
|
240
|
+
!contract.environment ||
|
|
241
|
+
!contract.appName ||
|
|
242
|
+
!expectedAppName ||
|
|
243
|
+
contract.appName !== expectedAppName ||
|
|
244
|
+
resumeCommandAppName !== contract.appName ||
|
|
245
|
+
resumeCommandEnvironment !== contract.environment ||
|
|
246
|
+
!resumeCommandRequestId ||
|
|
247
|
+
resumeCommandRequestId !== requestId ||
|
|
248
|
+
contract.correlationId !== requestId ||
|
|
249
|
+
stateImpact.mutation !== "NONE" ||
|
|
250
|
+
stateImpact.incumbentServing !== "PRESERVED" ||
|
|
251
|
+
stateImpact.operationState !== "BLOCKED" ||
|
|
252
|
+
retry.allowedNow !== true ||
|
|
253
|
+
retry.policy !== "IMMEDIATE")) ||
|
|
163
254
|
prerequisites.some((item) => item === null) ||
|
|
164
255
|
prerequisites.length > 8 ||
|
|
165
256
|
forbiddenActions.some((item) => item === null) ||
|
|
@@ -177,6 +268,7 @@ function readSelfServiceDiagnostic(responseBody, contract) {
|
|
|
177
268
|
action: {
|
|
178
269
|
kind: action.kind,
|
|
179
270
|
...(command === null ? {} : { command }),
|
|
271
|
+
...(requestId === null ? {} : { requestId }),
|
|
180
272
|
prerequisites,
|
|
181
273
|
},
|
|
182
274
|
forbiddenActions,
|
|
@@ -191,6 +283,7 @@ function appendSelfServiceState(lines, selfService) {
|
|
|
191
283
|
`Operation state: ${selfService.stateImpact.operationState}`,
|
|
192
284
|
`Retry policy: ${selfService.retry.policy} — ${selfService.retry.condition}`,
|
|
193
285
|
);
|
|
286
|
+
if (selfService.action.requestId) lines.push(`Request ID: ${selfService.action.requestId}`);
|
|
194
287
|
for (const prerequisite of selfService.action.prerequisites) {
|
|
195
288
|
lines.push(`Prerequisite: ${prerequisite}`);
|
|
196
289
|
}
|
|
@@ -252,7 +345,7 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText, opt
|
|
|
252
345
|
contract.operationId &&
|
|
253
346
|
/^[1-9][0-9]*$/u.test(contract.operationId)
|
|
254
347
|
) {
|
|
255
|
-
const selfService = readSelfServiceDiagnostic(responseBody, contract);
|
|
348
|
+
const selfService = readSelfServiceDiagnostic(responseBody, contract, options.expectedAppName);
|
|
256
349
|
if (
|
|
257
350
|
responseBody.retryReady === true &&
|
|
258
351
|
responseBody.retryReadinessReason === "READY" &&
|
|
@@ -285,9 +378,10 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText, opt
|
|
|
285
378
|
? PLATFORM_STATUS_MESSAGE
|
|
286
379
|
: (safeText(responseBody.error, 240) ?? fallbackMessage);
|
|
287
380
|
const lines = [`Deploy failed: ${publicMessage}`, `Code: ${contract.code}`, `Stage: ${contract.stage}`];
|
|
288
|
-
const selfService = readSelfServiceDiagnostic(responseBody, contract);
|
|
381
|
+
const selfService = readSelfServiceDiagnostic(responseBody, contract, options.expectedAppName);
|
|
289
382
|
appendSelfServiceState(lines, selfService);
|
|
290
383
|
if (contract.operationId) lines.push(`Operation ID: ${contract.operationId}`);
|
|
384
|
+
if (contract.requestId) lines.push(`Request ID: ${contract.requestId}`);
|
|
291
385
|
if (contract.blockingOperationId) lines.push(`Blocking operation ID: ${contract.blockingOperationId}`);
|
|
292
386
|
if (contract.recoveredOperationId) lines.push(`Recovered operation ID: ${contract.recoveredOperationId}`);
|
|
293
387
|
if (
|
|
@@ -310,6 +404,34 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText, opt
|
|
|
310
404
|
Number.isSafeInteger(Number(contract.operationId))
|
|
311
405
|
? `gencow deploy status --deployment ${contract.operationId}`
|
|
312
406
|
: "gencow deploy status";
|
|
407
|
+
if (contract.code === "PLATFORM_DEPLOYMENT_ADMISSION_PENDING") {
|
|
408
|
+
lines.push(
|
|
409
|
+
`Database state: ${safeText(responseBody.databaseState, 32) ?? "UNKNOWN"}`,
|
|
410
|
+
`Incumbent serving: ${safeText(responseBody.incumbentServing, 32) ?? "UNKNOWN"}`,
|
|
411
|
+
`Operation state: ${safeText(responseBody.operationState, 32) ?? "UNKNOWN"}`,
|
|
412
|
+
);
|
|
413
|
+
const requestId = safeText(responseBody.requestId, 128);
|
|
414
|
+
if (requestId && /^[A-Za-z0-9_-]{8,128}$/u.test(requestId)) lines.push(`Request ID: ${requestId}`);
|
|
415
|
+
lines.push(
|
|
416
|
+
"Server outcome: unknown; inspect the existing request before submitting another deployment.",
|
|
417
|
+
);
|
|
418
|
+
const resumeCommand = safeText(responseBody.resumeCommand, 240);
|
|
419
|
+
const resumeMatch = resumeCommand?.match(
|
|
420
|
+
/^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,
|
|
421
|
+
);
|
|
422
|
+
const resumeToken = resumeCommand?.match(/--resume ([A-Za-z0-9_-]{8,128})$/u)?.[1];
|
|
423
|
+
const resumeAppName = resumeMatch?.[1];
|
|
424
|
+
const expectedResumeApp = options.expectedAppName ?? responseBody.appName;
|
|
425
|
+
if (
|
|
426
|
+
resumeMatch &&
|
|
427
|
+
requestId &&
|
|
428
|
+
resumeToken === requestId &&
|
|
429
|
+
(!resumeAppName || !expectedResumeApp || resumeAppName === expectedResumeApp)
|
|
430
|
+
) {
|
|
431
|
+
lines.push(`Next command: ${resumeCommand}`);
|
|
432
|
+
return lines;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
313
435
|
lines.push(`Next command: ${selfService?.action.command ?? exactStatusCommand}`);
|
|
314
436
|
if (contract.blockingOperationId && responseBody.retryReadinessReason === "ACTIVE_OWNER" && selfService) {
|
|
315
437
|
lines.push(`Then check retry gate: ${exactStatusCommand}`);
|
|
@@ -321,7 +443,7 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText, opt
|
|
|
321
443
|
: (safeText(responseBody.error, 240) ?? fallbackMessage);
|
|
322
444
|
const lines = [`Deploy failed: ${publicMessage}`];
|
|
323
445
|
lines.push(`Code: ${contract.code}`, `Stage: ${contract.stage}`);
|
|
324
|
-
const selfService = readSelfServiceDiagnostic(responseBody, contract);
|
|
446
|
+
const selfService = readSelfServiceDiagnostic(responseBody, contract, options.expectedAppName);
|
|
325
447
|
appendSelfServiceState(lines, selfService);
|
|
326
448
|
if (isCliUpgradeContract(contract) && !selfService) {
|
|
327
449
|
lines.push(
|