gencow 0.1.232 → 0.1.234
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/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 +7 -0
- 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 +5 -1
- package/lib/deploy-runtime.mjs +2 -0
- package/lib/deployment-artifact-provenance.mjs +158 -0
- package/lib/dev-cloud-bundle.mjs +2 -0
- package/lib/dev-cloud-command.mjs +2 -1
- package/lib/migration-bundle-manifest.mjs +61 -1
- package/lib/migration-bundle-roundtrip.mjs +9 -1
- package/lib/migration-bundle-staging.mjs +8 -1
- package/lib/project-migration-manifest.mjs +8 -1
- package/package.json +2 -2
- package/runtime/server.mjs +1679 -1093
- package/runtime/tooling.mjs +11 -3
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,
|
|
@@ -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,
|
|
@@ -484,6 +485,12 @@ function createDbMigrationCommand(deps, command) {
|
|
|
484
485
|
"X-Gencow-Environment": envLabel,
|
|
485
486
|
"X-Gencow-Migration-Operation-ID": receiptId,
|
|
486
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
|
+
: {}),
|
|
487
494
|
},
|
|
488
495
|
body: bundleData,
|
|
489
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
|
}
|
|
@@ -113,6 +113,7 @@ export function createDeployPackageRuntime({
|
|
|
113
113
|
buildEnvImpl,
|
|
114
114
|
buildDeployClaimFromArchiveImpl = buildDeployClaimFromArchive,
|
|
115
115
|
cliVersion = "0.0.0",
|
|
116
|
+
sourceRevision,
|
|
116
117
|
cwdImpl = () => process.cwd(),
|
|
117
118
|
drizzleKitCmdImpl,
|
|
118
119
|
errorImpl = error,
|
|
@@ -213,6 +214,8 @@ export function createDeployPackageRuntime({
|
|
|
213
214
|
rmSyncImpl,
|
|
214
215
|
runCodegenForDeployImpl,
|
|
215
216
|
successImpl,
|
|
217
|
+
cliVersion,
|
|
218
|
+
sourceRevision,
|
|
216
219
|
});
|
|
217
220
|
}
|
|
218
221
|
if (
|
|
@@ -462,6 +465,7 @@ export function createDeployPackageRuntime({
|
|
|
462
465
|
try {
|
|
463
466
|
({ bundleBackendRoot } = await writeCanonicalDeployBundle({
|
|
464
467
|
cliVersion,
|
|
468
|
+
sourceRevision,
|
|
465
469
|
cwd,
|
|
466
470
|
backendRoot,
|
|
467
471
|
dependencyAuditManifest,
|
|
@@ -633,7 +637,7 @@ export function createDeployPackageRuntime({
|
|
|
633
637
|
...(activeDeployClaim
|
|
634
638
|
? {
|
|
635
639
|
"X-Gencow-Deploy-Claim": activeDeployClaim.header,
|
|
636
|
-
"X-Gencow-Deploy-Protocol":
|
|
640
|
+
"X-Gencow-Deploy-Protocol": String(activeDeployClaim.claim.protocolVersion ?? 1),
|
|
637
641
|
"X-Gencow-CLI-Version": activeDeployClaim.claim.generatedBy.version,
|
|
638
642
|
}
|
|
639
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,158 @@
|
|
|
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 codegenArtifact = canonicalizeCodegenArtifactProvenanceV1(input?.codegen);
|
|
144
|
+
const codegen = normalizeProvenance(
|
|
145
|
+
{
|
|
146
|
+
cliVersion: codegenArtifact.cliVersion,
|
|
147
|
+
sourceRevision: codegenArtifact.sourceRevision,
|
|
148
|
+
},
|
|
149
|
+
"codegen",
|
|
150
|
+
);
|
|
151
|
+
const packager = normalizeProvenance(input?.packager, "packager");
|
|
152
|
+
if (codegen.cliVersion !== packager.cliVersion || codegen.sourceRevision !== packager.sourceRevision) {
|
|
153
|
+
const error = new Error("PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH");
|
|
154
|
+
error.code = "PLATFORM_MIGRATION_ARTIFACT_CONTRACT_MISMATCH";
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return packager;
|
|
158
|
+
}
|
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,
|
|
@@ -115,7 +115,7 @@ export async function buildDevCloudDeployProtocolHeaders({
|
|
|
115
115
|
});
|
|
116
116
|
return {
|
|
117
117
|
"X-Gencow-Deploy-Claim": deployClaim.header,
|
|
118
|
-
"X-Gencow-Deploy-Protocol":
|
|
118
|
+
"X-Gencow-Deploy-Protocol": String(deployClaim.claim.protocolVersion ?? 1),
|
|
119
119
|
"X-Gencow-CLI-Version": version,
|
|
120
120
|
};
|
|
121
121
|
}
|
|
@@ -277,6 +277,7 @@ export function createDevCloudCommand(deps) {
|
|
|
277
277
|
rootDir: functionsDir,
|
|
278
278
|
envFile: config.envFile,
|
|
279
279
|
cliVersion: deps.cliVersion ?? "0.0.0",
|
|
280
|
+
sourceRevision: deps.sourceRevision,
|
|
280
281
|
generatorVersion: migrationGeneratorVersion,
|
|
281
282
|
config,
|
|
282
283
|
existsSyncImpl: deps.existsSyncImpl,
|