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.
@@ -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. The name remains unavailable until then.\n Type the app name to confirm: `,
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. The name remains unavailable until then.`);
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 { resolveCreatedAppResponse } from "./app-create-response.mjs";
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
- async function createCloudAppIfNeeded({ creds, appId, displayName, envTarget = "dev", gencowJsonPath }) {
561
- if (appId) return appId;
562
-
563
- infoImpl("Creating app (auto-generating ID)...");
564
- const createRes = await rpcMutationImpl(creds, "apps.create", { name: displayName });
565
- if (!createRes.ok) {
566
- const createErr = await readJsonObjectResponse(createRes);
567
- errorImpl(`App creation failed: ${createErr?.error || createRes.statusText}`);
568
- exitImpl(1);
569
- return null;
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: () => deployOnce(targetAppId),
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
  }
@@ -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
- if (!appId || prodAppId) {
213
- return prodAppId ?? appId;
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(creds, `/platform/apps/${appId}/create-prod`, {
229
- method: "POST",
230
- headers: { "Content-Type": "application/json" },
231
- body: JSON.stringify(region ? { region } : {}),
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,