@hyperframes/gcp-cloud-run 0.7.70 → 0.7.72
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/Dockerfile +1 -0
- package/dist/events.d.ts +71 -11
- package/dist/events.d.ts.map +1 -1
- package/dist/gcsTransport.d.ts +12 -0
- package/dist/gcsTransport.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +338 -6
- package/dist/index.js.map +2 -2
- package/dist/sdk/index.js +10 -2
- package/dist/sdk/index.js.map +2 -2
- package/dist/sdk/renderToCloudRun.d.ts +6 -1
- package/dist/sdk/renderToCloudRun.d.ts.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +333 -5
- package/dist/server.js.map +2 -2
- package/package.json +4 -3
- package/terraform/main.tf +5 -0
- package/terraform/outputs.tf +10 -0
- package/terraform/smoke-safety.test.ts +71 -0
- package/terraform/variables.tf +9 -0
- package/terraform/workflow.test.ts +148 -0
- package/terraform/workflow.yaml +149 -10
package/dist/index.js
CHANGED
|
@@ -8,7 +8,11 @@ import { Storage } from "@google-cloud/storage";
|
|
|
8
8
|
import { Hono } from "hono";
|
|
9
9
|
import {
|
|
10
10
|
assemble,
|
|
11
|
+
listPlanV2ArtifactsForTarget,
|
|
12
|
+
materializePlanV2Target,
|
|
11
13
|
plan,
|
|
14
|
+
planV2,
|
|
15
|
+
readPlanV2Manifest,
|
|
12
16
|
renderChunk
|
|
13
17
|
} from "@hyperframes/producer/distributed";
|
|
14
18
|
|
|
@@ -74,7 +78,15 @@ function formatExtension(format) {
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
// src/gcsTransport.ts
|
|
77
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
createReadStream,
|
|
83
|
+
createWriteStream,
|
|
84
|
+
existsSync as existsSync2,
|
|
85
|
+
mkdirSync,
|
|
86
|
+
rmSync,
|
|
87
|
+
statSync
|
|
88
|
+
} from "node:fs";
|
|
89
|
+
import { createHash } from "node:crypto";
|
|
78
90
|
import { dirname } from "node:path";
|
|
79
91
|
import { pipeline } from "node:stream/promises";
|
|
80
92
|
import * as tar from "tar";
|
|
@@ -103,6 +115,19 @@ async function downloadGcsObjectToFile(storage, uri, destPath) {
|
|
|
103
115
|
const file = storage.bucket(bucket).file(key);
|
|
104
116
|
await pipeline(file.createReadStream(), createWriteStream(destPath));
|
|
105
117
|
}
|
|
118
|
+
async function downloadGcsObjectToFileVerified(storage, uri, destPath, expectedSha256) {
|
|
119
|
+
assertSha256(expectedSha256);
|
|
120
|
+
await downloadGcsObjectToFile(storage, uri, destPath);
|
|
121
|
+
const actual = await sha256File(destPath);
|
|
122
|
+
if (actual !== expectedSha256) {
|
|
123
|
+
rmSync(destPath, { force: true });
|
|
124
|
+
const error = new Error(
|
|
125
|
+
`[gcsTransport] PLAN_ARTIFACT_DIGEST_MISMATCH: ${uri} expected ${expectedSha256}, got ${actual}`
|
|
126
|
+
);
|
|
127
|
+
error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
106
131
|
async function uploadFileToGcs(storage, localPath, uri, contentType) {
|
|
107
132
|
if (!existsSync2(localPath)) {
|
|
108
133
|
throw new Error(`[gcsTransport] upload source missing: ${localPath}`);
|
|
@@ -117,6 +142,73 @@ async function uploadFileToGcs(storage, localPath, uri, contentType) {
|
|
|
117
142
|
contentType
|
|
118
143
|
});
|
|
119
144
|
}
|
|
145
|
+
async function uploadContentAddressedFileToGcs(storage, localPath, uri, expectedSha256, contentType) {
|
|
146
|
+
assertSha256(expectedSha256);
|
|
147
|
+
if (!existsSync2(localPath)) {
|
|
148
|
+
throw new Error(`[gcsTransport] upload source missing: ${localPath}`);
|
|
149
|
+
}
|
|
150
|
+
const actualSha256 = await sha256File(localPath);
|
|
151
|
+
if (actualSha256 !== expectedSha256) {
|
|
152
|
+
throwDigestMismatch(
|
|
153
|
+
`local artifact ${localPath} expected ${expectedSha256}, got ${actualSha256}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const { bucket, key } = parseGcsUri(uri);
|
|
157
|
+
const bucketHandle = storage.bucket(bucket);
|
|
158
|
+
const file = bucketHandle.file(key);
|
|
159
|
+
const size = statSync(localPath).size;
|
|
160
|
+
if (await isReusableContentAddressedObject(file, uri, size, expectedSha256)) {
|
|
161
|
+
return "reused";
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
await bucketHandle.upload(localPath, {
|
|
165
|
+
destination: key,
|
|
166
|
+
contentType,
|
|
167
|
+
metadata: { metadata: { sha256: expectedSha256 } },
|
|
168
|
+
preconditionOpts: { ifGenerationMatch: 0 }
|
|
169
|
+
});
|
|
170
|
+
return "uploaded";
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (isGcsPreconditionFailed(error) && await isReusableContentAddressedObject(file, uri, size, expectedSha256)) {
|
|
173
|
+
return "reused";
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function isReusableContentAddressedObject(file, uri, expectedSize, expectedSha256) {
|
|
179
|
+
const [exists] = await file.exists();
|
|
180
|
+
if (!exists) return false;
|
|
181
|
+
const [metadata] = await file.getMetadata();
|
|
182
|
+
if (Number(metadata.size) === expectedSize && metadata.metadata?.sha256 === expectedSha256) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
throwDigestMismatch(
|
|
186
|
+
`immutable object ${uri} already exists with different digest metadata or size`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
async function sha256File(path) {
|
|
190
|
+
const hash = createHash("sha256");
|
|
191
|
+
for await (const chunk of createReadStream(path)) {
|
|
192
|
+
hash.update(chunk);
|
|
193
|
+
}
|
|
194
|
+
return hash.digest("hex");
|
|
195
|
+
}
|
|
196
|
+
function assertSha256(value) {
|
|
197
|
+
if (!/^[a-f0-9]{64}$/.test(value)) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`[gcsTransport] expected lowercase SHA-256 digest, got ${JSON.stringify(value)}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function throwDigestMismatch(detail) {
|
|
204
|
+
const error = new Error(`[gcsTransport] PLAN_ARTIFACT_DIGEST_MISMATCH: ${detail}`);
|
|
205
|
+
error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
function isGcsPreconditionFailed(error) {
|
|
209
|
+
if (!error || typeof error !== "object") return false;
|
|
210
|
+
return error.code === 412;
|
|
211
|
+
}
|
|
120
212
|
async function tarDirectory(sourceDir, destTarball) {
|
|
121
213
|
if (!existsSync2(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
122
214
|
throw new Error(`[gcsTransport] tar source must be an existing directory: ${sourceDir}`);
|
|
@@ -144,6 +236,7 @@ function getStorage() {
|
|
|
144
236
|
}
|
|
145
237
|
async function dispatch(event, deps) {
|
|
146
238
|
const unwrapped = unwrapEvent(event);
|
|
239
|
+
validatePlanProtocolShape(unwrapped);
|
|
147
240
|
validateEventGcsUris(unwrapped);
|
|
148
241
|
logEvent({ event: "handler_start", action: unwrapped.Action, input: summarizeEvent(unwrapped) });
|
|
149
242
|
try {
|
|
@@ -164,15 +257,52 @@ async function dispatch(event, deps) {
|
|
|
164
257
|
}
|
|
165
258
|
}
|
|
166
259
|
} catch (err) {
|
|
260
|
+
normalizeTerminalErrorName(err);
|
|
167
261
|
logEvent({
|
|
168
262
|
event: "handler_error",
|
|
169
263
|
action: unwrapped.Action,
|
|
264
|
+
input: summarizeEvent(unwrapped),
|
|
170
265
|
message: err instanceof Error ? err.message : String(err),
|
|
171
266
|
name: err instanceof Error ? err.name : void 0
|
|
172
267
|
});
|
|
173
268
|
throw err;
|
|
174
269
|
}
|
|
175
270
|
}
|
|
271
|
+
function validatePlanProtocolShape(event) {
|
|
272
|
+
const raw = event;
|
|
273
|
+
const protocol = raw.PlanProtocol;
|
|
274
|
+
if (protocol !== void 0 && protocol !== "v1" && protocol !== "v2") {
|
|
275
|
+
const error = new Error(
|
|
276
|
+
`[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected "v1", "v2", or absent`
|
|
277
|
+
);
|
|
278
|
+
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
if (event.Action === "plan") return;
|
|
282
|
+
const hasV1Locator = typeof raw.PlanGcsUri === "string";
|
|
283
|
+
const hasV2Manifest = typeof raw.PlanV2ManifestGcsUri === "string";
|
|
284
|
+
const hasV2Prefix = typeof raw.PlanV2ArtifactGcsPrefix === "string";
|
|
285
|
+
const valid = protocol === "v2" ? !hasV1Locator && hasV2Manifest && hasV2Prefix : hasV1Locator && !hasV2Manifest && !hasV2Prefix;
|
|
286
|
+
if (!valid) {
|
|
287
|
+
const error = new Error(
|
|
288
|
+
`[handler] ${protocol === "v2" ? "v2" : "v1"} ${event.Action} event has mixed or missing plan locators`
|
|
289
|
+
);
|
|
290
|
+
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
if (protocol === "v2" && event.Action === "assemble" && event.AudioGcsUri !== null) {
|
|
294
|
+
const error = new Error("[handler] v2 assemble audio must be materialized from the manifest");
|
|
295
|
+
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function normalizeTerminalErrorName(error) {
|
|
300
|
+
if (!error || typeof error !== "object") return;
|
|
301
|
+
const candidate = error;
|
|
302
|
+
if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE") {
|
|
303
|
+
candidate.name = candidate.code;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
176
306
|
var MAX_ENVELOPE_DEPTH = 4;
|
|
177
307
|
function unwrapEvent(event) {
|
|
178
308
|
let cursor = event;
|
|
@@ -209,18 +339,21 @@ function summarizeEvent(event) {
|
|
|
209
339
|
return {
|
|
210
340
|
projectGcsUri: event.ProjectGcsUri,
|
|
211
341
|
planOutputGcsPrefix: event.PlanOutputGcsPrefix,
|
|
342
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
212
343
|
format: event.Config.format,
|
|
213
344
|
fps: event.Config.fps
|
|
214
345
|
};
|
|
215
346
|
case "renderChunk":
|
|
216
347
|
return {
|
|
217
|
-
|
|
348
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
349
|
+
...event.PlanProtocol === "v2" ? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri } : { planGcsUri: event.PlanGcsUri },
|
|
218
350
|
chunkIndex: event.ChunkIndex,
|
|
219
351
|
format: event.Format
|
|
220
352
|
};
|
|
221
353
|
case "assemble":
|
|
222
354
|
return {
|
|
223
|
-
|
|
355
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
356
|
+
...event.PlanProtocol === "v2" ? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri } : { planGcsUri: event.PlanGcsUri },
|
|
224
357
|
chunkCount: event.ChunkGcsUris.length,
|
|
225
358
|
hasAudio: event.AudioGcsUri !== null,
|
|
226
359
|
outputGcsUri: event.OutputGcsUri,
|
|
@@ -234,6 +367,9 @@ function primeChrome(deps) {
|
|
|
234
367
|
process.env.PRODUCER_HEADLESS_SHELL_PATH = resolveChromeExecutablePath();
|
|
235
368
|
}
|
|
236
369
|
async function handlePlan(event, deps) {
|
|
370
|
+
if (event.PlanProtocol === "v2") {
|
|
371
|
+
return handlePlanV2(event, deps);
|
|
372
|
+
}
|
|
237
373
|
const started = Date.now();
|
|
238
374
|
const storage = deps?.storage ?? getStorage();
|
|
239
375
|
const primitive = deps?.primitives?.plan ?? plan;
|
|
@@ -275,7 +411,70 @@ async function handlePlan(event, deps) {
|
|
|
275
411
|
cleanupDir(work);
|
|
276
412
|
}
|
|
277
413
|
}
|
|
414
|
+
async function handlePlanV2(event, deps) {
|
|
415
|
+
const started = Date.now();
|
|
416
|
+
const storage = deps?.storage ?? getStorage();
|
|
417
|
+
const primitive = deps?.primitives?.planV2 ?? planV2;
|
|
418
|
+
primeChrome(deps);
|
|
419
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-plan-v2-"));
|
|
420
|
+
const projectArchive = join(work, "project.tar.gz");
|
|
421
|
+
const projectDir = join(work, "project");
|
|
422
|
+
const planV2Dir = join(work, "plan-v2");
|
|
423
|
+
try {
|
|
424
|
+
await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);
|
|
425
|
+
await untarDirectory(projectArchive, projectDir);
|
|
426
|
+
const result = await primitive(projectDir, { ...event.Config }, planV2Dir);
|
|
427
|
+
const manifest = readPlanV2Manifest(planV2Dir);
|
|
428
|
+
if (manifest.planHash !== result.planHash) {
|
|
429
|
+
throwPlanHashMismatch(result.planHash, manifest.planHash);
|
|
430
|
+
}
|
|
431
|
+
const outputPrefix = `${trimTrailingSlash(event.PlanOutputGcsPrefix)}/v2`;
|
|
432
|
+
const artifactPrefix = `${outputPrefix}/artifacts/sha256`;
|
|
433
|
+
const uniqueArtifacts = [
|
|
434
|
+
...new Map(manifest.artifacts.map((artifact) => [artifact.sha256, artifact])).values()
|
|
435
|
+
];
|
|
436
|
+
await mapConcurrent(uniqueArtifacts, 16, async (artifact) => {
|
|
437
|
+
await uploadContentAddressedFileToGcs(
|
|
438
|
+
storage,
|
|
439
|
+
planV2BlobPath(planV2Dir, artifact.sha256),
|
|
440
|
+
planV2BlobUri(artifactPrefix, artifact.sha256),
|
|
441
|
+
artifact.sha256
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
const manifestUri = `${outputPrefix}/manifest.json`;
|
|
445
|
+
await uploadContentAddressedFileToGcs(
|
|
446
|
+
storage,
|
|
447
|
+
result.manifestPath,
|
|
448
|
+
manifestUri,
|
|
449
|
+
await sha256File(result.manifestPath),
|
|
450
|
+
"application/json"
|
|
451
|
+
);
|
|
452
|
+
return {
|
|
453
|
+
Action: "plan",
|
|
454
|
+
PlanProtocol: "v2",
|
|
455
|
+
PlanV2ManifestGcsUri: manifestUri,
|
|
456
|
+
PlanV2ArtifactGcsPrefix: artifactPrefix,
|
|
457
|
+
PlanHash: result.planHash,
|
|
458
|
+
ChunkCount: result.chunkCount,
|
|
459
|
+
TotalFrames: result.totalFrames,
|
|
460
|
+
Fps: result.fps,
|
|
461
|
+
Width: result.width,
|
|
462
|
+
Height: result.height,
|
|
463
|
+
Format: result.format,
|
|
464
|
+
HasAudio: manifest.artifacts.some((artifact) => artifact.path === "audio.aac"),
|
|
465
|
+
AudioGcsUri: null,
|
|
466
|
+
FfmpegVersion: result.ffmpegVersion,
|
|
467
|
+
ProducerVersion: result.producerVersion,
|
|
468
|
+
DurationMs: Date.now() - started
|
|
469
|
+
};
|
|
470
|
+
} finally {
|
|
471
|
+
cleanupDir(work);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
278
474
|
async function handleRenderChunk(event, deps) {
|
|
475
|
+
if (event.PlanProtocol === "v2") {
|
|
476
|
+
return handleRenderChunkV2(event, deps);
|
|
477
|
+
}
|
|
279
478
|
const started = Date.now();
|
|
280
479
|
const storage = deps?.storage ?? getStorage();
|
|
281
480
|
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
@@ -310,6 +509,42 @@ async function handleRenderChunk(event, deps) {
|
|
|
310
509
|
cleanupDir(work);
|
|
311
510
|
}
|
|
312
511
|
}
|
|
512
|
+
async function handleRenderChunkV2(event, deps) {
|
|
513
|
+
const started = Date.now();
|
|
514
|
+
const storage = deps?.storage ?? getStorage();
|
|
515
|
+
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
516
|
+
primeChrome(deps);
|
|
517
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-chunk-v2-"));
|
|
518
|
+
try {
|
|
519
|
+
const planDir = await downloadAndMaterializePlanV2(
|
|
520
|
+
storage,
|
|
521
|
+
event,
|
|
522
|
+
{ role: "chunk", chunkIndex: event.ChunkIndex },
|
|
523
|
+
work
|
|
524
|
+
);
|
|
525
|
+
const chunkOutputBase = join(
|
|
526
|
+
work,
|
|
527
|
+
event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
|
|
528
|
+
);
|
|
529
|
+
const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
|
|
530
|
+
const chunkUri = await uploadChunkOutput(
|
|
531
|
+
storage,
|
|
532
|
+
result,
|
|
533
|
+
event.ChunkOutputGcsPrefix,
|
|
534
|
+
event.ChunkIndex
|
|
535
|
+
);
|
|
536
|
+
return {
|
|
537
|
+
Action: "renderChunk",
|
|
538
|
+
ChunkGcsUri: chunkUri,
|
|
539
|
+
ChunkIndex: event.ChunkIndex,
|
|
540
|
+
Sha256: result.sha256,
|
|
541
|
+
FramesEncoded: result.framesEncoded,
|
|
542
|
+
DurationMs: Date.now() - started
|
|
543
|
+
};
|
|
544
|
+
} finally {
|
|
545
|
+
cleanupDir(work);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
313
548
|
async function uploadChunkOutput(storage, result, prefix, chunkIndex) {
|
|
314
549
|
const trimmed = trimTrailingSlash(prefix);
|
|
315
550
|
if (result.outputKind === "file") {
|
|
@@ -325,6 +560,9 @@ async function uploadChunkOutput(storage, result, prefix, chunkIndex) {
|
|
|
325
560
|
return uri;
|
|
326
561
|
}
|
|
327
562
|
async function handleAssemble(event, deps) {
|
|
563
|
+
if (event.PlanProtocol === "v2") {
|
|
564
|
+
return handleAssembleV2(event, deps);
|
|
565
|
+
}
|
|
328
566
|
const started = Date.now();
|
|
329
567
|
const storage = deps?.storage ?? getStorage();
|
|
330
568
|
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
@@ -365,6 +603,91 @@ async function handleAssemble(event, deps) {
|
|
|
365
603
|
cleanupDir(work);
|
|
366
604
|
}
|
|
367
605
|
}
|
|
606
|
+
async function handleAssembleV2(event, deps) {
|
|
607
|
+
const started = Date.now();
|
|
608
|
+
const storage = deps?.storage ?? getStorage();
|
|
609
|
+
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
610
|
+
const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-assemble-v2-"));
|
|
611
|
+
try {
|
|
612
|
+
const planDir = await downloadAndMaterializePlanV2(storage, event, { role: "assembler" }, work);
|
|
613
|
+
const audioPath = existsSync3(join(planDir, "audio.aac")) ? join(planDir, "audio.aac") : null;
|
|
614
|
+
const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);
|
|
615
|
+
const finalOutput = event.Format === "png-sequence" ? join(work, "output-frames") : join(work, `output${formatExtension(event.Format)}`);
|
|
616
|
+
const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
|
|
617
|
+
cfr: event.Cfr === true
|
|
618
|
+
});
|
|
619
|
+
if (event.Format === "png-sequence") {
|
|
620
|
+
const tarball = `${finalOutput}.tar.gz`;
|
|
621
|
+
await tarDirectory(finalOutput, tarball);
|
|
622
|
+
await uploadFileToGcs(storage, tarball, event.OutputGcsUri, "application/gzip");
|
|
623
|
+
} else {
|
|
624
|
+
await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
Action: "assemble",
|
|
628
|
+
OutputGcsUri: event.OutputGcsUri,
|
|
629
|
+
FramesEncoded: result.framesEncoded,
|
|
630
|
+
FileSize: result.fileSize,
|
|
631
|
+
DurationMs: Date.now() - started
|
|
632
|
+
};
|
|
633
|
+
} finally {
|
|
634
|
+
cleanupDir(work);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function downloadAndMaterializePlanV2(storage, event, target, work) {
|
|
638
|
+
const transportDir = join(work, "plan-v2");
|
|
639
|
+
mkdirSync2(transportDir, { recursive: true });
|
|
640
|
+
await downloadGcsObjectToFile(
|
|
641
|
+
storage,
|
|
642
|
+
event.PlanV2ManifestGcsUri,
|
|
643
|
+
join(transportDir, "plan.json")
|
|
644
|
+
);
|
|
645
|
+
const manifest = readPlanV2Manifest(transportDir);
|
|
646
|
+
if (manifest.planHash !== event.PlanHash) {
|
|
647
|
+
throwPlanHashMismatch(event.PlanHash, manifest.planHash);
|
|
648
|
+
}
|
|
649
|
+
const artifacts = listPlanV2ArtifactsForTarget(manifest, target);
|
|
650
|
+
const uniqueArtifacts = [
|
|
651
|
+
...new Map(artifacts.map((artifact) => [artifact.sha256, artifact])).values()
|
|
652
|
+
];
|
|
653
|
+
await mapConcurrent(uniqueArtifacts, 16, async (artifact) => {
|
|
654
|
+
await downloadPlanV2Artifact(storage, event.PlanV2ArtifactGcsPrefix, transportDir, artifact);
|
|
655
|
+
});
|
|
656
|
+
const planDir = join(work, "plan");
|
|
657
|
+
materializePlanV2Target(transportDir, target, planDir);
|
|
658
|
+
return planDir;
|
|
659
|
+
}
|
|
660
|
+
async function downloadPlanV2Artifact(storage, artifactPrefix, planV2Dir, artifact) {
|
|
661
|
+
await downloadGcsObjectToFileVerified(
|
|
662
|
+
storage,
|
|
663
|
+
planV2BlobUri(artifactPrefix, artifact.sha256),
|
|
664
|
+
planV2BlobPath(planV2Dir, artifact.sha256),
|
|
665
|
+
artifact.sha256
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
function planV2BlobPath(planV2Dir, digest) {
|
|
669
|
+
return join(planV2Dir, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
670
|
+
}
|
|
671
|
+
function planV2BlobUri(prefix, digest) {
|
|
672
|
+
return `${trimTrailingSlash(prefix)}/${digest.slice(0, 2)}/${digest}`;
|
|
673
|
+
}
|
|
674
|
+
function throwPlanHashMismatch(expected, actual) {
|
|
675
|
+
const error = new Error(
|
|
676
|
+
`PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match v2 manifest planHash=${actual}`
|
|
677
|
+
);
|
|
678
|
+
error.name = "PLAN_HASH_MISMATCH";
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
async function mapConcurrent(values, concurrency, fn) {
|
|
682
|
+
let cursor = 0;
|
|
683
|
+
async function worker() {
|
|
684
|
+
while (cursor < values.length) {
|
|
685
|
+
const index = cursor++;
|
|
686
|
+
await fn(values[index]);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => worker()));
|
|
690
|
+
}
|
|
368
691
|
async function downloadChunkObjects(storage, uris, workDir, format) {
|
|
369
692
|
const chunksDir = join(workDir, "chunks");
|
|
370
693
|
mkdirSync2(chunksDir, { recursive: true });
|
|
@@ -393,10 +716,10 @@ function getEventGcsUris(event) {
|
|
|
393
716
|
case "plan":
|
|
394
717
|
return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];
|
|
395
718
|
case "renderChunk":
|
|
396
|
-
return [event.PlanGcsUri, event.ChunkOutputGcsPrefix];
|
|
719
|
+
return event.PlanProtocol === "v2" ? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix, event.ChunkOutputGcsPrefix] : [event.PlanGcsUri, event.ChunkOutputGcsPrefix];
|
|
397
720
|
case "assemble":
|
|
398
721
|
return [
|
|
399
|
-
event.PlanGcsUri,
|
|
722
|
+
...event.PlanProtocol === "v2" ? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix] : [event.PlanGcsUri],
|
|
400
723
|
...event.ChunkGcsUris,
|
|
401
724
|
event.OutputGcsUri,
|
|
402
725
|
event.AudioGcsUri
|
|
@@ -465,12 +788,17 @@ var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
|
465
788
|
// Handler-boundary guards.
|
|
466
789
|
"GCS_URI_NOT_ALLOWED",
|
|
467
790
|
"PLAN_HASH_MISMATCH",
|
|
791
|
+
"PLAN_ARTIFACT_DIGEST_MISMATCH",
|
|
792
|
+
"PLAN_PROTOCOL_UNSUPPORTED",
|
|
793
|
+
"PLAN_V2_INTEGRITY_UNRECOVERABLE",
|
|
468
794
|
// Producer error class names (`.name`) + their string code aliases — the
|
|
469
795
|
// class sets `.name` to the class name but wraps a `code`; cover both so a
|
|
470
796
|
// raw-code throw is caught too. Mirrors the AWS state machine's
|
|
471
797
|
// non-retryable list.
|
|
472
798
|
"FormatNotSupportedInDistributedError",
|
|
473
799
|
"PlanTooLargeError",
|
|
800
|
+
"PlanProtocolUnsupportedError",
|
|
801
|
+
"PlanV2IntegrityError",
|
|
474
802
|
"RenderChunkValidationError",
|
|
475
803
|
"FFMPEG_VERSION_MISMATCH",
|
|
476
804
|
"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED",
|
|
@@ -640,7 +968,8 @@ async function renderToCloudRun(opts) {
|
|
|
640
968
|
PlanOutputGcsPrefix: planOutputGcsPrefix,
|
|
641
969
|
OutputGcsUri: outputGcsUri,
|
|
642
970
|
ServiceUrl: opts.serviceUrl,
|
|
643
|
-
Config: opts.config
|
|
971
|
+
Config: opts.config,
|
|
972
|
+
PlanProtocol: opts.planProtocol ?? "v1"
|
|
644
973
|
};
|
|
645
974
|
validateWorkflowsInputSize(argument);
|
|
646
975
|
const executions = opts.executions ?? await defaultExecutionsClient();
|
|
@@ -847,16 +1176,19 @@ export {
|
|
|
847
1176
|
deploySite,
|
|
848
1177
|
dispatch,
|
|
849
1178
|
downloadGcsObjectToFile,
|
|
1179
|
+
downloadGcsObjectToFileVerified,
|
|
850
1180
|
formatGcsUri,
|
|
851
1181
|
getRenderProgress,
|
|
852
1182
|
getTerraformModuleDir,
|
|
853
1183
|
parseGcsUri,
|
|
854
1184
|
renderToCloudRun,
|
|
855
1185
|
resolveChromeExecutablePath,
|
|
1186
|
+
sha256File,
|
|
856
1187
|
startServer,
|
|
857
1188
|
tarDirectory,
|
|
858
1189
|
untarDirectory,
|
|
859
1190
|
unwrapEvent,
|
|
1191
|
+
uploadContentAddressedFileToGcs,
|
|
860
1192
|
uploadFileToGcs,
|
|
861
1193
|
validateDistributedRenderConfig
|
|
862
1194
|
};
|