gencow 0.1.231 → 0.1.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/gencow.mjs +13 -1
- package/core/index.js +1 -0
- package/lib/backend-bundle-file-plan.mjs +14 -1
- package/lib/cli-dev-runtime.mjs +4 -0
- package/lib/codegen-command.mjs +12 -0
- package/lib/db-command.mjs +2 -0
- package/lib/db-push-command.mjs +10 -1
- package/lib/deploy-bundle-staging.mjs +2 -0
- package/lib/deploy-codegen-preflight.mjs +4 -0
- package/lib/deploy-contract.mjs +60 -1
- package/lib/deploy-package-claim.mjs +35 -6
- package/lib/deploy-package-runtime.mjs +15 -2
- package/lib/deploy-runtime.mjs +2 -0
- package/lib/deployment-artifact-provenance.mjs +151 -0
- package/lib/dev-cloud-bundle.mjs +2 -0
- package/lib/dev-cloud-command.mjs +39 -17
- package/lib/migration-bundle-manifest.mjs +101 -1
- package/lib/migration-bundle-roundtrip.mjs +4 -0
- package/lib/migration-bundle-staging.mjs +8 -1
- package/lib/migration-client-diagnostic.mjs +6 -0
- package/lib/project-migration-manifest.mjs +8 -1
- package/lib/tenant-capability-compat.mjs +49 -0
- package/package.json +4 -4
- package/runtime/server.mjs +3283 -2394
- package/runtime/tooling.mjs +1 -0
- package/server/index.js.map +0 -7
package/bin/gencow.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
updateEnvLocalUrl,
|
|
39
39
|
} from "../lib/cli-project-runtime.mjs";
|
|
40
40
|
import { createCodegenCommand } from "../lib/codegen-command.mjs";
|
|
41
|
+
import { resolveCliArtifactSourceRevision } from "../lib/deployment-artifact-provenance.mjs";
|
|
41
42
|
import { runCliCommand } from "../lib/cli-command-runner.mjs";
|
|
42
43
|
import { createDoctorCommand } from "../lib/doctor-command.mjs";
|
|
43
44
|
import { maybeCheckCliVersion } from "../lib/cli-version-check.mjs";
|
|
@@ -89,8 +90,12 @@ import { failReleaseAttempt, finalizeReleaseAttempt, prepareReleaseAttempt } fro
|
|
|
89
90
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
90
91
|
const CLI_PACKAGE_JSON = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8"));
|
|
91
92
|
const CLI_VERSION = CLI_PACKAGE_JSON.version || "0.0.0";
|
|
93
|
+
const CLI_SOURCE_REVISION = resolveCliArtifactSourceRevision(CLI_VERSION);
|
|
92
94
|
const _drizzleKitCmd = (subcmd, options = {}) => buildDrizzleKitCommand(subcmd, options);
|
|
93
|
-
const generateApiTs = createApiCodegenRuntime({
|
|
95
|
+
const generateApiTs = createApiCodegenRuntime({
|
|
96
|
+
cliVersion: CLI_VERSION,
|
|
97
|
+
sourceRevision: CLI_SOURCE_REVISION,
|
|
98
|
+
});
|
|
94
99
|
const runAddCommand = createAddCommand({
|
|
95
100
|
loadConfig,
|
|
96
101
|
updateComponentReadmeImpl: updateComponentReadme,
|
|
@@ -113,6 +118,8 @@ const runAppCommand = createAppCommand({
|
|
|
113
118
|
const runBackupCommand = createBackupCommand();
|
|
114
119
|
const { login: runLoginCommand, logout: runLogoutCommand, whoami: runWhoamiCommand } = createAuthCommands();
|
|
115
120
|
const runCodegenCommand = createCodegenCommand({
|
|
121
|
+
cliVersion: CLI_VERSION,
|
|
122
|
+
sourceRevision: CLI_SOURCE_REVISION,
|
|
116
123
|
loadConfig,
|
|
117
124
|
findServerRoot,
|
|
118
125
|
resolveCoreImport,
|
|
@@ -159,6 +166,7 @@ const {
|
|
|
159
166
|
dbStudio: runDbStudioCommand,
|
|
160
167
|
} = createDbCommands({
|
|
161
168
|
cliVersion: CLI_VERSION,
|
|
169
|
+
sourceRevision: CLI_SOURCE_REVISION,
|
|
162
170
|
buildEnv,
|
|
163
171
|
cleanupOldBackups,
|
|
164
172
|
drizzleKitCmdImpl: _drizzleKitCmd,
|
|
@@ -169,6 +177,7 @@ const {
|
|
|
169
177
|
});
|
|
170
178
|
const runDevCloudCommand = createDevCloudCommand({
|
|
171
179
|
cliVersion: CLI_VERSION,
|
|
180
|
+
sourceRevision: CLI_SOURCE_REVISION,
|
|
172
181
|
buildEnvImpl: buildEnv,
|
|
173
182
|
requireCreds,
|
|
174
183
|
loadConfig,
|
|
@@ -246,6 +255,7 @@ const runStaticDeployRuntime = createStaticDeployRuntime({
|
|
|
246
255
|
const runDeployCommand = createDeployCommand({
|
|
247
256
|
buildEnvImpl: buildEnv,
|
|
248
257
|
cliVersion: CLI_VERSION,
|
|
258
|
+
sourceRevision: CLI_SOURCE_REVISION,
|
|
249
259
|
cwdImpl: () => process.cwd(),
|
|
250
260
|
drizzleKitCmdImpl: _drizzleKitCmd,
|
|
251
261
|
existsSyncImpl: existsSync,
|
|
@@ -280,6 +290,8 @@ const runInitCommand = createInitCommand({
|
|
|
280
290
|
templateFeatureDir: resolve(__dirname, "..", "templateFeature"),
|
|
281
291
|
runCodegenImpl: async (projectDir) => {
|
|
282
292
|
const runProjectCodegen = createCodegenCommand({
|
|
293
|
+
cliVersion: CLI_VERSION,
|
|
294
|
+
sourceRevision: CLI_SOURCE_REVISION,
|
|
283
295
|
loadConfig: (opts = {}) => loadConfig({ ...opts, cwd: projectDir }),
|
|
284
296
|
findServerRoot,
|
|
285
297
|
resolveCoreImport,
|
package/core/index.js
CHANGED
|
@@ -3016,6 +3016,7 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
|
|
|
3016
3016
|
"GENCOW_PLATFORM_DB",
|
|
3017
3017
|
"GENCOW_MANAGED_PUBLIC_ORIGINS",
|
|
3018
3018
|
"PLATFORM_OPENAI_KEY",
|
|
3019
|
+
"PLATFORM_OPENROUTER_KEY",
|
|
3019
3020
|
"PLATFORM_GOOGLE_KEY",
|
|
3020
3021
|
"PLATFORM_GOOGLE_CLIENT_ID",
|
|
3021
3022
|
"PLATFORM_GOOGLE_CLIENT_SECRET",
|
|
@@ -25,7 +25,7 @@ function collectFiles(backendRoot, archiveRoot, excludes, directory = backendRoo
|
|
|
25
25
|
throw new Error(`Bundle entry ${JSON.stringify(entry)} must be a regular file`);
|
|
26
26
|
}
|
|
27
27
|
const relativePath = canonicalPath(relative(backendRoot, path));
|
|
28
|
-
const archivePath = canonicalPath(`${archiveRoot}/${relativePath}`);
|
|
28
|
+
const archivePath = canonicalPath(archiveRoot ? `${archiveRoot}/${relativePath}` : relativePath);
|
|
29
29
|
if (!shouldIncludeTarEntry(archivePath, excludes)) continue;
|
|
30
30
|
const bytes = readFileSync(path);
|
|
31
31
|
files.push({ relativePath, archivePath, size: bytes.length, sha256: sha256(bytes) });
|
|
@@ -33,6 +33,19 @@ function collectFiles(backendRoot, archiveRoot, excludes, directory = backendRoo
|
|
|
33
33
|
return files;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function buildArchivePayloadFilePlan({
|
|
37
|
+
workspaceRoot,
|
|
38
|
+
excludes = [],
|
|
39
|
+
excludedPaths = [".gencow/migration-bundle-manifest.json"],
|
|
40
|
+
}) {
|
|
41
|
+
const files = collectFiles(workspaceRoot, "", excludes).filter(
|
|
42
|
+
(file) => !excludedPaths.includes(file.relativePath),
|
|
43
|
+
);
|
|
44
|
+
assertUniqueCanonicalPaths(files);
|
|
45
|
+
const rows = files.map((file) => [file.relativePath, file.size, file.sha256]);
|
|
46
|
+
return { rows, sha256: sha256(JSON.stringify(rows)) };
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
function assertUniqueCanonicalPaths(files) {
|
|
37
50
|
const exact = new Set();
|
|
38
51
|
const folded = new Set();
|
package/lib/cli-dev-runtime.mjs
CHANGED
|
@@ -146,6 +146,8 @@ export function createApiCodegenRuntime(options = {}) {
|
|
|
146
146
|
successImpl = success,
|
|
147
147
|
updateComponentReadmeImpl = updateComponentReadme,
|
|
148
148
|
emitAuthSchemaArtifactImpl,
|
|
149
|
+
cliVersion,
|
|
150
|
+
sourceRevision,
|
|
149
151
|
warnImpl = warn,
|
|
150
152
|
writeFileSyncImpl = writeFileSync,
|
|
151
153
|
} = options;
|
|
@@ -174,6 +176,8 @@ export function createApiCodegenRuntime(options = {}) {
|
|
|
174
176
|
resolvePathImpl,
|
|
175
177
|
serverPublishDir: resolveCodegenServerPublishDir(cwd, config),
|
|
176
178
|
successImpl,
|
|
179
|
+
cliVersion,
|
|
180
|
+
sourceRevision,
|
|
177
181
|
});
|
|
178
182
|
assertToolingRuntimeCapabilities(bundled, ["registryRuntime"], {
|
|
179
183
|
label: "tooling runtime",
|
package/lib/codegen-command.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveProjectSelection,
|
|
11
11
|
selectCodegenClient,
|
|
12
12
|
} from "./project-context.mjs";
|
|
13
|
+
import { writeCodegenProvenance } from "./deployment-artifact-provenance.mjs";
|
|
13
14
|
|
|
14
15
|
export { formatAuthSchemaConflictMessage } from "./auth-schema-provenance-guard.mjs";
|
|
15
16
|
|
|
@@ -193,6 +194,8 @@ export async function runCodegenWithReporting({
|
|
|
193
194
|
resolvePathImpl = resolve,
|
|
194
195
|
serverPublishDir,
|
|
195
196
|
successImpl,
|
|
197
|
+
cliVersion,
|
|
198
|
+
sourceRevision,
|
|
196
199
|
}) {
|
|
197
200
|
const bundled = await (loadCodegenBundleImpl ?? loadCodegenBundleDefault)();
|
|
198
201
|
|
|
@@ -223,6 +226,13 @@ export async function runCodegenWithReporting({
|
|
|
223
226
|
});
|
|
224
227
|
logCodegenDiagnostics(logImpl, result.diagnostics, diagnosticsColors);
|
|
225
228
|
assertCodegenSucceeded(result);
|
|
229
|
+
if (cliVersion && sourceRevision) {
|
|
230
|
+
writeCodegenProvenance({
|
|
231
|
+
cliVersion,
|
|
232
|
+
sourceRevision,
|
|
233
|
+
serverPublishDir: effectiveServerPublishDir,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
226
236
|
return { bundled, result };
|
|
227
237
|
}
|
|
228
238
|
|
|
@@ -347,6 +357,8 @@ export function createCodegenCommand(deps) {
|
|
|
347
357
|
publishDir,
|
|
348
358
|
resolvePathImpl: deps.resolvePathImpl,
|
|
349
359
|
successImpl: deps.successImpl,
|
|
360
|
+
cliVersion: deps.cliVersion,
|
|
361
|
+
sourceRevision: deps.sourceRevision,
|
|
350
362
|
});
|
|
351
363
|
deps.successImpl("Codegen completed successfully.");
|
|
352
364
|
} catch (error) {
|
package/lib/db-command.mjs
CHANGED
|
@@ -149,6 +149,7 @@ function renderCloudSeedMissingFile({ infoImpl = info, logImpl = log, warnImpl =
|
|
|
149
149
|
|
|
150
150
|
export function createDbCommands({
|
|
151
151
|
cliVersion = "0.0.0",
|
|
152
|
+
sourceRevision,
|
|
152
153
|
buildEnv,
|
|
153
154
|
cleanupOldBackups,
|
|
154
155
|
createInterfaceImpl,
|
|
@@ -219,6 +220,7 @@ export function createDbCommands({
|
|
|
219
220
|
warnImpl,
|
|
220
221
|
writeFileSyncImpl,
|
|
221
222
|
cliVersion,
|
|
223
|
+
sourceRevision,
|
|
222
224
|
};
|
|
223
225
|
const runDbPush = createDbPushCommand(dbMigrationDependencies);
|
|
224
226
|
const runDbCheck = createDbCheckCommand(dbMigrationDependencies);
|
package/lib/db-push-command.mjs
CHANGED
|
@@ -426,6 +426,7 @@ function createDbMigrationCommand(deps, command) {
|
|
|
426
426
|
cliVersion: deps.cliVersion,
|
|
427
427
|
generatorVersion,
|
|
428
428
|
sourceMode,
|
|
429
|
+
sourceRevision: deps.sourceRevision,
|
|
429
430
|
schemaSourcePaths,
|
|
430
431
|
cpSyncImpl: deps.cpSyncImpl,
|
|
431
432
|
mkdirSyncImpl: deps.mkdirSyncImpl,
|
|
@@ -445,7 +446,9 @@ function createDbMigrationCommand(deps, command) {
|
|
|
445
446
|
emitFailure(
|
|
446
447
|
error?.code === "MIGRATION_BUNDLE_LOCAL_INTEGRITY_FAILED"
|
|
447
448
|
? buildLocalBundleIntegrityFailureDiagnostic(clientCorrelationId())
|
|
448
|
-
:
|
|
449
|
+
: error?.code === "MIGRATION_BUNDLE_SCHEMA_CHANGE_WITHOUT_FORWARD_MIGRATION"
|
|
450
|
+
? buildClientUserFixableDiagnostic(error.code, clientCorrelationId())
|
|
451
|
+
: buildClientBundleFailureDiagnostic(clientCorrelationId(), retryCommand),
|
|
449
452
|
);
|
|
450
453
|
return;
|
|
451
454
|
}
|
|
@@ -482,6 +485,12 @@ function createDbMigrationCommand(deps, command) {
|
|
|
482
485
|
"X-Gencow-Environment": envLabel,
|
|
483
486
|
"X-Gencow-Migration-Operation-ID": receiptId,
|
|
484
487
|
"X-Gencow-Migration-Bundle-Sha256": stagedBundle.manifest.bundleSha256,
|
|
488
|
+
...(stagedBundle.manifest.artifactIdentitySha256
|
|
489
|
+
? {
|
|
490
|
+
"X-Gencow-Migration-Artifact-Identity-Sha256":
|
|
491
|
+
stagedBundle.manifest.artifactIdentitySha256,
|
|
492
|
+
}
|
|
493
|
+
: {}),
|
|
485
494
|
},
|
|
486
495
|
body: bundleData,
|
|
487
496
|
},
|
|
@@ -13,6 +13,7 @@ export const CANONICAL_BUNDLE_BACKEND_ROOT = "gencow";
|
|
|
13
13
|
|
|
14
14
|
export async function writeCanonicalDeployBundle({
|
|
15
15
|
cliVersion,
|
|
16
|
+
sourceRevision,
|
|
16
17
|
cwd,
|
|
17
18
|
backendRoot,
|
|
18
19
|
dependencyAuditManifest,
|
|
@@ -122,6 +123,7 @@ export async function writeCanonicalDeployBundle({
|
|
|
122
123
|
config: canonicalManifestConfig ?? undefined,
|
|
123
124
|
backendExcludes: envExcludes,
|
|
124
125
|
cliVersion,
|
|
126
|
+
sourceRevision,
|
|
125
127
|
generatorVersion,
|
|
126
128
|
existsSyncImpl,
|
|
127
129
|
mkdirSyncImpl,
|
|
@@ -63,6 +63,8 @@ export async function prepareDeployCodegenArtifacts({
|
|
|
63
63
|
rmSyncImpl,
|
|
64
64
|
runCodegenForDeployImpl = runCodegenWithReporting,
|
|
65
65
|
successImpl,
|
|
66
|
+
cliVersion,
|
|
67
|
+
sourceRevision,
|
|
66
68
|
}) {
|
|
67
69
|
const authSchemaPath = resolvePathImpl(cwd, backendRoot, "schema-auth.ts");
|
|
68
70
|
const serverPublishDir = resolveCodegenServerPublishDir(cwd, config);
|
|
@@ -109,6 +111,8 @@ export async function prepareDeployCodegenArtifacts({
|
|
|
109
111
|
publishDir: deployCodegenClientDir,
|
|
110
112
|
serverPublishDir: deployCodegenServerDir,
|
|
111
113
|
successImpl,
|
|
114
|
+
cliVersion,
|
|
115
|
+
sourceRevision,
|
|
112
116
|
});
|
|
113
117
|
} catch {
|
|
114
118
|
cleanup();
|
package/lib/deploy-contract.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2,
|
|
5
|
+
MIGRATION_BUNDLE_CAPABILITY_V1,
|
|
6
|
+
} from "@gencow/migration-contract";
|
|
4
7
|
|
|
5
8
|
export const DEPLOY_CLAIM_PROTOCOL_VERSION = 1;
|
|
9
|
+
export const DEPLOY_CLAIM_PROTOCOL_VERSION_V2 = 2;
|
|
6
10
|
const TOP_LEVEL_FIELDS = [
|
|
7
11
|
"protocolVersion",
|
|
8
12
|
"requestId",
|
|
@@ -139,10 +143,65 @@ export function canonicalizeDeployClaimV1(input) {
|
|
|
139
143
|
return claim;
|
|
140
144
|
}
|
|
141
145
|
|
|
146
|
+
export function canonicalizeDeployClaimV2(input) {
|
|
147
|
+
requireExactFields(input, TOP_LEVEL_FIELDS, "deploy claim");
|
|
148
|
+
if (input.protocolVersion !== DEPLOY_CLAIM_PROTOCOL_VERSION_V2) {
|
|
149
|
+
throw new Error("deploy claim protocolVersion is not supported");
|
|
150
|
+
}
|
|
151
|
+
const migrationBundle = requireObject(input.migrationBundle, "deploy claim migrationBundle");
|
|
152
|
+
requireExactFields(
|
|
153
|
+
migrationBundle,
|
|
154
|
+
["manifestSha256", "requiredCapability", "artifactIdentitySha256"],
|
|
155
|
+
"deploy claim migrationBundle",
|
|
156
|
+
);
|
|
157
|
+
const legacy = canonicalizeDeployClaimV1({
|
|
158
|
+
...input,
|
|
159
|
+
protocolVersion: DEPLOY_CLAIM_PROTOCOL_VERSION,
|
|
160
|
+
migrationBundle: {
|
|
161
|
+
manifestSha256: migrationBundle.manifestSha256,
|
|
162
|
+
requiredCapability:
|
|
163
|
+
migrationBundle.requiredCapability === MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2
|
|
164
|
+
? MIGRATION_BUNDLE_CAPABILITY_V1
|
|
165
|
+
: migrationBundle.requiredCapability,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const identitySha256 = requireSha256(
|
|
169
|
+
migrationBundle.artifactIdentitySha256,
|
|
170
|
+
"migration artifactIdentitySha256",
|
|
171
|
+
true,
|
|
172
|
+
);
|
|
173
|
+
if (
|
|
174
|
+
legacy.declaredRuntime !== "server" ||
|
|
175
|
+
identitySha256 === null ||
|
|
176
|
+
migrationBundle.requiredCapability !== MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2
|
|
177
|
+
) {
|
|
178
|
+
throw new Error("deploy claim artifact identity is inconsistent");
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
...legacy,
|
|
182
|
+
protocolVersion: DEPLOY_CLAIM_PROTOCOL_VERSION_V2,
|
|
183
|
+
migrationBundle: {
|
|
184
|
+
manifestSha256: legacy.migrationBundle.manifestSha256,
|
|
185
|
+
requiredCapability: identitySha256 ? MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2 : "none",
|
|
186
|
+
artifactIdentitySha256: identitySha256,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function canonicalizeDeployClaim(input) {
|
|
192
|
+
return input?.protocolVersion === DEPLOY_CLAIM_PROTOCOL_VERSION_V2
|
|
193
|
+
? canonicalizeDeployClaimV2(input)
|
|
194
|
+
: canonicalizeDeployClaimV1(input);
|
|
195
|
+
}
|
|
196
|
+
|
|
142
197
|
export function encodeDeployClaimV1(input) {
|
|
143
198
|
return Buffer.from(JSON.stringify(canonicalizeDeployClaimV1(input))).toString("base64url");
|
|
144
199
|
}
|
|
145
200
|
|
|
201
|
+
export function encodeDeployClaimV2(input) {
|
|
202
|
+
return Buffer.from(JSON.stringify(canonicalizeDeployClaimV2(input))).toString("base64url");
|
|
203
|
+
}
|
|
204
|
+
|
|
146
205
|
export function hashDeployClaimV1(input) {
|
|
147
206
|
return createHash("sha256")
|
|
148
207
|
.update(JSON.stringify(canonicalizeDeployClaimV1(input)))
|
|
@@ -3,9 +3,19 @@ import { Readable } from "node:stream";
|
|
|
3
3
|
import { pipeline } from "node:stream/promises";
|
|
4
4
|
import { list } from "tar";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2,
|
|
8
|
+
MIGRATION_BUNDLE_CAPABILITY_V1,
|
|
9
|
+
inspectPnpmDependencyLock,
|
|
10
|
+
} from "@gencow/migration-contract";
|
|
7
11
|
|
|
8
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
canonicalizeDeployClaimV1,
|
|
14
|
+
canonicalizeDeployClaimV2,
|
|
15
|
+
createDeployRequestId,
|
|
16
|
+
encodeDeployClaimV1,
|
|
17
|
+
encodeDeployClaimV2,
|
|
18
|
+
} from "./deploy-contract.mjs";
|
|
9
19
|
import { DEPLOY_LOCKFILE_CANDIDATES, UNSUPPORTED_DEPLOY_LOCKFILES } from "./deploy-lockfile-selection.mjs";
|
|
10
20
|
|
|
11
21
|
const PACKAGE_MANIFEST = "package.json";
|
|
@@ -139,8 +149,19 @@ export async function buildDeployClaimFromArchive({
|
|
|
139
149
|
assertDeployDependencyLock(packageBytes, lockfile ? selected.get(lockfile[0]) : null);
|
|
140
150
|
}
|
|
141
151
|
const migrationBytes = selected.get(MIGRATION_MANIFEST);
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
let artifactIdentitySha256 = null;
|
|
153
|
+
if (migrationBytes) {
|
|
154
|
+
try {
|
|
155
|
+
const manifest = JSON.parse(migrationBytes.toString("utf8"));
|
|
156
|
+
artifactIdentitySha256 = manifest.artifactIdentitySha256 ?? null;
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error("Migration manifest is not valid JSON");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const protocolVersion = artifactIdentitySha256 ? 2 : 1;
|
|
162
|
+
const canonicalize = protocolVersion === 2 ? canonicalizeDeployClaimV2 : canonicalizeDeployClaimV1;
|
|
163
|
+
const claim = canonicalize({
|
|
164
|
+
protocolVersion,
|
|
144
165
|
requestId,
|
|
145
166
|
sourceBundleSha256: sha256(bundleBuffer),
|
|
146
167
|
declaredRuntime,
|
|
@@ -152,9 +173,17 @@ export async function buildDeployClaimFromArchive({
|
|
|
152
173
|
},
|
|
153
174
|
migrationBundle: {
|
|
154
175
|
manifestSha256: migrationBytes ? sha256(migrationBytes) : null,
|
|
155
|
-
requiredCapability: migrationBytes
|
|
176
|
+
requiredCapability: migrationBytes
|
|
177
|
+
? artifactIdentitySha256
|
|
178
|
+
? MIGRATION_ARTIFACT_IDENTITY_CAPABILITY_V2
|
|
179
|
+
: MIGRATION_BUNDLE_CAPABILITY_V1
|
|
180
|
+
: "none",
|
|
181
|
+
...(protocolVersion === 2 ? { artifactIdentitySha256 } : {}),
|
|
156
182
|
},
|
|
157
183
|
generatedBy: { name: "gencow-cli", version: cliVersion },
|
|
158
184
|
});
|
|
159
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
claim,
|
|
187
|
+
header: protocolVersion === 2 ? encodeDeployClaimV2(claim) : encodeDeployClaimV1(claim),
|
|
188
|
+
};
|
|
160
189
|
}
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "./deploy-lockfile-selection.mjs";
|
|
43
43
|
import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
|
|
44
44
|
import { writeCanonicalDeployBundle } from "./deploy-bundle-staging.mjs";
|
|
45
|
+
import { buildClientUserFixableDiagnostic } from "./migration-client-diagnostic.mjs";
|
|
45
46
|
|
|
46
47
|
async function loadProjectConfigDefault(options) {
|
|
47
48
|
const { loadConfig } = await import("./cli-project-runtime.mjs");
|
|
@@ -112,6 +113,7 @@ export function createDeployPackageRuntime({
|
|
|
112
113
|
buildEnvImpl,
|
|
113
114
|
buildDeployClaimFromArchiveImpl = buildDeployClaimFromArchive,
|
|
114
115
|
cliVersion = "0.0.0",
|
|
116
|
+
sourceRevision,
|
|
115
117
|
cwdImpl = () => process.cwd(),
|
|
116
118
|
drizzleKitCmdImpl,
|
|
117
119
|
errorImpl = error,
|
|
@@ -212,6 +214,8 @@ export function createDeployPackageRuntime({
|
|
|
212
214
|
rmSyncImpl,
|
|
213
215
|
runCodegenForDeployImpl,
|
|
214
216
|
successImpl,
|
|
217
|
+
cliVersion,
|
|
218
|
+
sourceRevision,
|
|
215
219
|
});
|
|
216
220
|
}
|
|
217
221
|
if (
|
|
@@ -461,6 +465,7 @@ export function createDeployPackageRuntime({
|
|
|
461
465
|
try {
|
|
462
466
|
({ bundleBackendRoot } = await writeCanonicalDeployBundle({
|
|
463
467
|
cliVersion,
|
|
468
|
+
sourceRevision,
|
|
464
469
|
cwd,
|
|
465
470
|
backendRoot,
|
|
466
471
|
dependencyAuditManifest,
|
|
@@ -484,7 +489,15 @@ export function createDeployPackageRuntime({
|
|
|
484
489
|
writeFileSyncImpl,
|
|
485
490
|
}));
|
|
486
491
|
} catch (caught) {
|
|
487
|
-
|
|
492
|
+
if (caught?.code === "MIGRATION_BUNDLE_SCHEMA_CHANGE_WITHOUT_FORWARD_MIGRATION") {
|
|
493
|
+
for (const line of renderMigrationDiagnosticLines(
|
|
494
|
+
buildClientUserFixableDiagnostic(caught.code, `client-${Date.now()}`),
|
|
495
|
+
)) {
|
|
496
|
+
errorImpl(line);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
errorImpl(`Packaging failed: ${caught.message}`);
|
|
500
|
+
}
|
|
488
501
|
exitImpl(1);
|
|
489
502
|
cleanupPreparedDeploySource();
|
|
490
503
|
return null;
|
|
@@ -624,7 +637,7 @@ export function createDeployPackageRuntime({
|
|
|
624
637
|
...(activeDeployClaim
|
|
625
638
|
? {
|
|
626
639
|
"X-Gencow-Deploy-Claim": activeDeployClaim.header,
|
|
627
|
-
"X-Gencow-Deploy-Protocol":
|
|
640
|
+
"X-Gencow-Deploy-Protocol": String(activeDeployClaim.claim.protocolVersion ?? 1),
|
|
628
641
|
"X-Gencow-CLI-Version": activeDeployClaim.claim.generatedBy.version,
|
|
629
642
|
}
|
|
630
643
|
: {}),
|
package/lib/deploy-runtime.mjs
CHANGED
|
@@ -352,6 +352,7 @@ async function runRollbackDeploy({
|
|
|
352
352
|
export function createDeployCommand({
|
|
353
353
|
buildEnvImpl,
|
|
354
354
|
cliVersion,
|
|
355
|
+
sourceRevision,
|
|
355
356
|
createDeployPackageRuntimeImpl = createDeployPackageRuntime,
|
|
356
357
|
createExistingBundleCandidateImpl = createExistingBundleCandidate,
|
|
357
358
|
createInterfaceImpl,
|
|
@@ -394,6 +395,7 @@ export function createDeployCommand({
|
|
|
394
395
|
const packageRuntimeOptions = {
|
|
395
396
|
buildEnvImpl,
|
|
396
397
|
cliVersion,
|
|
398
|
+
sourceRevision,
|
|
397
399
|
createDeployPackageRuntimeImpl,
|
|
398
400
|
drizzleKitCmdImpl,
|
|
399
401
|
errorImpl,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
canonicalizeCodegenArtifactProvenanceV1,
|
|
8
|
+
compareCanonicalCodegenArtifactPaths,
|
|
9
|
+
hashGeneratedSchemaArtifacts,
|
|
10
|
+
} from "@gencow/migration-contract/artifact-identity";
|
|
11
|
+
|
|
12
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
13
|
+
const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
14
|
+
export const CODEGEN_PROVENANCE_FILENAME = ".gencow-codegen-provenance.json";
|
|
15
|
+
const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
const CLI_IDENTITY_FILES = [
|
|
17
|
+
"package.json",
|
|
18
|
+
"bin/gencow.mjs",
|
|
19
|
+
"lib/codegen-command.mjs",
|
|
20
|
+
"lib/migration-bundle-manifest.mjs",
|
|
21
|
+
"lib/backend-bundle-file-plan.mjs",
|
|
22
|
+
"lib/deployment-artifact-provenance.mjs",
|
|
23
|
+
"runtime/tooling.mjs",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function normalizeProvenance(value, label) {
|
|
27
|
+
if (
|
|
28
|
+
!value ||
|
|
29
|
+
typeof value !== "object" ||
|
|
30
|
+
Array.isArray(value) ||
|
|
31
|
+
Object.keys(value).length !== 2 ||
|
|
32
|
+
typeof value.cliVersion !== "string" ||
|
|
33
|
+
!VERSION.test(value.cliVersion) ||
|
|
34
|
+
typeof value.sourceRevision !== "string" ||
|
|
35
|
+
!SHA256.test(value.sourceRevision)
|
|
36
|
+
) {
|
|
37
|
+
throw new Error(`${label} artifact provenance is invalid`);
|
|
38
|
+
}
|
|
39
|
+
return { cliVersion: value.cliVersion, sourceRevision: value.sourceRevision };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function deriveCliSourceRevision(cliVersion, sourceFiles = []) {
|
|
43
|
+
if (typeof cliVersion !== "string" || !VERSION.test(cliVersion)) {
|
|
44
|
+
throw new Error("CLI version is invalid");
|
|
45
|
+
}
|
|
46
|
+
const rows = sourceFiles.map(({ path, bytes }) => [
|
|
47
|
+
String(path).replaceAll("\\", "/").normalize("NFC"),
|
|
48
|
+
Buffer.byteLength(bytes),
|
|
49
|
+
createHash("sha256").update(bytes).digest("hex"),
|
|
50
|
+
]);
|
|
51
|
+
rows.sort((left, right) => left[0].localeCompare(right[0]));
|
|
52
|
+
return createHash("sha256").update(JSON.stringify({ cliVersion, rows })).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveCliArtifactSourceRevision(cliVersion, options = {}) {
|
|
56
|
+
const root = options.cliRoot ?? CLI_ROOT;
|
|
57
|
+
const read = options.readFileSyncImpl ?? readFileSync;
|
|
58
|
+
return deriveCliSourceRevision(
|
|
59
|
+
cliVersion,
|
|
60
|
+
CLI_IDENTITY_FILES.map((path) => ({ path, bytes: read(resolve(root, path)) })),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectGeneratedArtifacts(root, directory = root) {
|
|
65
|
+
const rows = [];
|
|
66
|
+
for (const entry of readdirSync(directory).sort()) {
|
|
67
|
+
if (entry === CODEGEN_PROVENANCE_FILENAME) continue;
|
|
68
|
+
const path = resolve(directory, entry);
|
|
69
|
+
const stats = lstatSync(path);
|
|
70
|
+
if (stats.isDirectory()) {
|
|
71
|
+
rows.push(...collectGeneratedArtifacts(root, path));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
75
|
+
throw new Error("Generated schema artifact must be a regular file");
|
|
76
|
+
}
|
|
77
|
+
const bytes = readFileSync(path);
|
|
78
|
+
rows.push({
|
|
79
|
+
path: relative(root, path).replaceAll("\\", "/").normalize("NFC"),
|
|
80
|
+
size: bytes.length,
|
|
81
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return rows.sort((left, right) => compareCanonicalCodegenArtifactPaths(left.path, right.path));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildCodegenProvenance({ cliVersion, sourceRevision, serverPublishDir }) {
|
|
88
|
+
const producer = normalizeProvenance({ cliVersion, sourceRevision }, "codegen");
|
|
89
|
+
const artifacts = collectGeneratedArtifacts(serverPublishDir);
|
|
90
|
+
return canonicalizeCodegenArtifactProvenanceV1({
|
|
91
|
+
version: 1,
|
|
92
|
+
...producer,
|
|
93
|
+
generatedSchemaDigest: hashGeneratedSchemaArtifacts(artifacts),
|
|
94
|
+
artifacts,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function writeCodegenProvenance({ cliVersion, sourceRevision, serverPublishDir }) {
|
|
99
|
+
const provenance = buildCodegenProvenance({ cliVersion, sourceRevision, serverPublishDir });
|
|
100
|
+
const markerPath = resolve(serverPublishDir, CODEGEN_PROVENANCE_FILENAME);
|
|
101
|
+
const temporaryPath = resolve(
|
|
102
|
+
dirname(serverPublishDir),
|
|
103
|
+
`.${basename(serverPublishDir)}-${CODEGEN_PROVENANCE_FILENAME}.tmp-${process.pid}-${randomUUID()}`,
|
|
104
|
+
);
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(temporaryPath, `${JSON.stringify(provenance)}\n`, { mode: 0o600, flag: "wx" });
|
|
107
|
+
renameSync(temporaryPath, markerPath);
|
|
108
|
+
} finally {
|
|
109
|
+
rmSync(temporaryPath, { force: true });
|
|
110
|
+
}
|
|
111
|
+
return provenance;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function readCodegenProvenance(path) {
|
|
115
|
+
if (!existsSync(path)) return null;
|
|
116
|
+
const stats = lstatSync(path);
|
|
117
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
118
|
+
throw new Error("codegen artifact provenance is invalid");
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return canonicalizeCodegenArtifactProvenanceV1(JSON.parse(readFileSync(path, "utf8")));
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error("codegen artifact provenance is invalid");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function verifyCodegenProvenance(path) {
|
|
128
|
+
const provenance = readCodegenProvenance(path);
|
|
129
|
+
if (!provenance) return null;
|
|
130
|
+
const actual = collectGeneratedArtifacts(dirname(path));
|
|
131
|
+
if (
|
|
132
|
+
JSON.stringify(actual) !== JSON.stringify(provenance.artifacts) ||
|
|
133
|
+
hashGeneratedSchemaArtifacts(actual) !== provenance.generatedSchemaDigest
|
|
134
|
+
) {
|
|
135
|
+
const error = new Error("PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH");
|
|
136
|
+
error.code = "PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH";
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
return provenance;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function assertMatchingCodegenAndPackagerProvenance(input) {
|
|
143
|
+
const codegen = normalizeProvenance(input?.codegen, "codegen");
|
|
144
|
+
const packager = normalizeProvenance(input?.packager, "packager");
|
|
145
|
+
if (codegen.cliVersion !== packager.cliVersion || codegen.sourceRevision !== packager.sourceRevision) {
|
|
146
|
+
const error = new Error("PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH");
|
|
147
|
+
error.code = "PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH";
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
return packager;
|
|
151
|
+
}
|
package/lib/dev-cloud-bundle.mjs
CHANGED
|
@@ -96,6 +96,7 @@ export async function createDevCloudDeployBundle({
|
|
|
96
96
|
envFile,
|
|
97
97
|
functionsDir,
|
|
98
98
|
cliVersion = "0.0.0",
|
|
99
|
+
sourceRevision,
|
|
99
100
|
generatorVersion = "unknown",
|
|
100
101
|
config,
|
|
101
102
|
auditDependencyVersionsImpl = auditDependencyVersions,
|
|
@@ -232,6 +233,7 @@ export async function createDevCloudDeployBundle({
|
|
|
232
233
|
backendExcludes: envExcludes,
|
|
233
234
|
config: loadedProjectConfig ? canonicalConfig : undefined,
|
|
234
235
|
cliVersion,
|
|
236
|
+
sourceRevision,
|
|
235
237
|
generatorVersion,
|
|
236
238
|
existsSyncImpl,
|
|
237
239
|
mkdirSyncImpl,
|