gencow 0.1.182 → 0.1.185
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 +14 -4
- package/lib/db-command.mjs +1 -0
- package/lib/db-migration-command-steps.mjs +32 -56
- package/lib/db-push-command.mjs +111 -35
- package/lib/deploy-correlation-diagnostic.mjs +7 -0
- package/lib/deploy-lockfile-selection.mjs +42 -0
- package/lib/deploy-package-claim.mjs +4 -6
- package/lib/deploy-package-runtime.mjs +27 -7
- package/lib/dev-cloud-bundle.mjs +27 -9
- package/lib/dev-cloud-command.mjs +13 -4
- package/lib/migration-advisory.mjs +41 -0
- package/lib/migration-diagnostic.mjs +81 -5
- package/lib/migration-operation-client.mjs +2 -1
- package/lib/project-config-selection.mjs +32 -0
- package/package.json +3 -3
- package/runtime/config.mjs +0 -3
- package/runtime/server.mjs +299 -96
- package/runtime/server.mjs.map +4 -4
- package/runtime/tooling.mjs +0 -3
|
@@ -13,6 +13,7 @@ import { resolveCodegenServerPublishDir } from "./codegen-command.mjs";
|
|
|
13
13
|
import { logEnvFileOperationWithType, resolveBackendEnvFile } from "./backend-env-resolver.mjs";
|
|
14
14
|
import { parseEnvFile } from "./env-parser.mjs";
|
|
15
15
|
import { BOLD, DIM, RESET, error, info, success, warn } from "./output.mjs";
|
|
16
|
+
import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
|
|
16
17
|
import { hasWorkspaceConfigSourceRuntime } from "./runtime-mode.mjs";
|
|
17
18
|
import { SUPPORTED_DRIZZLE_KIT_GENERATOR_VERSION } from "./drizzle-generate.mjs";
|
|
18
19
|
|
|
@@ -244,9 +245,9 @@ export async function loadConfig(options = {}) {
|
|
|
244
245
|
|
|
245
246
|
// Prefer gencow.config.js. gencow.config.ts is a legacy path handled by a
|
|
246
247
|
// regex parser; it will be removed once all projects migrate.
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
const path =
|
|
248
|
+
const configSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
|
|
249
|
+
warnProjectConfigSelectionOnce({ cwd, selection: configSelection, warnImpl });
|
|
250
|
+
const path = configSelection.selectedFile ? resolvePathImpl(cwd, configSelection.selectedFile) : null;
|
|
250
251
|
|
|
251
252
|
if (!path) {
|
|
252
253
|
warnImpl("No gencow.config.js found — using defaults");
|
|
@@ -286,10 +287,19 @@ export async function loadConfig(options = {}) {
|
|
|
286
287
|
let parsed;
|
|
287
288
|
try {
|
|
288
289
|
const trimPath = (value) => (typeof value === "string" && value.trim().length > 0 ? value.trim() : null);
|
|
289
|
-
|
|
290
|
+
const rootDir = trimPath(raw?.rootDir);
|
|
291
|
+
const functionsDir = trimPath(raw?.functionsDir);
|
|
292
|
+
if (rootDir && functionsDir) {
|
|
293
|
+
warnImpl("[gencow] Both config paths are set. Using `rootDir`; ignoring deprecated `functionsDir`.");
|
|
294
|
+
} else if (!rootDir && functionsDir && !functionsDirDeprecationWarned) {
|
|
290
295
|
warnImpl("[gencow] `functionsDir` is deprecated; use `rootDir` instead (same meaning).");
|
|
291
296
|
functionsDirDeprecationWarned = true;
|
|
292
297
|
}
|
|
298
|
+
if (trimPath(raw?.codegen?.clientOutDir) && trimPath(raw?.codegen?.outDir)) {
|
|
299
|
+
warnImpl(
|
|
300
|
+
"[gencow] Both codegen output paths are set. Using `codegen.clientOutDir`; ignoring deprecated `codegen.outDir`.",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
293
303
|
parsed = gencowConfigSchema.parse(raw ?? {});
|
|
294
304
|
} catch (caught) {
|
|
295
305
|
const message = `Invalid gencow.config: ${caught.message}`;
|
package/lib/db-command.mjs
CHANGED
|
@@ -115,6 +115,7 @@ function renderDbCheckHelp(logImpl = log) {
|
|
|
115
115
|
logImpl(` ${BOLD}Options:${RESET}`);
|
|
116
116
|
logImpl(` ${DIM}--prod${RESET} Check the production DB (no mutation or confirmation)`);
|
|
117
117
|
logImpl(` ${DIM}--app, -a${RESET} Target specific app`);
|
|
118
|
+
logImpl(` ${DIM}--inspect-state${RESET} Run the dedicated platform read-only state inspection`);
|
|
118
119
|
logImpl(` ${DIM}--json${RESET} Print a stable machine-readable result\n`);
|
|
119
120
|
logImpl(` ${DIM}db:check inspects committed migrations and never runs drizzle-kit.${RESET}\n`);
|
|
120
121
|
}
|
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MIGRATION_PUSH_RESULT_VERSION,
|
|
3
|
-
isCanonicalMigrationCapabilityManifestV1,
|
|
4
|
-
} from "@gencow/migration-contract";
|
|
5
1
|
import {
|
|
6
2
|
buildClientUserFixableDiagnostic,
|
|
7
3
|
buildLocalExecutionFailureDiagnostic,
|
|
8
|
-
buildMigrationCapabilityPreflightUnsupportedDiagnostic,
|
|
9
4
|
} from "./migration-client-diagnostic.mjs";
|
|
10
5
|
import { resolveBackendEnvFile } from "./backend-env-resolver.mjs";
|
|
11
|
-
import {
|
|
12
|
-
buildMigrationNoResponseDiagnostic,
|
|
13
|
-
enrichMigrationDiagnostic,
|
|
14
|
-
extractActionableMigrationDiagnostic,
|
|
15
|
-
isActionableMigrationDiagnostic,
|
|
16
|
-
} from "./migration-diagnostic.mjs";
|
|
6
|
+
import { buildMigrationNoResponseDiagnostic } from "./migration-diagnostic.mjs";
|
|
17
7
|
import { buildMigrationStatusCommand, fetchMigrationOperationStatus } from "./migration-operation-client.mjs";
|
|
18
8
|
import { BOLD, CYAN, DIM, RED, RESET } from "./output.mjs";
|
|
9
|
+
import { emitMigrationAdvisories, extractMigrationAdvisories } from "./migration-advisory.mjs";
|
|
19
10
|
|
|
20
11
|
const NO_CHANGE_MARKERS = ["No schema changes", "nothing to migrate", "No changes detected"];
|
|
21
12
|
|
|
@@ -146,50 +137,6 @@ export async function handleMigrationStatus({
|
|
|
146
137
|
}
|
|
147
138
|
}
|
|
148
139
|
|
|
149
|
-
export async function checkMigrationCapability({
|
|
150
|
-
deps,
|
|
151
|
-
creds,
|
|
152
|
-
appId,
|
|
153
|
-
isCheck,
|
|
154
|
-
functionsDir,
|
|
155
|
-
retryCommand,
|
|
156
|
-
correlationId,
|
|
157
|
-
emitFailure,
|
|
158
|
-
}) {
|
|
159
|
-
try {
|
|
160
|
-
const response = await deps.platformFetchImpl(creds, `/platform/apps/${appId}/migration-capabilities`, {
|
|
161
|
-
method: "GET",
|
|
162
|
-
headers: { "X-Gencow-CLI-Version": deps.cliVersion },
|
|
163
|
-
});
|
|
164
|
-
const data = await response.json().catch(() => ({}));
|
|
165
|
-
if (!response.ok) {
|
|
166
|
-
emitFailure(
|
|
167
|
-
isActionableMigrationDiagnostic(data)
|
|
168
|
-
? enrichMigrationDiagnostic(extractActionableMigrationDiagnostic(data), {
|
|
169
|
-
projectRoot: deps.cwdImpl(),
|
|
170
|
-
functionsDir,
|
|
171
|
-
retryCommand,
|
|
172
|
-
})
|
|
173
|
-
: buildMigrationCapabilityPreflightUnsupportedDiagnostic(correlationId()),
|
|
174
|
-
);
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
if (
|
|
178
|
-
data?.success !== true ||
|
|
179
|
-
data?.app !== appId ||
|
|
180
|
-
!isCanonicalMigrationCapabilityManifestV1(data?.capability, deps.cliVersion) ||
|
|
181
|
-
(!isCheck && data?.pushResultVersion !== MIGRATION_PUSH_RESULT_VERSION)
|
|
182
|
-
) {
|
|
183
|
-
emitFailure(buildMigrationCapabilityPreflightUnsupportedDiagnostic(correlationId()));
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
return true;
|
|
187
|
-
} catch {
|
|
188
|
-
emitFailure(buildMigrationCapabilityPreflightUnsupportedDiagnostic(correlationId()));
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
140
|
export function emitCloudMigrationSuccess({
|
|
194
141
|
deps,
|
|
195
142
|
command,
|
|
@@ -198,11 +145,13 @@ export function emitCloudMigrationSuccess({
|
|
|
198
145
|
existingBundleMode,
|
|
199
146
|
isCheck,
|
|
200
147
|
checkData,
|
|
148
|
+
isInspection,
|
|
201
149
|
pushSuccess,
|
|
202
150
|
jsonMode,
|
|
203
151
|
emitJson,
|
|
204
152
|
}) {
|
|
205
153
|
const migration = pushSuccess?.migration ?? null;
|
|
154
|
+
const warnings = extractMigrationAdvisories(isCheck ? checkData?.plan?.advisories : pushSuccess?.warnings);
|
|
206
155
|
if (jsonMode) {
|
|
207
156
|
emitJson({
|
|
208
157
|
diagnosticVersion: 1,
|
|
@@ -212,17 +161,43 @@ export function emitCloudMigrationSuccess({
|
|
|
212
161
|
environment,
|
|
213
162
|
databaseState: isCheck ? "UNCHANGED" : "APPLIED",
|
|
214
163
|
...(existingBundleMode ? { projectState: "UNCHANGED" } : {}),
|
|
215
|
-
...(isCheck
|
|
164
|
+
...(isCheck
|
|
165
|
+
? isInspection
|
|
166
|
+
? { inspection: checkData.inspection, capability: checkData.capability }
|
|
167
|
+
: { plan: checkData.plan, capability: checkData.capability }
|
|
168
|
+
: {}),
|
|
216
169
|
...(migration ? { migration } : {}),
|
|
217
170
|
...(pushSuccess ? { receiptId: pushSuccess.receipt.receiptId } : {}),
|
|
171
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
218
172
|
});
|
|
219
173
|
return;
|
|
220
174
|
}
|
|
221
175
|
if (isCheck) {
|
|
176
|
+
if (isInspection) {
|
|
177
|
+
const ready = checkData.inspection.outcome === "READY";
|
|
178
|
+
(ready ? deps.successImpl : deps.warnImpl)(
|
|
179
|
+
ready
|
|
180
|
+
? "Migration state inspection completed; the plan is ready."
|
|
181
|
+
: "Migration state inspection completed; the plan remains blocked.",
|
|
182
|
+
);
|
|
183
|
+
deps.logImpl(
|
|
184
|
+
` ${DIM}Code: ${checkData.inspection.code} · Next: ${checkData.inspection.nextAction} · Database unchanged${RESET}\n`,
|
|
185
|
+
);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
222
188
|
deps.successImpl("Migration plan is safe to proceed.");
|
|
189
|
+
if (checkData.plan.databaseBootstrapRequired === true) {
|
|
190
|
+
deps.logImpl(` ${DIM}Fresh database will be bootstrapped by deploy or db:push.${RESET}`);
|
|
191
|
+
}
|
|
192
|
+
if (checkData.plan.migrationStorePreparationRequired === true) {
|
|
193
|
+
deps.logImpl(
|
|
194
|
+
` ${DIM}Gencow will prepare the classified migration store during deploy or db:push.${RESET}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
223
197
|
deps.logImpl(
|
|
224
198
|
` ${DIM}Applied verified: ${checkData.plan.appliedVerifiedCount} · Pending: ${checkData.plan.pendingCount} · Database unchanged${RESET}\n`,
|
|
225
199
|
);
|
|
200
|
+
emitMigrationAdvisories(warnings, deps.warnImpl);
|
|
226
201
|
return;
|
|
227
202
|
}
|
|
228
203
|
deps.successImpl(
|
|
@@ -231,4 +206,5 @@ export function emitCloudMigrationSuccess({
|
|
|
231
206
|
: "Versioned migrations applied.",
|
|
232
207
|
);
|
|
233
208
|
deps.logImpl(` ${DIM}Receipt: ${pushSuccess.receipt.receiptId}${RESET}\n`);
|
|
209
|
+
emitMigrationAdvisories(warnings, deps.warnImpl);
|
|
234
210
|
}
|
package/lib/db-push-command.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
MIGRATION_NEXT_ACTION_KINDS,
|
|
3
|
+
isCompatibleMigrationCapabilityManifestV1,
|
|
4
|
+
} from "@gencow/migration-contract";
|
|
2
5
|
import {
|
|
3
6
|
resolveDrizzleKitGeneratorVersion,
|
|
4
7
|
runDrizzleGenerateWithFallback,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
enrichMigrationDiagnostic,
|
|
13
16
|
extractActionableMigrationDiagnostic,
|
|
14
17
|
isActionableMigrationDiagnostic,
|
|
18
|
+
normalizeRetiredMigrationGateDiagnostic,
|
|
15
19
|
renderMigrationDiagnosticLines,
|
|
16
20
|
} from "./migration-diagnostic.mjs";
|
|
17
21
|
import {
|
|
@@ -31,7 +35,6 @@ import {
|
|
|
31
35
|
readMigrationStatusReceiptId,
|
|
32
36
|
} from "./migration-operation-client.mjs";
|
|
33
37
|
import {
|
|
34
|
-
checkMigrationCapability,
|
|
35
38
|
emitCloudMigrationSuccess,
|
|
36
39
|
handleLocalMigrationCommand,
|
|
37
40
|
handleMigrationStatus,
|
|
@@ -39,6 +42,7 @@ import {
|
|
|
39
42
|
promptDbPushConfirm,
|
|
40
43
|
} from "./db-migration-command-steps.mjs";
|
|
41
44
|
import { resolveConfiguredSchemaPaths, resolveDrizzleSchemaPaths } from "./cli-schema-paths.mjs";
|
|
45
|
+
import { emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
|
|
42
46
|
|
|
43
47
|
function clientCorrelationId() {
|
|
44
48
|
return globalThis.crypto?.randomUUID?.() ?? `client-${Date.now()}`;
|
|
@@ -65,6 +69,16 @@ export function buildDbMigrationRetryCommand({ command, appId, args, isProd, jso
|
|
|
65
69
|
if (explicitAppRequested(args)) parts.push("--app", shellQuote(appId));
|
|
66
70
|
if (isProd) parts.push("--prod");
|
|
67
71
|
if (args.includes("--existing-bundle")) parts.push("--existing-bundle");
|
|
72
|
+
if (args.includes("--inspect-state")) parts.push("--inspect-state");
|
|
73
|
+
if (jsonMode) parts.push("--json");
|
|
74
|
+
return parts.join(" ");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildDbMigrationInspectionCommand({ appId, args, isProd, jsonMode }) {
|
|
78
|
+
const parts = ["gencow", "db:check"];
|
|
79
|
+
if (explicitAppRequested(args)) parts.push("--app", shellQuote(appId));
|
|
80
|
+
if (isProd) parts.push("--prod");
|
|
81
|
+
parts.push("--inspect-state");
|
|
68
82
|
if (jsonMode) parts.push("--json");
|
|
69
83
|
return parts.join(" ");
|
|
70
84
|
}
|
|
@@ -75,9 +89,65 @@ function renderFailure(diagnostic, { errorImpl, logImpl }) {
|
|
|
75
89
|
lines.slice(1).forEach((line) => logImpl(line));
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
function extractReadOnlyMigrationInspection(data) {
|
|
93
|
+
const inspection = data?.inspection;
|
|
94
|
+
if (
|
|
95
|
+
inspection?.readOnly !== true ||
|
|
96
|
+
!["READY", "BLOCKED"].includes(inspection.outcome) ||
|
|
97
|
+
typeof inspection.code !== "string" ||
|
|
98
|
+
!/^[A-Z][A-Z0-9_]{0,127}$/u.test(inspection.code) ||
|
|
99
|
+
typeof inspection.nextAction !== "string" ||
|
|
100
|
+
!(
|
|
101
|
+
MIGRATION_NEXT_ACTION_KINDS.includes(inspection.nextAction) ||
|
|
102
|
+
inspection.nextAction === "RETRY_OR_PUSH" ||
|
|
103
|
+
inspection.nextAction === "PLATFORM_RECONCILIATION"
|
|
104
|
+
) ||
|
|
105
|
+
(inspection.correlationId !== undefined &&
|
|
106
|
+
(typeof inspection.correlationId !== "string" ||
|
|
107
|
+
!/^[A-Za-z0-9._:-]{1,200}$/u.test(inspection.correlationId)))
|
|
108
|
+
) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const plan = inspection.plan;
|
|
112
|
+
if (
|
|
113
|
+
inspection.outcome === "READY" &&
|
|
114
|
+
!(
|
|
115
|
+
typeof plan?.planId === "string" &&
|
|
116
|
+
/^mig_[a-f0-9]{24}$/u.test(plan.planId) &&
|
|
117
|
+
Number.isSafeInteger(plan.appliedVerifiedCount) &&
|
|
118
|
+
plan.appliedVerifiedCount >= 0 &&
|
|
119
|
+
Number.isSafeInteger(plan.pendingCount) &&
|
|
120
|
+
plan.pendingCount >= 0
|
|
121
|
+
)
|
|
122
|
+
) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
readOnly: true,
|
|
127
|
+
outcome: inspection.outcome,
|
|
128
|
+
code: inspection.code,
|
|
129
|
+
nextAction: inspection.nextAction,
|
|
130
|
+
...(inspection.correlationId ? { correlationId: inspection.correlationId } : {}),
|
|
131
|
+
...(inspection.outcome === "READY"
|
|
132
|
+
? {
|
|
133
|
+
plan: {
|
|
134
|
+
planId: plan.planId,
|
|
135
|
+
appliedVerifiedCount: plan.appliedVerifiedCount,
|
|
136
|
+
pendingCount: plan.pendingCount,
|
|
137
|
+
...(plan.databaseBootstrapRequired === true ? { databaseBootstrapRequired: true } : {}),
|
|
138
|
+
...(plan.migrationStorePreparationRequired === true
|
|
139
|
+
? { migrationStorePreparationRequired: true }
|
|
140
|
+
: {}),
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
78
147
|
function createDbMigrationCommand(deps, command) {
|
|
79
148
|
return async (...args) => {
|
|
80
149
|
const isCheck = command === "db:check";
|
|
150
|
+
const isInspection = isCheck && args.includes("--inspect-state");
|
|
81
151
|
const existingBundleMode = isCheck || args.includes("--existing-bundle");
|
|
82
152
|
const sourceMode = existingBundleMode ? "EXISTING_BUNDLE" : "GENERATED";
|
|
83
153
|
const jsonMode = args.includes("--json");
|
|
@@ -98,7 +168,7 @@ function createDbMigrationCommand(deps, command) {
|
|
|
98
168
|
const execute = async () => {
|
|
99
169
|
let config;
|
|
100
170
|
try {
|
|
101
|
-
config = await deps.loadConfig();
|
|
171
|
+
config = await deps.loadConfig(jsonMode ? { warnImpl: () => {} } : undefined);
|
|
102
172
|
} catch (error) {
|
|
103
173
|
if (!jsonMode) throw error;
|
|
104
174
|
emitFailure(
|
|
@@ -161,6 +231,10 @@ function createDbMigrationCommand(deps, command) {
|
|
|
161
231
|
return;
|
|
162
232
|
}
|
|
163
233
|
|
|
234
|
+
if (!isCheck && !statusReceiptId && !jsonMode) {
|
|
235
|
+
emitMigrationResponsibilityNotice(deps.warnImpl);
|
|
236
|
+
}
|
|
237
|
+
|
|
164
238
|
if (
|
|
165
239
|
!isCheck &&
|
|
166
240
|
!statusReceiptId &&
|
|
@@ -320,22 +394,7 @@ function createDbMigrationCommand(deps, command) {
|
|
|
320
394
|
}
|
|
321
395
|
|
|
322
396
|
const retryCommand = buildDbMigrationRetryCommand({ command, appId, args, isProd, jsonMode });
|
|
323
|
-
|
|
324
|
-
const capabilityReady = await checkMigrationCapability({
|
|
325
|
-
deps,
|
|
326
|
-
creds,
|
|
327
|
-
appId,
|
|
328
|
-
isCheck,
|
|
329
|
-
functionsDir,
|
|
330
|
-
retryCommand,
|
|
331
|
-
correlationId: clientCorrelationId,
|
|
332
|
-
emitFailure,
|
|
333
|
-
});
|
|
334
|
-
remoteRequestStarted = false;
|
|
335
|
-
if (!capabilityReady) {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
397
|
+
const inspectionCommand = buildDbMigrationInspectionCommand({ appId, args, isProd, jsonMode });
|
|
339
398
|
let stagedBundle;
|
|
340
399
|
try {
|
|
341
400
|
stagedBundle = await stageMigrationBundleArchive({
|
|
@@ -387,7 +446,7 @@ function createDbMigrationCommand(deps, command) {
|
|
|
387
446
|
remoteRequestStarted = true;
|
|
388
447
|
res = await deps.platformFetchImpl(
|
|
389
448
|
creds,
|
|
390
|
-
`/platform/apps/${appId}/${isCheck ? "schema-check" : "schema-push"}`,
|
|
449
|
+
`/platform/apps/${appId}/${isInspection ? "schema-inspection" : isCheck ? "schema-check" : "schema-push"}`,
|
|
391
450
|
{
|
|
392
451
|
method: "POST",
|
|
393
452
|
headers: {
|
|
@@ -431,11 +490,15 @@ function createDbMigrationCommand(deps, command) {
|
|
|
431
490
|
const data = recoveredSuccess ? null : await res.json().catch(() => ({}));
|
|
432
491
|
if (!recoveredSuccess && !res.ok) {
|
|
433
492
|
const diagnostic = isActionableMigrationDiagnostic(data)
|
|
434
|
-
? enrichMigrationDiagnostic(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
493
|
+
? enrichMigrationDiagnostic(
|
|
494
|
+
normalizeRetiredMigrationGateDiagnostic(extractActionableMigrationDiagnostic(data)),
|
|
495
|
+
{
|
|
496
|
+
projectRoot: deps.cwdImpl(),
|
|
497
|
+
functionsDir,
|
|
498
|
+
retryCommand,
|
|
499
|
+
inspectionCommand,
|
|
500
|
+
},
|
|
501
|
+
)
|
|
439
502
|
: isCheck
|
|
440
503
|
? buildMigrationCheckUnsupportedResponseDiagnostic(
|
|
441
504
|
typeof data.correlationId === "string" && data.correlationId.length <= 200
|
|
@@ -452,17 +515,29 @@ function createDbMigrationCommand(deps, command) {
|
|
|
452
515
|
return;
|
|
453
516
|
}
|
|
454
517
|
|
|
455
|
-
const
|
|
456
|
-
|
|
518
|
+
const inspectionResult = isInspection ? extractReadOnlyMigrationInspection(data) : null;
|
|
519
|
+
const validInspectionSuccess =
|
|
520
|
+
isInspection &&
|
|
457
521
|
data?.dryRun === true &&
|
|
458
522
|
data?.databaseState === "UNCHANGED" &&
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
523
|
+
inspectionResult !== null &&
|
|
524
|
+
isCompatibleMigrationCapabilityManifestV1(data?.capability, deps.cliVersion);
|
|
525
|
+
const validCheckSuccess =
|
|
526
|
+
isCheck &&
|
|
527
|
+
(validInspectionSuccess ||
|
|
528
|
+
(data?.dryRun === true &&
|
|
529
|
+
data?.databaseState === "UNCHANGED" &&
|
|
530
|
+
typeof data?.plan?.planId === "string" &&
|
|
531
|
+
/^mig_[a-f0-9]{24}$/u.test(data.plan.planId) &&
|
|
532
|
+
Number.isSafeInteger(data?.plan?.appliedVerifiedCount) &&
|
|
533
|
+
data.plan.appliedVerifiedCount >= 0 &&
|
|
534
|
+
Number.isSafeInteger(data?.plan?.pendingCount) &&
|
|
535
|
+
data.plan.pendingCount >= 0 &&
|
|
536
|
+
(data.plan.databaseBootstrapRequired === undefined ||
|
|
537
|
+
data.plan.databaseBootstrapRequired === true) &&
|
|
538
|
+
(data.plan.migrationStorePreparationRequired === undefined ||
|
|
539
|
+
data.plan.migrationStorePreparationRequired === true) &&
|
|
540
|
+
isCompatibleMigrationCapabilityManifestV1(data?.capability, deps.cliVersion)));
|
|
466
541
|
if (
|
|
467
542
|
!recoveredSuccess &&
|
|
468
543
|
(data?.success !== true || data?.app !== appId || (isCheck && !validCheckSuccess))
|
|
@@ -504,7 +579,8 @@ function createDbMigrationCommand(deps, command) {
|
|
|
504
579
|
environment: envLabel,
|
|
505
580
|
existingBundleMode,
|
|
506
581
|
isCheck,
|
|
507
|
-
checkData: data,
|
|
582
|
+
checkData: isInspection ? { ...data, inspection: inspectionResult } : data,
|
|
583
|
+
isInspection,
|
|
508
584
|
pushSuccess,
|
|
509
585
|
jsonMode,
|
|
510
586
|
emitJson,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const DEPLOY_LOCKFILE_CANDIDATES = Object.freeze([
|
|
2
|
+
{ file: "bun.lock", packageManager: "bun" },
|
|
3
|
+
{ file: "bun.lockb", packageManager: "bun" },
|
|
4
|
+
{ file: "pnpm-lock.yaml", packageManager: "pnpm" },
|
|
5
|
+
{ file: "package-lock.json", packageManager: "npm" },
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function declaredPackageManager(value) {
|
|
9
|
+
if (typeof value !== "string") return null;
|
|
10
|
+
return /^(bun|pnpm|npm)(?:@|$)/u.exec(value.trim())?.[1] ?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function selectDeployLockfile({ availableFiles, packageManager }) {
|
|
14
|
+
const available = new Set(availableFiles);
|
|
15
|
+
const detected = DEPLOY_LOCKFILE_CANDIDATES.filter(({ file }) => available.has(file));
|
|
16
|
+
const declared = declaredPackageManager(packageManager);
|
|
17
|
+
const declaredMatch = declared ? detected.find((candidate) => candidate.packageManager === declared) : null;
|
|
18
|
+
const selected = declaredMatch ?? detected[0] ?? null;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
selectedFile: selected?.file ?? null,
|
|
22
|
+
selectedPackageManager: selected?.packageManager ?? "none",
|
|
23
|
+
ignoredFiles: detected.filter(({ file }) => file !== selected?.file).map(({ file }) => file),
|
|
24
|
+
selectionSource: declaredMatch
|
|
25
|
+
? "package.json#packageManager"
|
|
26
|
+
: selected
|
|
27
|
+
? "deterministic priority"
|
|
28
|
+
: "none",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function detectDeployLockfile({ cwd, packageManager, existsSyncImpl, resolvePathImpl }) {
|
|
33
|
+
const availableFiles = DEPLOY_LOCKFILE_CANDIDATES.filter(({ file }) =>
|
|
34
|
+
existsSyncImpl(resolvePathImpl(cwd, file)),
|
|
35
|
+
).map(({ file }) => file);
|
|
36
|
+
return selectDeployLockfile({ availableFiles, packageManager });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
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(", ")}.`;
|
|
42
|
+
}
|
|
@@ -6,15 +6,13 @@ 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
10
|
|
|
10
11
|
const PACKAGE_MANIFEST = "package.json";
|
|
11
12
|
const MIGRATION_MANIFEST = ".gencow/migration-bundle-manifest.json";
|
|
12
|
-
const LOCKFILES = new Map(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
["bun.lock", "bun"],
|
|
16
|
-
["bun.lockb", "bun"],
|
|
17
|
-
]);
|
|
13
|
+
const LOCKFILES = new Map(
|
|
14
|
+
DEPLOY_LOCKFILE_CANDIDATES.map(({ file, packageManager }) => [file, packageManager]),
|
|
15
|
+
);
|
|
18
16
|
const PROJECT_AUTH = new Set(["gencow/schema-auth.ts", "gencow/schema-auth.js"]);
|
|
19
17
|
const LEGACY_AUTH = new Set([
|
|
20
18
|
"gencow/auth-schema.ts",
|
|
@@ -27,8 +27,13 @@ import { assertToolingRuntimeCapabilities, loadToolingRuntime } from "./tooling-
|
|
|
27
27
|
import {
|
|
28
28
|
extractActionableMigrationDiagnostic,
|
|
29
29
|
isActionableMigrationDiagnostic,
|
|
30
|
+
normalizeRetiredMigrationGateDiagnostic,
|
|
30
31
|
renderMigrationDiagnosticLines,
|
|
31
32
|
} from "./migration-diagnostic.mjs";
|
|
33
|
+
import { emitMigrationAdvisories, emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
|
|
34
|
+
import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
|
|
35
|
+
import { detectDeployLockfile, formatDeployLockfileSelectionWarning } from "./deploy-lockfile-selection.mjs";
|
|
36
|
+
import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
|
|
32
37
|
|
|
33
38
|
async function loadInternalToolingDefault() {
|
|
34
39
|
return loadToolingRuntime({
|
|
@@ -340,10 +345,11 @@ export function createDeployPackageRuntime({
|
|
|
340
345
|
let dependencyAuditManifestPath = null;
|
|
341
346
|
let dependencyAuditManifest = null;
|
|
342
347
|
let dependencyVersionMessage = "";
|
|
348
|
+
let projectPkg;
|
|
343
349
|
try {
|
|
344
350
|
const { DEPLOY_DEPENDENCY_AUDIT_MANIFEST, auditDependencyVersions, formatDependencyVersionAuditError } =
|
|
345
351
|
await (loadDeployAuditorImpl ?? loadDeployAuditorDefault)();
|
|
346
|
-
|
|
352
|
+
projectPkg = JSON.parse(readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8"));
|
|
347
353
|
const dependencyVersionResult = auditDependencyVersions(projectPkg);
|
|
348
354
|
dependencyVersionMessage = formatDependencyVersionAuditError(dependencyVersionResult);
|
|
349
355
|
dependencyAuditManifestPath = resolvePathImpl(cwd, DEPLOY_DEPENDENCY_AUDIT_MANIFEST);
|
|
@@ -360,6 +366,17 @@ export function createDeployPackageRuntime({
|
|
|
360
366
|
return null;
|
|
361
367
|
}
|
|
362
368
|
|
|
369
|
+
const lockfileSelection = detectDeployLockfile({
|
|
370
|
+
cwd,
|
|
371
|
+
packageManager: projectPkg.packageManager,
|
|
372
|
+
existsSyncImpl,
|
|
373
|
+
resolvePathImpl,
|
|
374
|
+
});
|
|
375
|
+
const lockfileWarning = formatDeployLockfileSelectionWarning(lockfileSelection);
|
|
376
|
+
if (lockfileWarning) warnImpl(lockfileWarning);
|
|
377
|
+
const projectConfigSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
|
|
378
|
+
warnProjectConfigSelectionOnce({ cwd, selection: projectConfigSelection, warnImpl });
|
|
379
|
+
|
|
363
380
|
try {
|
|
364
381
|
mkdirSyncImpl(dirname(dependencyAuditManifestPath), { recursive: true });
|
|
365
382
|
writeFileSyncImpl(dependencyAuditManifestPath, JSON.stringify(dependencyAuditManifest, null, 2));
|
|
@@ -438,17 +455,14 @@ export function createDeployPackageRuntime({
|
|
|
438
455
|
exitImpl(1);
|
|
439
456
|
return null;
|
|
440
457
|
}
|
|
441
|
-
if (
|
|
442
|
-
else if (existsSyncImpl(resolvePathImpl(cwd, "gencow.config.js"))) filesToPack.push("gencow.config.js");
|
|
458
|
+
if (projectConfigSelection.selectedFile) filesToPack.push(projectConfigSelection.selectedFile);
|
|
443
459
|
if (dependencyAuditManifestPath) filesToPack.push(".gencow/deploy-dependency-audit.json");
|
|
444
460
|
if (cronManifestPath) filesToPack.push(CRON_MANIFEST_RELATIVE_PATH);
|
|
445
|
-
if (
|
|
446
|
-
if (existsSyncImpl(resolvePathImpl(cwd, "package-lock.json"))) filesToPack.push("package-lock.json");
|
|
461
|
+
if (lockfileSelection.selectedFile) filesToPack.push(lockfileSelection.selectedFile);
|
|
447
462
|
if (existsSyncImpl(resolvePathImpl(cwd, "tsconfig.json"))) filesToPack.push("tsconfig.json");
|
|
448
463
|
|
|
449
464
|
if (auditResult?.runtimeDeps?.length >= 0) {
|
|
450
465
|
try {
|
|
451
|
-
const projectPkg = JSON.parse(readFileSyncImpl(resolvePathImpl(cwd, "package.json"), "utf8"));
|
|
452
466
|
const allDeps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
|
|
453
467
|
const runtimeDeps = {};
|
|
454
468
|
for (const dependency of auditResult.runtimeDeps) {
|
|
@@ -618,6 +632,9 @@ export function createDeployPackageRuntime({
|
|
|
618
632
|
explicitRequestId = null,
|
|
619
633
|
}) {
|
|
620
634
|
const cwd = cwdImpl();
|
|
635
|
+
if (deployClaim?.claim?.migrationBundle?.manifestSha256) {
|
|
636
|
+
emitMigrationResponsibilityNotice(warnImpl);
|
|
637
|
+
}
|
|
621
638
|
let receipt = deployClaim
|
|
622
639
|
? acquireDeployReceipt({
|
|
623
640
|
cwd,
|
|
@@ -717,7 +734,7 @@ export function createDeployPackageRuntime({
|
|
|
717
734
|
errData?.retryClass === "retryable-infra";
|
|
718
735
|
if (receipt && !retryableResponse) releaseDeployReceipt(receipt);
|
|
719
736
|
const migrationDiagnostic = isActionableMigrationDiagnostic(errData?.diagnostic)
|
|
720
|
-
? extractActionableMigrationDiagnostic(errData.diagnostic)
|
|
737
|
+
? normalizeRetiredMigrationGateDiagnostic(extractActionableMigrationDiagnostic(errData.diagnostic))
|
|
721
738
|
: null;
|
|
722
739
|
if (migrationDiagnostic) {
|
|
723
740
|
const lines = renderMigrationDiagnosticLines(migrationDiagnostic);
|
|
@@ -725,6 +742,8 @@ export function createDeployPackageRuntime({
|
|
|
725
742
|
lines.slice(1).forEach((line) => logImpl(line));
|
|
726
743
|
} else {
|
|
727
744
|
errorImpl(`Deploy failed: ${errData.error || deployRes.statusText}`);
|
|
745
|
+
const correlationLine = formatDeployCorrelationId(errData?.correlationId);
|
|
746
|
+
if (correlationLine) errorImpl(correlationLine);
|
|
728
747
|
}
|
|
729
748
|
await showCrashLogs(errData, "Server startup failure cause:", targetAppId);
|
|
730
749
|
exitImpl(1);
|
|
@@ -732,6 +751,7 @@ export function createDeployPackageRuntime({
|
|
|
732
751
|
}
|
|
733
752
|
|
|
734
753
|
const deployData = await deployRes.json();
|
|
754
|
+
emitMigrationAdvisories(deployData?.warnings, warnImpl);
|
|
735
755
|
if (receipt) releaseDeployReceipt(receipt);
|
|
736
756
|
const deployElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
737
757
|
logImpl("");
|
package/lib/dev-cloud-bundle.mjs
CHANGED
|
@@ -14,16 +14,10 @@ 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";
|
|
18
|
+
import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
|
|
17
19
|
|
|
18
|
-
const OPTIONAL_BUNDLE_FILES = [
|
|
19
|
-
"gencow.config.js",
|
|
20
|
-
"gencow.config.ts",
|
|
21
|
-
"bun.lock",
|
|
22
|
-
"bun.lockb",
|
|
23
|
-
"package-lock.json",
|
|
24
|
-
"pnpm-lock.yaml",
|
|
25
|
-
"tsconfig.json",
|
|
26
|
-
];
|
|
20
|
+
const OPTIONAL_BUNDLE_FILES = ["tsconfig.json"];
|
|
27
21
|
|
|
28
22
|
function readJson(path, readFileSyncImpl) {
|
|
29
23
|
return JSON.parse(readFileSyncImpl(path, "utf8"));
|
|
@@ -126,6 +120,16 @@ export async function createDevCloudDeployBundle({
|
|
|
126
120
|
if (!existsSyncImpl(functionsSource)) throw new Error(`Backend root dir not found: ${functionsSource}`);
|
|
127
121
|
|
|
128
122
|
const projectPkg = readJson(resolvePathImpl(cwd, "package.json"), readFileSyncImpl);
|
|
123
|
+
const lockfileSelection = detectDeployLockfile({
|
|
124
|
+
cwd,
|
|
125
|
+
packageManager: projectPkg.packageManager,
|
|
126
|
+
existsSyncImpl,
|
|
127
|
+
resolvePathImpl,
|
|
128
|
+
});
|
|
129
|
+
const lockfileWarning = formatDeployLockfileSelectionWarning(lockfileSelection);
|
|
130
|
+
if (lockfileWarning) warnImpl(lockfileWarning);
|
|
131
|
+
const projectConfigSelection = detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl });
|
|
132
|
+
warnProjectConfigSelectionOnce({ cwd, selection: projectConfigSelection, warnImpl });
|
|
129
133
|
const dependencyVersionResult = auditDependencyVersionsImpl(projectPkg);
|
|
130
134
|
const dependencyVersionMessage = formatDependencyVersionAuditError(dependencyVersionResult);
|
|
131
135
|
if (dependencyVersionMessage) throw new Error(dependencyVersionMessage);
|
|
@@ -173,6 +177,20 @@ export async function createDevCloudDeployBundle({
|
|
|
173
177
|
existsSyncImpl,
|
|
174
178
|
resolvePathImpl,
|
|
175
179
|
});
|
|
180
|
+
if (projectConfigSelection.selectedFile) {
|
|
181
|
+
copyFileSyncImpl(
|
|
182
|
+
resolvePathImpl(cwd, projectConfigSelection.selectedFile),
|
|
183
|
+
resolvePathImpl(stagingDir, projectConfigSelection.selectedFile),
|
|
184
|
+
);
|
|
185
|
+
filesToPack.push(projectConfigSelection.selectedFile);
|
|
186
|
+
}
|
|
187
|
+
if (lockfileSelection.selectedFile) {
|
|
188
|
+
copyFileSyncImpl(
|
|
189
|
+
resolvePathImpl(cwd, lockfileSelection.selectedFile),
|
|
190
|
+
resolvePathImpl(stagingDir, lockfileSelection.selectedFile),
|
|
191
|
+
);
|
|
192
|
+
filesToPack.push(lockfileSelection.selectedFile);
|
|
193
|
+
}
|
|
176
194
|
|
|
177
195
|
const migrationManifestStage = stageProjectMigrationManifest({
|
|
178
196
|
cwd: stagingDir,
|