gencow 0.1.188 → 0.1.189

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.
@@ -595,7 +595,7 @@ export function findServerRoot(options = {}) {
595
595
  const siblingServer = resolvePathImpl(dirnameValue, "../../server");
596
596
  if (existsSyncImpl(siblingServer)) return siblingServer;
597
597
 
598
- errorImpl("Server runtime not found. Run 'bun install'.");
598
+ errorImpl("Server runtime not found. Run 'pnpm install'.");
599
599
  exitImpl(1);
600
600
  return null;
601
601
  }
@@ -180,9 +180,22 @@ export function emitCloudMigrationSuccess({
180
180
  ? "Migration state inspection completed; the plan is ready."
181
181
  : "Migration state inspection completed; the plan remains blocked.",
182
182
  );
183
- deps.logImpl(
184
- ` ${DIM}Code: ${checkData.inspection.code} · Next: ${checkData.inspection.nextAction} · Database unchanged${RESET}\n`,
185
- );
183
+ deps.logImpl(` ${DIM}Database unchanged${RESET}\n`);
184
+ const plan = checkData.inspection.plan;
185
+ if (
186
+ ready &&
187
+ plan?.safeToRewritePending === true &&
188
+ Array.isArray(plan.pendingMigrationNames) &&
189
+ plan.pendingMigrationNames.length > 0
190
+ ) {
191
+ deps.logImpl(` Applied migrations: ${plan.appliedVerifiedCount} (keep unchanged)`);
192
+ deps.logImpl(" Pending migrations that may be regenerated:");
193
+ plan.pendingMigrationNames.forEach((name) => deps.logImpl(` - ${name}`));
194
+ deps.logImpl(" Next: back up or commit the project, then remove only the listed pending folders.");
195
+ deps.logImpl(" gencow db:generate");
196
+ deps.logImpl(" pnpm exec drizzle-kit check");
197
+ deps.logImpl(` gencow db:check${environment === "prod" ? " --prod" : ""}`);
198
+ }
186
199
  return;
187
200
  }
188
201
  deps.successImpl("Migration plan is safe to proceed.");
@@ -110,6 +110,21 @@ function extractReadOnlyMigrationInspection(data) {
110
110
  return null;
111
111
  }
112
112
  const plan = inspection.plan;
