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 CHANGED
@@ -585,6 +585,7 @@ const cmd =
585
585
  ? "version"
586
586
  : rawCmd;
587
587
  maybeCheckCliVersion({
588
+ args,
588
589
  command: cmd,
589
590
  currentVersion: CLI_VERSION,
590
591
  errorImpl: error,
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
@@ -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
 
@@ -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 declaredCodegenClients = getDeclaredCodegenClients(config);
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
+ }
@@ -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
- index + 2 !== deployArgs.length ||
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]*)?|db:check(?: --prod)? --inspect-state)$/u;
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(