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.
- package/lib/cli-project-runtime.mjs +1 -1
- package/lib/db-migration-command-steps.mjs +16 -3
- package/lib/db-push-command.mjs +18 -0
- package/lib/deploy-auditor.mjs +1 -1
- package/lib/deploy-dependency-compat.mjs +1 -1
- package/lib/deploy-failure-diagnostic.mjs +12 -3
- package/lib/deploy-lockfile-selection.mjs +47 -11
- package/lib/deploy-package-claim.mjs +14 -2
- package/lib/deploy-package-runtime.mjs +12 -43
- package/lib/deployment-operation-poll.mjs +86 -37
- package/lib/dev-cloud-bundle.mjs +9 -21
- package/lib/doctor-command.mjs +49 -1
- package/lib/init-command.mjs +4 -3
- package/lib/install-features.mjs +4 -5
- package/lib/migration-bundle-manifest.mjs +6 -14
- package/lib/migration-diagnostic.mjs +13 -1
- package/lib/platform-client.mjs +3 -1
- package/lib/readme-codegen.mjs +2 -2
- package/lib/runtime-command.mjs +4 -11
- package/lib/static-deploy-command.mjs +1 -0
- package/package.json +3 -3
- package/runtime/server.mjs +278 -21
- package/runtime/server.mjs.map +4 -4
- package/templates/ai-chat/prompt.md +1 -1
- package/templates/auth.ts +1 -1
- package/templates/fullstack/prompt.md +1 -1
- package/templates/task-app/prompt.md +1 -1
|
@@ -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 '
|
|
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
|
-
|
|
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.");
|
package/lib/db-push-command.mjs
CHANGED
|
@@ -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 }
|
package/lib/deploy-auditor.mjs
CHANGED
|
@@ -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 `
|
|
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(`
|
|
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
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
|
10
|
-
return
|
|
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
|
|
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:
|
|
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.
|
|
41
|
+
const availableFiles = [...DEPLOY_LOCKFILE_CANDIDATES.map(({ file }) => file), ...UNSUPPORTED_DEPLOY_LOCKFILES].filter((file) =>
|
|
34
42
|
existsSyncImpl(resolvePathImpl(cwd, file)),
|
|
35
|
-
)
|
|
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 (
|
|
41
|
-
|
|
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 {
|
|
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([
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
411
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
!
|
|
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
|
|
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
|
|
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,
|
package/lib/dev-cloud-bundle.mjs
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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,
|