113
+ const pendingMigrationNames = plan?.pendingMigrationNames;
114
+ const hasSafePendingTail =
115
+ plan?.safeToRewritePending === true &&
116
+ Array.isArray(pendingMigrationNames) &&
117
+ pendingMigrationNames.length === plan?.pendingCount &&
118
+ pendingMigrationNames.length > 0 &&
119
+ new Set(pendingMigrationNames).size === pendingMigrationNames.length &&
120
+ pendingMigrationNames.every(
121
+ (name) =>
122
+ typeof name === "string" &&
123
+ name.length <= 200 &&
124
+ name !== "." &&
125
+ name !== ".." &&
126
+ !/[\\/\u0000-\u001f\u007f]/u.test(name),
127
+ );
113
128
  if (
114
129
  inspection.outcome === "READY" &&
115
130
  !(
@@ -135,6 +150,9 @@ function extractReadOnlyMigrationInspection(data) {
135
150
  planId: plan.planId,
136
151
  appliedVerifiedCount: plan.appliedVerifiedCount,
137
152
  pendingCount: plan.pendingCount,
153
+ ...(hasSafePendingTail
154
+ ? { pendingMigrationNames: [...pendingMigrationNames], safeToRewritePending: true }
155
+ : {}),
138
156
  ...(plan.databaseBootstrapRequired === true ? { databaseBootstrapRequired: true } : {}),
139
157
  ...(plan.migrationStorePreparationRequired === true
140
158
  ? { migrationStorePreparationRequired: true }
@@ -2,7 +2,7 @@
2
2
  * Deploy Auditor — Pre-deploy dependency analysis (informational).
3
3
  *
4
4
  * Detects user third-party dependencies that need auto-install on the
5
- * Gencow cloud runtime. User deps are installed via `bun install` on
5
+ * Gencow cloud runtime. User deps are installed via frozen `pnpm install` on
6
6
  * the server during provisioning.
7
7
  *
8
8
  * How it works:
@@ -78,7 +78,7 @@ export function formatDependencyVersionAuditError(result) {
78
78
 
79
79
  lines.push("");
80
80
  lines.push(" Fix:");
81
- lines.push(` bun add ${installTargets.join(" ")}`);
81
+ lines.push(` pnpm add ${installTargets.join(" ")}`);
82
82
  lines.push(" bunx gencow@latest deploy");
83
83
  lines.push("");
84
84
 
@@ -15,7 +15,8 @@ const DEPLOY_FAILURE_STAGES = new Set([
15
15
  const RETRY_CLASSES = new Set(["terminal-client", "retryable-infra", "platform-action"]);
16
16
  const USER_ACTIONS = new Set([...MIGRATION_NEXT_ACTION_KINDS, "OPERATOR_ACTION"]);
17
17
  const STRUCTURED_CONTRACT_FIELDS = ["code", "stage", "retryClass", "userAction", "correlationId"];
18
- const PLATFORM_ACTION_REQUIRED_MESSAGE = "Platform action is required before retrying";
18
+ const PLATFORM_RECOVERY_MESSAGE =
19
+ "Gencow is recovering this deployment. Your app files do not need changes.";
19
20
 
20
21
  function safeText(value, maxLength) {
21
22
  if (typeof value !== "string") return null;
@@ -49,6 +50,7 @@ function readDeployFailureContract(value) {
49
50
  return {
50
51
  code,
51
52
  stage,
53
+ retryClass,
52
54
  userAction: userAction === "CONTACT_SUPPORT" ? "OPERATOR_ACTION" : userAction,
53
55
  correlationId,
54
56
  legacySupportAction: userAction === "CONTACT_SUPPORT",
@@ -57,7 +59,7 @@ function readDeployFailureContract(value) {
57
59
 
58
60
  function normalizeLegacySupportMessage(message) {
59
61
  return typeof message === "string" && /contact support/iu.test(message)
60
- ? PLATFORM_ACTION_REQUIRED_MESSAGE
62
+ ? PLATFORM_RECOVERY_MESSAGE
61
63
  : message;
62
64
  }
63
65
 
@@ -83,8 +85,15 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
83
85
  if (isLegacyCorrelationEnvelope) lines.push(correlationLine);
84
86
  return lines;
85
87
  }
88
+ const platformOwned = contract.retryClass === "platform-action" && contract.userAction === "OPERATOR_ACTION";
89
+ if (platformOwned) {
90
+ const lines = [`Deploy failed: ${PLATFORM_RECOVERY_MESSAGE}`];
91
+ const correlationLine = formatDeployCorrelationId(contract.correlationId);
92
+ if (correlationLine) lines.push(correlationLine);
93
+ return lines;
94
+ }
86
95
  const publicMessage = contract.legacySupportAction
87
- ? PLATFORM_ACTION_REQUIRED_MESSAGE
96
+ ? PLATFORM_RECOVERY_MESSAGE
88
97
  : (safeText(responseBody.error, 240) ?? fallbackMessage);
89
98
  const lines = [`Deploy failed: ${publicMessage}`];
90
99
  lines.push(`Code: ${contract.code}`, `Stage: ${contract.stage}`);
@@ -1,42 +1,78 @@
1
1
  export const DEPLOY_LOCKFILE_CANDIDATES = Object.freeze([
2
- { file: "bun.lock", packageManager: "bun" },
3
- { file: "bun.lockb", packageManager: "bun" },
4
2
  { file: "pnpm-lock.yaml", packageManager: "pnpm" },
5
- { file: "package-lock.json", packageManager: "npm" },
6
3
  ]);
4
+ export const UNSUPPORTED_DEPLOY_LOCKFILES = Object.freeze([
5
+ "bun.lock",
6
+ "bun.lockb",
7
+ "package-lock.json",
8
+ "npm-shrinkwrap.json",
9
+ "yarn.lock",
10
+ ]);
11
+ export const APPROVED_DEPLOY_PACKAGE_MANAGER = "pnpm@10.33.0";
7
12
 
8
13
  function declaredPackageManager(value) {
9
- if (typeof value !== "string") return null;
10
- return /^(bun|pnpm|npm)(?:@|$)/u.exec(value.trim())?.[1] ?? null;
14
+ if (typeof value !== "string") return "";
15
+ return value.trim();
11
16
  }
12
17
 
13
18
  export function selectDeployLockfile({ availableFiles, packageManager }) {
14
19
  const available = new Set(availableFiles);
15
20
  const detected = DEPLOY_LOCKFILE_CANDIDATES.filter(({ file }) => available.has(file));
21
+ const unsupportedFiles = UNSUPPORTED_DEPLOY_LOCKFILES.filter((file) => available.has(file));
16
22
  const declared = declaredPackageManager(packageManager);
17
- const declaredMatch = declared ? detected.find((candidate) => candidate.packageManager === declared) : null;
23
+ const declaredMatch = declared === APPROVED_DEPLOY_PACKAGE_MANAGER ? detected[0] ?? null : null;
18
24
  const selected = declaredMatch ?? detected[0] ?? null;
19
25
 
20
26
  return {
21
27
  selectedFile: selected?.file ?? null,
22
28
  selectedPackageManager: selected?.packageManager ?? "none",
23
- ignoredFiles: detected.filter(({ file }) => file !== selected?.file).map(({ file }) => file),
29
+ ignoredFiles: unsupportedFiles,
24
30
  selectionSource: declaredMatch
25
31
  ? "package.json#packageManager"
26
32
  : selected
27
33
  ? "deterministic priority"
28
34
  : "none",
35
+ declaredPackageManager: declared,
36
+ packageManagerCompliant: declared === APPROVED_DEPLOY_PACKAGE_MANAGER,
29
37
  };
30
38
  }
31
39
 
32
40
  export function detectDeployLockfile({ cwd, packageManager, existsSyncImpl, resolvePathImpl }) {
33
- const availableFiles = DEPLOY_LOCKFILE_CANDIDATES.filter(({ file }) =>
41
+ const availableFiles = [...DEPLOY_LOCKFILE_CANDIDATES.map(({ file }) => file), ...UNSUPPORTED_DEPLOY_LOCKFILES].filter((file) =>
34
42
  existsSyncImpl(resolvePathImpl(cwd, file)),
35
- ).map(({ file }) => file);
43
+ );
36
44
  return selectDeployLockfile({ availableFiles, packageManager });
37
45
  }
38
46
 
47
+ export function assertCanonicalDeployLockfile(selection) {
48
+ if (!selection.packageManagerCompliant) {
49
+ const found = selection.declaredPackageManager || "missing";
50
+ throw new Error(
51
+ `package.json#packageManager must be ${APPROVED_DEPLOY_PACKAGE_MANAGER} for Gencow deploy (found: ${found}). ` +
52
+ `Run: pnpm import; remove ${UNSUPPORTED_DEPLOY_LOCKFILES.join(", ")}; then pnpm install --lockfile-only.`,
53
+ );
54
+ }
55
+ if (selection.selectedFile !== "pnpm-lock.yaml") {
56
+ const suffix = selection.ignoredFiles.length ? ` Found unsupported: ${selection.ignoredFiles.join(", ")}.` : "";
57
+ throw new Error(`pnpm-lock.yaml is required for Gencow deploy.${suffix}`);
58
+ }
59
+ return selection;
60
+ }
61
+
39
62
  export function formatDeployLockfileSelectionWarning(selection) {
40
- if (!selection.selectedFile || selection.ignoredFiles.length === 0) return null;
41
- return `Multiple lockfiles found. Using ${selection.selectedFile} ${selection.selectionSource === "package.json#packageManager" ? "from" : "by"} ${selection.selectionSource}; ignoring ${selection.ignoredFiles.join(", ")}.`;
63
+ if (
64
+ !selection.packageManagerCompliant ||
65
+ selection.selectedFile !== "pnpm-lock.yaml" ||
66
+ selection.ignoredFiles.length === 0
67
+ ) {
68
+ return null;
69
+ }
70
+ const files = selection.ignoredFiles.join(", ");
71
+ return [
72
+ `Warning: ${files} were excluded from this deploy because this project declares ${APPROVED_DEPLOY_PACKAGE_MANAGER}.`,
73
+ "pnpm-lock.yaml remains the dependency authority. Remove the other lockfiles to prevent local dependency drift:",
74
+ "",
75
+ ` rm -f ${selection.ignoredFiles.join(" ")}`,
76
+ ` Remove-Item ${files} -ErrorAction SilentlyContinue # PowerShell`,
77
+ ].join("\n");
42
78
  }
@@ -6,7 +6,10 @@ import { list } from "tar";
6
6
  import { MIGRATION_BUNDLE_CAPABILITY_V1 } from "@gencow/migration-contract";
7
7
 
8
8
  import { canonicalizeDeployClaimV1, createDeployRequestId, encodeDeployClaimV1 } from "./deploy-contract.mjs";
9
- import { DEPLOY_LOCKFILE_CANDIDATES } from "./deploy-lockfile-selection.mjs";
9
+ import {
10
+ DEPLOY_LOCKFILE_CANDIDATES,
11
+ UNSUPPORTED_DEPLOY_LOCKFILES,
12
+ } from "./deploy-lockfile-selection.mjs";
10
13
 
11
14
  const PACKAGE_MANIFEST = "package.json";
12
15
  const MIGRATION_MANIFEST = ".gencow/migration-bundle-manifest.json";
@@ -26,7 +29,12 @@ const CANONICAL_LEGACY_AUTH_PATTERN = new RegExp(
26
29
  `^${CANONICAL_DEPLOY_BACKEND_ROOT}/(?:auth-schema\\.(?:ts|js)|generated/auth-schema\\.(?:ts|js))$`,
27
30
  "u",
28
31
  );
29
- const SELECTED = new Set([PACKAGE_MANIFEST, MIGRATION_MANIFEST, ...LOCKFILES.keys()]);
32
+ const SELECTED = new Set([
33
+ PACKAGE_MANIFEST,
34
+ MIGRATION_MANIFEST,
35
+ ...LOCKFILES.keys(),
36
+ ...UNSUPPORTED_DEPLOY_LOCKFILES,
37
+ ]);
30
38
 
31
39
  function sha256(value) {
32
40
  return createHash("sha256").update(value).digest("hex");
@@ -89,6 +97,10 @@ export async function buildDeployClaimFromArchive({
89
97
  throw new Error("Static deploy contains a server runtime entry");
90
98
 
91
99
  const lockfiles = [...LOCKFILES].filter(([path]) => selected.has(path));
100
+ const unsupportedLockfiles = UNSUPPORTED_DEPLOY_LOCKFILES.filter((path) => selected.has(path));
101
+ if (unsupportedLockfiles.length > 0) {
102
+ throw new Error(`Deploy dependency lockfile is ambiguous or unsupported: ${unsupportedLockfiles.join(", ")}`);
103
+ }
92
104
  if (lockfiles.length > 1) throw new Error("Deploy dependency lockfile is ambiguous");
93
105
  const projectAuth = findAuthSchemaFiles(allFiles, CANONICAL_PROJECT_AUTH_PATTERN);
94
106
  const legacyAuth = findAuthSchemaFiles(allFiles, CANONICAL_LEGACY_AUTH_PATTERN);
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
3
3
  import { dirname, resolve } from "path";
4
4
  import { fetchAppDiagnosticLogsViaRest, normalizeDiagnosticLogLines } from "./app-diagnostics.mjs";
5
5
  import { resolveCreatedAppResponse } from "./app-create-response.mjs";
@@ -29,7 +29,11 @@ import {
29
29
  renderMigrationDiagnosticLines,
30
30
  } from "./migration-diagnostic.mjs";
31
31
  import { emitMigrationAdvisories, emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
32
- import { detectDeployLockfile, formatDeployLockfileSelectionWarning } from "./deploy-lockfile-selection.mjs";
32
+ import {
33
+ assertCanonicalDeployLockfile,
34
+ detectDeployLockfile,
35
+ formatDeployLockfileSelectionWarning,
36
+ } from "./deploy-lockfile-selection.mjs";
33
37
  import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
34
38
  import { writeCanonicalDeployBundle } from "./deploy-bundle-staging.mjs";
35
39
  async function loadProjectConfigDefault(options) {
@@ -133,7 +137,6 @@ export function createDeployPackageRuntime({
133
137
  statSyncImpl = statSync,
134
138
  successImpl = success,
135
139
  tarCreateImpl,
136
- unlinkSyncImpl = unlinkSync,
137
140
  verifyAppReadyImpl,
138
141
  warnImpl = warn,
139
142
  writeCronManifestForProjectImpl,
@@ -333,7 +336,6 @@ export function createDeployPackageRuntime({
333
336
 
334
337
  infoImpl("Packaging project...");
335
338
  const tmpBundle = resolvePathImpl(cwd, ".gencow", "deploy-bundle.tar.gz");
336
- const filteredPkgPath = resolvePathImpl(cwd, ".gencow", "deploy-package.json");
337
339
  mkdirSyncImpl(dirname(tmpBundle), { recursive: true });
338
340
  const projectConfig = await loadProjectConfigForDeploy({
339
341
  cwd,
@@ -379,18 +381,17 @@ export function createDeployPackageRuntime({
379
381
  return null;
380
382
  }
381
383
 
382
- const lockfileSelection = detectDeployLockfile({
384
+ const lockfileSelection = assertCanonicalDeployLockfile(detectDeployLockfile({
383
385
  cwd,
384
386
  packageManager: projectPkg.packageManager,
385
387
  existsSyncImpl,
386
388
  resolvePathImpl,
387
- });
389
+ }));
388
390
  const lockfileWarning = formatDeployLockfileSelectionWarning(lockfileSelection);
389
391
  if (lockfileWarning) warnImpl(lockfileWarning);
390
392
  const projectConfigSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
391
393
  warnProjectConfigSelectionOnce({ cwd, selection: projectConfigSelection, warnImpl });
392
394
 
393
- let auditResult = null;
394
395
  if (!forceDeploy) {
395
396
  try {
396
397
  const { auditDeployDependencies, formatAuditError } = await (
@@ -398,7 +399,7 @@ export function createDeployPackageRuntime({
398
399
  )();
399
400
  const entryPoint = resolvePathImpl(cwd, backendRoot, "index.ts");
400
401
  if (existsSyncImpl(entryPoint)) {
401
- auditResult = await auditDeployDependencies(entryPoint);
402
+ const auditResult = await auditDeployDependencies(entryPoint);
402
403
  const auditMessage = formatAuditError(auditResult);
403
404
  if (auditMessage) logImpl(auditMessage);
404
405
  }
@@ -407,35 +408,8 @@ export function createDeployPackageRuntime({
407
408
  }
408
409
  }
409
410
 
410
- let useFilteredPkg = false;
411
- if (auditResult?.runtimeDeps?.length >= 0) {
412
- try {
413
- const allDeps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
414
- const runtimeDeps = {};
415
- for (const dependency of auditResult.runtimeDeps) {
416
- if (allDeps[dependency]) runtimeDeps[dependency] = allDeps[dependency];
417
- }
418
- writeFileSyncImpl(
419
- filteredPkgPath,
420
- JSON.stringify({ name: projectPkg.name || "gencow-app", dependencies: runtimeDeps }, null, 2),
421
- );
422
- useFilteredPkg = true;
423
-
424
- const totalDeps = Object.keys(allDeps).length;
425
- const runtimeCount = Object.keys(runtimeDeps).length;
426
- if (totalDeps > runtimeCount) {
427
- infoImpl(
428
- `${DIM}package.json filtered: ${runtimeCount}/${totalDeps} packages will be installed on server.${RESET}`,
429
- );
430
- }
431
- } catch {
432
- useFilteredPkg = false;
433
- }
434
- }
435
-
436
- const stagedPackageJson = useFilteredPkg
437
- ? readFileSyncImpl(filteredPkgPath, "utf8")
438
- : readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8");
411
+ // Keep the manifest byte-compatible with the canonical pnpm lock importer.
412
+ const stagedPackageJson = readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8");
439
413
 
440
414
  let bundleBackendRoot;
441
415
  try {
@@ -465,12 +439,6 @@ export function createDeployPackageRuntime({
465
439
  errorImpl(`Packaging failed: ${caught.message}`);
466
440
  exitImpl(1);
467
441
  return null;
468
- } finally {
469
- try {
470
- if (useFilteredPkg) unlinkSyncImpl(filteredPkgPath);
471
- } catch {
472
- /* ignore */
473
- }
474
442
  }
475
443
 
476
444
  const bundleBuffer = readFileSyncImpl(tmpBundle);
@@ -616,6 +584,7 @@ export function createDeployPackageRuntime({
616
584
  pollAcceptedDeploymentOperation({
617
585
  initialResponse,
618
586
  creds,
587
+ expectedAppName: appId,
619
588
  platformFetch: platformFetchImpl,
620
589
  setTimeoutImpl,
621
590
  });
@@ -1,5 +1,11 @@
1
1
  import { readJsonObjectResponse } from "./http-response-json.mjs";
2
2
 
3
+ const PENDING_STATES = new Set(["accepted", "queued", "running"]);
4
+ const TERMINAL_STATES = new Set(["succeeded", "failed", "blocked", "cancelled"]);
5
+ const OPERATION_ID = /^[1-9][0-9]*$/u;
6
+ const CORRELATION_ID = /^[A-Za-z0-9_-]{8,128}$/u;
7
+ const APP_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/u;
8
+
3
9
  function invalidOperationResponse(message) {
4
10
  return {
5
11
  ok: false,
@@ -15,53 +21,96 @@ function invalidOperationResponse(message) {
15
21
  };
16
22
  }
17
23
 
24
+ function readIdentity(body) {
25
+ const operationId = Number(body?.operationId ?? body?.deployId);
26
+ return {
27
+ operationId,
28
+ correlationId: body?.correlationId,
29
+ appName: body?.appName,
30
+ pollUrl: body?.pollUrl,
31
+ state: body?.state,
32
+ };
33
+ }
34
+
35
+ function identityIsValid(identity, expected, requirePollUrl) {
36
+ const operationText = String(identity.operationId);
37
+ return Boolean(
38
+ Number.isSafeInteger(identity.operationId) &&
39
+ identity.operationId > 0 &&
40
+ OPERATION_ID.test(operationText) &&
41
+ typeof identity.correlationId === "string" &&
42
+ CORRELATION_ID.test(identity.correlationId) &&
43
+ typeof identity.appName === "string" &&
44
+ APP_NAME.test(identity.appName) &&
45
+ identity.appName === expected.appName &&
46
+ (expected.operationId === null || identity.operationId === expected.operationId) &&
47
+ (expected.correlationId === null || identity.correlationId === expected.correlationId) &&
48
+ (!requirePollUrl ||
49
+ (typeof identity.pollUrl === "string" &&
50
+ identity.pollUrl === `/platform/deployments/${identity.operationId}` &&
51
+ (expected.pollUrl === null || identity.pollUrl === expected.pollUrl))),
52
+ );
53
+ }
54
+
55
+ function isNonterminalResponse(response, body, identity) {
56
+ return (
57
+ response.status === 202 ||
58
+ (response.status === 409 &&
59
+ typeof body?.code === "string" &&
60
+ body.code.endsWith("_CONVERGENCE_PENDING") &&
61
+ PENDING_STATES.has(identity.state))
62
+ );
63
+ }
64
+
18
65
  export async function pollAcceptedDeploymentOperation({
19
66
  initialResponse,
20
67
  creds,
68
+ expectedAppName,
21
69
  platformFetch,
22
70
  setTimeoutImpl,
23
71
  }) {
72
+ const expected = {
73
+ operationId: null,
74
+ correlationId: null,
75
+ pollUrl: null,
76
+ appName: expectedAppName,
77
+ };
78
+ if (typeof expected.appName !== "string" || !APP_NAME.test(expected.appName)) {
79
+ return invalidOperationResponse("Deployment operation app identity is invalid");
80
+ }
24
81
  let response = initialResponse;
25
- let expectedOperationId = null;
26
- let expectedPollUrl = null;
27
- for (let attempt = 0; attempt < 900 && response.status === 202; attempt += 1) {
28
- const accepted = await readJsonObjectResponse(response);
29
- const pollUrl = accepted?.pollUrl;
30
- const operationId = Number(accepted?.operationId);
82
+ for (let attempt = 0; attempt < 900; attempt += 1) {
83
+ const body = await readJsonObjectResponse(response);
84
+ const identity = readIdentity(body);
85
+ const nonterminal = isNonterminalResponse(response, body, identity);
86
+ if (nonterminal) {
87
+ if (!PENDING_STATES.has(identity.state) || !identityIsValid(identity, expected, true)) {
88
+ return invalidOperationResponse("Platform returned an invalid deployment operation");
89
+ }
90
+ expected.operationId ??= identity.operationId;
91
+ expected.correlationId ??= identity.correlationId;
92
+ expected.pollUrl ??= identity.pollUrl;
93
+ const retryAfter = Number(response.headers?.get?.("Retry-After"));
94
+ const retryDelayMs =
95
+ Number.isFinite(retryAfter) && retryAfter >= 1 && retryAfter <= 10 ? retryAfter * 1000 : 1000;
96
+ await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, retryDelayMs));
97
+ response = await platformFetch(creds, expected.pollUrl, { method: "GET" });
98
+ continue;
99
+ }
31
100
  if (
32
- typeof pollUrl !== "string" ||
33
- !/^\/platform\/deployments\/[1-9][0-9]*$/u.test(pollUrl) ||
34
- !Number.isSafeInteger(operationId) ||
35
- operationId <= 0 ||
36
- pollUrl !== `/platform/deployments/${operationId}` ||
37
- (expectedOperationId !== null && operationId !== expectedOperationId) ||
38
- (expectedPollUrl !== null && pollUrl !== expectedPollUrl)
101
+ expected.operationId === null ||
102
+ !TERMINAL_STATES.has(identity.state) ||
103
+ !identityIsValid(identity, expected, false)
39
104
  ) {
40
- return invalidOperationResponse("Platform returned an invalid deployment operation");
41
- }
42
- expectedOperationId ??= operationId;
43
- expectedPollUrl ??= pollUrl;
44
- const retryAfter = Number(response.headers?.get?.("Retry-After"));
45
- const retryDelayMs =
46
- Number.isFinite(retryAfter) && retryAfter >= 1 && retryAfter <= 10 ? retryAfter * 1000 : 1000;
47
- await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, retryDelayMs));
48
- response = await platformFetch(creds, pollUrl, { method: "GET" });
49
- }
50
- if (response.status !== 202) {
51
- if (expectedOperationId !== null) {
52
- const terminal = await readJsonObjectResponse(response);
53
- if (Number(terminal?.operationId ?? terminal?.deployId) !== expectedOperationId) {
54
- return invalidOperationResponse("Platform changed deployment operation identity");
55
- }
56
- return {
57
- ok: response.ok,
58
- status: response.status,
59
- statusText: response.statusText,
60
- headers: response.headers,
61
- json: async () => terminal,
62
- };
105
+ return invalidOperationResponse("Platform changed deployment operation identity or state");
63
106
  }
64
- return response;
107
+ return {
108
+ ok: response.ok,
109
+ status: response.status,
110
+ statusText: response.statusText,
111
+ headers: response.headers,
112
+ json: async () => body,
113
+ };
65
114
  }
66
115
  return {
67
116
  ok: false,
@@ -14,7 +14,11 @@ import {
14
14
  } from "./deploy-auditor.mjs";
15
15
  import { shouldIncludeTarEntry } from "./tar-archive.mjs";
16
16
  import { verifyMigrationBundleArchiveRoundTrip } from "./migration-bundle-roundtrip.mjs";
17
- import { detectDeployLockfile, formatDeployLockfileSelectionWarning } from "./deploy-lockfile-selection.mjs";
17
+ import {
18
+ assertCanonicalDeployLockfile,
19
+ detectDeployLockfile,
20
+ formatDeployLockfileSelectionWarning,
21
+ } from "./deploy-lockfile-selection.mjs";
18
22
  import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
19
23
  import {
20
24
  canonicalizeBundleProjectConfig,
@@ -32,21 +36,6 @@ function readJson(path, readFileSyncImpl) {
32
36
  return JSON.parse(readFileSyncImpl(path, "utf8"));
33
37
  }
34
38
 
35
- function buildFilteredPackageJson(projectPkg, runtimeDeps) {
36
- if (!Array.isArray(runtimeDeps)) return projectPkg;
37
-
38
- const allDeps = { ...(projectPkg.dependencies || {}), ...(projectPkg.devDependencies || {}) };
39
- const dependencies = {};
40
- for (const dependency of runtimeDeps) {
41
- if (allDeps[dependency]) dependencies[dependency] = allDeps[dependency];
42
- }
43
-
44
- return {
45
- name: projectPkg.name || "gencow-app",
46
- dependencies,
47
- };
48
- }
49
-
50
39
  function copyOptionalBundleFiles({
51
40
  cwd,
52
41
  stagingDir,
@@ -132,12 +121,12 @@ export async function createDevCloudDeployBundle({
132
121
  if (!existsSyncImpl(functionsSource)) throw new Error(`Backend root dir not found: ${functionsSource}`);
133
122
 
134
123
  const projectPkg = readJson(resolvePathImpl(cwd, "package.json"), readFileSyncImpl);
135
- const lockfileSelection = detectDeployLockfile({
124
+ const lockfileSelection = assertCanonicalDeployLockfile(detectDeployLockfile({
136
125
  cwd,
137
126
  packageManager: projectPkg.packageManager,
138
127
  existsSyncImpl,
139
128
  resolvePathImpl,
140
- });
129
+ }));
141
130
  const lockfileWarning = formatDeployLockfileSelectionWarning(lockfileSelection);
142
131
  if (lockfileWarning) warnImpl(lockfileWarning);
143
132
  const projectConfigSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
@@ -160,7 +149,7 @@ export async function createDevCloudDeployBundle({
160
149
  })
161
150
  : null);
162
151
 
163
- const { auditResult, auditMessage } = await collectRuntimeDependencyAudit({
152
+ const { auditMessage } = await collectRuntimeDependencyAudit({
164
153
  cwd,
165
154
  backendRootDir: bundleRootDir,
166
155
  warnImpl,
@@ -216,8 +205,7 @@ export async function createDevCloudDeployBundle({
216
205
  });
217
206
  if (cronManifest) filesToPack.push(CRON_MANIFEST_RELATIVE_PATH);
218
207
 
219
- const filteredPackage = buildFilteredPackageJson(projectPkg, auditResult?.runtimeDeps);
220
- writeFileSyncImpl(resolvePathImpl(stagingDir, "package.json"), JSON.stringify(filteredPackage, null, 2));
208
+ writeFileSyncImpl(resolvePathImpl(stagingDir, "package.json"), JSON.stringify(projectPkg, null, 2));
221
209
 
222
210
  copyOptionalBundleFiles({
223
211
  cwd,