mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
feat(cloud): default distributed plans to v2 (#3311)
* feat(cloud): default distributed plans to v2 * fix(cloud): address plan v2 review feedback * fix(examples): document explicit v2 samples
This commit is contained in:
@@ -70,7 +70,6 @@ const site = await deploySite({
|
||||
|
||||
const handle = await renderToLambda({
|
||||
siteHandle: site,
|
||||
planProtocol: "v2",
|
||||
bucketName: site.bucketName,
|
||||
stateMachineArn: "arn:aws:states:us-east-1:123456789012:stateMachine:hyperframes-render",
|
||||
config: {
|
||||
@@ -88,10 +87,17 @@ const progress = await getRenderProgress({ executionArn: handle.executionArn });
|
||||
console.log(progress.status, progress.overallProgress, progress.costs.displayCost);
|
||||
```
|
||||
|
||||
Plan v2 is recommended for new integrations because workers fetch
|
||||
manifest-selected content-addressed artifacts. For backwards compatibility,
|
||||
omitting `planProtocol` still selects v1; existing callers do not change
|
||||
behavior until they opt in.
|
||||
Plan v2 is the default because workers fetch manifest-selected
|
||||
content-addressed artifacts. The SDK sends explicit v2 when `planProtocol` is
|
||||
omitted. Deprecated v1 compatibility remains available by passing
|
||||
`planProtocol: "v1"`.
|
||||
|
||||
<Warning>
|
||||
For an existing installation, pause new renders and drain active Step
|
||||
Functions executions. Redeploy the Lambda handler and SAM/CDK state machine
|
||||
from the same package version before upgrading the application SDK. Older
|
||||
infrastructure may default omission to v1 or lack v2 support.
|
||||
</Warning>
|
||||
|
||||
`renderToLambda()` validates the distributed render config before starting the Step Functions execution, so invalid dimensions, formats, chunk sizes, or payload sizes fail synchronously.
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ import { getRenderProgress, renderToCloudRun } from "@hyperframes/gcp-cloud-run/
|
||||
|
||||
const handle = await renderToCloudRun({
|
||||
projectDir: "./my-composition",
|
||||
planProtocol: "v2",
|
||||
config: { fps: 30, width: 1920, height: 1080, format: "mp4" },
|
||||
bucketName: "hyperframes-render-my-project",
|
||||
projectId: "my-project",
|
||||
@@ -97,10 +96,17 @@ while (progress.status === "running") {
|
||||
console.log(progress.status, progress.outputFile, progress.costs.displayCost);
|
||||
```
|
||||
|
||||
Plan v2 is recommended for new integrations because workers fetch
|
||||
manifest-selected content-addressed artifacts. For backwards compatibility,
|
||||
omitting `planProtocol` still selects v1; existing callers do not change
|
||||
behavior until they opt in.
|
||||
Plan v2 is the default because workers fetch manifest-selected
|
||||
content-addressed artifacts. The SDK sends explicit v2 when `planProtocol` is
|
||||
omitted. Deprecated v1 compatibility remains available by passing
|
||||
`planProtocol: "v1"`.
|
||||
|
||||
<Warning>
|
||||
For an existing installation, pause new renders and drain active workflow
|
||||
executions. Redeploy the Cloud Run image and Cloud Workflows definition from
|
||||
the same package version before upgrading the application SDK. Older
|
||||
workflows may default omission to v1 or lack v2 support.
|
||||
</Warning>
|
||||
|
||||
Pass `projectDir` for one-shot uploads, or call `deploySite()` separately and reuse the returned site handle across many renders.
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ aws stepfunctions start-execution \
|
||||
"ProjectS3Uri": "s3://${RENDER_BUCKET}/projects/my-project.tar.gz",
|
||||
"PlanOutputS3Prefix": "s3://${RENDER_BUCKET}/renders/$(date +%s)/",
|
||||
"OutputS3Uri": "s3://${RENDER_BUCKET}/output.mp4",
|
||||
"PlanProtocol": "v2",
|
||||
"Config": {
|
||||
"fps": 30,
|
||||
"width": 1920,
|
||||
@@ -92,10 +91,20 @@ EOF
|
||||
|
||||
The Step Functions execution kicks off Plan, fans out RenderChunk via
|
||||
the Map state, and finally Assemble. Final mp4 lands at `OutputS3Uri`.
|
||||
Plan v2 is recommended for new integrations. `PlanProtocol` may be `"v1"` or
|
||||
`"v2"`; absent still defaults to v1 for backwards compatibility. V2 uses
|
||||
separate manifest and content-addressed artifact locators throughout the
|
||||
workflow and never places a v2 object in `PlanS3Uri`.
|
||||
Plan v2 is the default when `PlanProtocol` is absent. V2 uses separate
|
||||
manifest and content-addressed artifact locators throughout the workflow and
|
||||
never places a v2 object in `PlanS3Uri`. The deprecated v1 transport remains
|
||||
available by sending `"PlanProtocol": "v1"` explicitly.
|
||||
|
||||
### Upgrading an existing stack
|
||||
|
||||
Pause new renders and let active Step Functions executions drain before the
|
||||
upgrade. Redeploy the Lambda handler and this state machine (or the matching
|
||||
CDK construct) from the same package version before upgrading the application
|
||||
that calls `renderToLambda`. The new SDK sends explicit v2 by default, while
|
||||
older infrastructure may default omission to v1 or lack v2 support. Keep
|
||||
passing `planProtocol: "v1"` until the infrastructure redeploy completes if
|
||||
you need a staged migration.
|
||||
|
||||
## Local invocation
|
||||
|
||||
@@ -112,10 +121,13 @@ sam validate
|
||||
sam local invoke RenderFunction --event sample-events/plan.json
|
||||
```
|
||||
|
||||
The `sample-events/` directory ships small JSON payloads for each of the
|
||||
three actions. They reference fake S3 URIs — useful for sanity-checking
|
||||
the handler's dispatch logic; not for full end-to-end testing (real S3
|
||||
calls require credentials and a project zip to actually exist).
|
||||
The `sample-events/` directory ships three tiers for each action:
|
||||
`*.json` demonstrates default v2 with `PlanProtocol` omitted, `*-v1.json`
|
||||
demonstrates deprecated explicit-v1 compatibility, and `*-v2.json`
|
||||
demonstrates callers that stamp v2 explicitly. They reference fake S3 URIs —
|
||||
useful for sanity-checking the handler's dispatch logic; not for full
|
||||
end-to-end testing (real S3 calls require credentials and a project zip to
|
||||
actually exist).
|
||||
|
||||
## End-to-end smoke + benchmark
|
||||
|
||||
@@ -124,7 +136,7 @@ the architecture works on a deployed Lambda — use the local smoke
|
||||
script:
|
||||
|
||||
```bash
|
||||
# Defaults use the fixture's meta.json minPsnr (30 dB for mp4-h264-sdr).
|
||||
# Defaults use Plan v2 and the fixture's meta.json minPsnr (30 dB for mp4-h264-sdr).
|
||||
./scripts/smoke.sh
|
||||
|
||||
# Customised:
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Action": "assemble",
|
||||
"PlanProtocol": "v1",
|
||||
"PlanS3Uri": "s3://example-bucket/renders/sample/plan.tar.gz",
|
||||
"ChunkS3Uris": [
|
||||
"s3://example-bucket/renders/sample/chunks/0000.mp4",
|
||||
"s3://example-bucket/renders/sample/chunks/0001.mp4"
|
||||
],
|
||||
"AudioS3Uri": null,
|
||||
"OutputS3Uri": "s3://example-bucket/renders/sample/output.mp4",
|
||||
"Format": "mp4"
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"Action": "assemble",
|
||||
"PlanS3Uri": "s3://example-bucket/renders/sample/plan.tar.gz",
|
||||
"PlanV2ManifestS3Uri": "s3://example-bucket/renders/sample/v2/manifest.json",
|
||||
"PlanV2ArtifactS3Prefix": "s3://example-bucket/renders/sample/v2/artifacts/sha256",
|
||||
"PlanHash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"ChunkS3Uris": [
|
||||
"s3://example-bucket/renders/sample/chunks/0000.mp4",
|
||||
"s3://example-bucket/renders/sample/chunks/0001.mp4",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Action": "plan",
|
||||
"PlanProtocol": "v1",
|
||||
"ProjectS3Uri": "s3://example-bucket/projects/sample.tar.gz",
|
||||
"PlanOutputS3Prefix": "s3://example-bucket/renders/sample/",
|
||||
"Config": {
|
||||
"fps": 30,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"format": "mp4",
|
||||
"chunkSize": 240,
|
||||
"maxParallelChunks": 8,
|
||||
"runtimeCap": "lambda"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"Action": "plan",
|
||||
"ProjectS3Uri": "s3://example-bucket/projects/sample-composition.tar.gz",
|
||||
"ProjectS3Uri": "s3://example-bucket/projects/sample.tar.gz",
|
||||
"PlanOutputS3Prefix": "s3://example-bucket/renders/sample/",
|
||||
"Config": {
|
||||
"fps": 30,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Action": "renderChunk",
|
||||
"PlanProtocol": "v1",
|
||||
"PlanS3Uri": "s3://example-bucket/renders/sample/plan.tar.gz",
|
||||
"PlanHash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"ChunkIndex": 0,
|
||||
"ChunkOutputS3Prefix": "s3://example-bucket/renders/sample/",
|
||||
"Format": "mp4"
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Action": "renderChunk",
|
||||
"PlanS3Uri": "s3://example-bucket/renders/sample/plan.tar.gz",
|
||||
"PlanV2ManifestS3Uri": "s3://example-bucket/renders/sample/v2/manifest.json",
|
||||
"PlanV2ArtifactS3Prefix": "s3://example-bucket/renders/sample/v2/artifacts/sha256",
|
||||
"PlanHash": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"ChunkIndex": 0,
|
||||
"ChunkOutputS3Prefix": "s3://example-bucket/renders/sample/",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
# --region <region> (default: $AWS_REGION or us-east-1)
|
||||
# --profile <name> (default: $AWS_PROFILE, otherwise the AWS
|
||||
# default profile resolution chain)
|
||||
# --plan-protocol <v1|v2|both> (default: v1)
|
||||
# --plan-protocol <v1|v2|both> (default: v2)
|
||||
# --keep-stack (skip `sam delete` at the end)
|
||||
# --skip-build (skip the ZIP rebuild; use the existing one)
|
||||
#
|
||||
@@ -82,7 +82,7 @@ SMOKE_RUN_ID="${HYPERFRAMES_SMOKE_RUN_ID:-$(hf_new_smoke_run_id)}"
|
||||
STACK_NAME="${STACK_NAME:-hyperframes-lambda-smoke-${SMOKE_RUN_ID}}"
|
||||
AWS_REGION="${AWS_REGION:-us-east-1}"
|
||||
AWS_PROFILE="${AWS_PROFILE:-}"
|
||||
PLAN_PROTOCOL="${PLAN_PROTOCOL:-v1}"
|
||||
PLAN_PROTOCOL="${PLAN_PROTOCOL:-v2}"
|
||||
KEEP_STACK="false"
|
||||
SKIP_BUILD="false"
|
||||
REQUIRE_ENCODED_SHA_EQUAL="${REQUIRE_ENCODED_SHA_EQUAL:-false}"
|
||||
@@ -110,7 +110,7 @@ Flags:
|
||||
--stack-name <name> SAM stack name (default: hyperframes-lambda-smoke-<unique-run-id>)
|
||||
--region <region> AWS region (default: $AWS_REGION or us-east-1)
|
||||
--profile <name> AWS profile (default: $AWS_PROFILE)
|
||||
--plan-protocol <v1|v2|both> plan transport(s) to compare (default: v1)
|
||||
--plan-protocol <v1|v2|both> plan transport(s) to compare (default: v2)
|
||||
--reserved-concurrency <N> Lambda Map MaxConcurrency cap (default: 16)
|
||||
--keep-stack skip `sam delete` at the end (manual teardown later)
|
||||
--require-encoded-sha-equal also gate byte-identical encoded MP4 output
|
||||
|
||||
@@ -222,12 +222,12 @@ Resources:
|
||||
- Variable: $.PlanProtocol
|
||||
IsPresent: true
|
||||
Next: UnsupportedPlanProtocol
|
||||
Default: Plan
|
||||
Default: PlanV2
|
||||
|
||||
UnsupportedPlanProtocol:
|
||||
Type: Fail
|
||||
Error: PLAN_PROTOCOL_UNSUPPORTED
|
||||
Cause: PlanProtocol must be "v1", "v2", or absent (defaults to v1).
|
||||
Cause: PlanProtocol must be "v1", "v2", or absent (defaults to v2).
|
||||
|
||||
Plan:
|
||||
Type: Task
|
||||
@@ -236,6 +236,7 @@ Resources:
|
||||
FunctionName: !GetAtt RenderFunction.Arn
|
||||
Payload:
|
||||
Action: plan
|
||||
PlanProtocol: v1
|
||||
ProjectS3Uri.$: "$.ProjectS3Uri"
|
||||
PlanOutputS3Prefix.$: "$.PlanOutputS3Prefix"
|
||||
Config.$: "$.Config"
|
||||
@@ -391,6 +392,7 @@ Resources:
|
||||
FunctionName: !GetAtt RenderFunction.Arn
|
||||
Payload:
|
||||
Action: renderChunk
|
||||
PlanProtocol: v1
|
||||
ChunkIndex.$: "$.ChunkIndex"
|
||||
PlanS3Uri.$: "$.PlanS3Uri"
|
||||
PlanHash.$: "$.PlanHash"
|
||||
@@ -427,6 +429,7 @@ Resources:
|
||||
FunctionName: !GetAtt RenderFunction.Arn
|
||||
Payload:
|
||||
Action: assemble
|
||||
PlanProtocol: v1
|
||||
PlanS3Uri.$: "$.Plan.PlanS3Uri"
|
||||
ChunkS3Uris.$: "$.Chunks[*].ChunkS3Uri"
|
||||
AudioS3Uri.$: "$.Plan.AudioS3Uri"
|
||||
|
||||
@@ -8,7 +8,7 @@ Cloud Workflows adapter for HyperFrames distributed rendering.
|
||||
|
||||
```text
|
||||
scripts/smoke.sh Owner-isolated real-GCP deploy, render, parity, cleanup
|
||||
sample-events/ v1 and v2 handler request examples
|
||||
sample-events/ Default-v2, explicit-v1, and explicit-v2 request examples
|
||||
```
|
||||
|
||||
The Terraform module and Cloud Workflows definition live in
|
||||
@@ -16,8 +16,9 @@ The Terraform module and Cloud Workflows definition live in
|
||||
|
||||
## Protocol rollout
|
||||
|
||||
The workflow defaults to plan protocol v1 when `PlanProtocol` is absent. V2 is
|
||||
accepted only when the caller explicitly sends `PlanProtocol: "v2"`.
|
||||
The workflow defaults to Plan v2 when `PlanProtocol` is absent. Deprecated v1
|
||||
compatibility remains available only when the caller explicitly sends
|
||||
`PlanProtocol: "v1"`.
|
||||
|
||||
V1 and v2 use disjoint plan locators:
|
||||
|
||||
@@ -26,9 +27,15 @@ V1 and v2 use disjoint plan locators:
|
||||
|
||||
The workflow validates that the plan response matches the selected protocol
|
||||
before starting chunk fan-out. It never silently falls back from v2 to v1.
|
||||
Deploy the v2 workflow only with a Cloud Run image whose handler implements
|
||||
the matching v2 request/response contract. An older v1-only handler will keep
|
||||
serving default v1 requests, but explicit v2 smoke executions will fail closed.
|
||||
Deploy the workflow only with a Cloud Run image whose handler implements the
|
||||
matching v2 request/response contract.
|
||||
|
||||
For an existing installation, pause new renders and drain active workflow
|
||||
executions. Redeploy the Cloud Run image and workflow from the same package
|
||||
version before upgrading the application SDK. The new SDK sends explicit v2;
|
||||
older workflows may still default omission to v1 or lack v2 support. Keep
|
||||
passing `planProtocol: "v1"` until the infrastructure redeploy completes if
|
||||
you need a staged migration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -40,7 +47,7 @@ serving default v1 requests, but explicit v2 smoke executions will fail closed.
|
||||
|
||||
## Run the smoke
|
||||
|
||||
V1 remains the safe default:
|
||||
Plan v2 is the normal smoke path:
|
||||
|
||||
```bash
|
||||
./scripts/smoke.sh \
|
||||
@@ -120,12 +127,17 @@ old invocation's state or image implicitly.
|
||||
The sample events mirror the request bodies sent by Cloud Workflows:
|
||||
|
||||
```bash
|
||||
# V1
|
||||
# Default v2 (PlanProtocol omitted)
|
||||
curl -sX POST localhost:8080/ \
|
||||
-H 'content-type: application/json' \
|
||||
--data @sample-events/plan.json | jq .
|
||||
|
||||
# Explicit v2
|
||||
# Deprecated explicit v1 compatibility
|
||||
curl -sX POST localhost:8080/ \
|
||||
-H 'content-type: application/json' \
|
||||
--data @sample-events/plan-v1.json | jq .
|
||||
|
||||
# Explicit v2 for callers that always stamp the protocol
|
||||
curl -sX POST localhost:8080/ \
|
||||
-H 'content-type: application/json' \
|
||||
--data @sample-events/plan-v2.json | jq .
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Action": "assemble",
|
||||
"PlanProtocol": "v1",
|
||||
"PlanGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/plan.tar.gz",
|
||||
"ChunkGcsUris": [
|
||||
"gs://hyperframes-render-PROJECT/renders/hf-render-demo/chunks/0000.mp4",
|
||||
"gs://hyperframes-render-PROJECT/renders/hf-render-demo/chunks/0001.mp4"
|
||||
],
|
||||
"AudioGcsUri": null,
|
||||
"OutputGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/output.mp4",
|
||||
"Format": "mp4"
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"Action": "assemble",
|
||||
"PlanProtocol": "v1",
|
||||
"PlanGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/plan.tar.gz",
|
||||
"PlanV2ManifestGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/v2/manifest.json",
|
||||
"PlanV2ArtifactGcsPrefix": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/v2/artifacts/sha256",
|
||||
"PlanHash": "REPLACE_WITH_PLAN_HASH",
|
||||
"ChunkGcsUris": [
|
||||
"gs://hyperframes-render-PROJECT/renders/hf-render-demo/chunks/0000.mp4",
|
||||
"gs://hyperframes-render-PROJECT/renders/hf-render-demo/chunks/0001.mp4"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"Action": "plan",
|
||||
"PlanProtocol": "v1",
|
||||
"ProjectGcsUri": "gs://hyperframes-render-PROJECT/sites/abc123/project.tar.gz",
|
||||
"PlanOutputGcsPrefix": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/",
|
||||
"Config": { "fps": 30, "width": 1920, "height": 1080, "format": "mp4" }
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"Action": "plan",
|
||||
"PlanProtocol": "v1",
|
||||
"ProjectGcsUri": "gs://hyperframes-render-PROJECT/sites/abc123/project.tar.gz",
|
||||
"PlanOutputGcsPrefix": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/",
|
||||
"Config": { "fps": 30, "width": 1920, "height": 1080, "format": "mp4" }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Action": "renderChunk",
|
||||
"PlanProtocol": "v1",
|
||||
"PlanGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/plan.tar.gz",
|
||||
"PlanHash": "REPLACE_WITH_PLAN_HASH",
|
||||
"ChunkIndex": 0,
|
||||
"ChunkOutputGcsPrefix": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/",
|
||||
"Format": "mp4"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Action": "renderChunk",
|
||||
"PlanProtocol": "v1",
|
||||
"PlanGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/plan.tar.gz",
|
||||
"PlanV2ManifestGcsUri": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/v2/manifest.json",
|
||||
"PlanV2ArtifactGcsPrefix": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/v2/artifacts/sha256",
|
||||
"PlanHash": "REPLACE_WITH_PLAN_HASH",
|
||||
"ChunkIndex": 0,
|
||||
"ChunkOutputGcsPrefix": "gs://hyperframes-render-PROJECT/renders/hf-render-demo/",
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
# Owner-isolated real-GCP smoke + v1/v2 parity test for the HyperFrames
|
||||
# Cloud Run adapter.
|
||||
#
|
||||
# The default is intentionally v1-only. Plan protocol v2 must be opted into
|
||||
# explicitly with --protocols v1,v2. Every invocation derives a unique,
|
||||
# length-safe resource prefix and uses an isolated Terraform working directory
|
||||
# and state file. Cleanup verifies every owned resource is absent and fails
|
||||
# closed on API/authentication errors.
|
||||
# The default exercises Plan v2. Deprecated v1 compatibility can be selected
|
||||
# explicitly with --protocols v1 or compared with --protocols v1,v2. Every
|
||||
# invocation derives a unique, length-safe resource prefix and uses an isolated
|
||||
# Terraform working directory and state file. Cleanup verifies every owned
|
||||
# resource is absent and fails closed on API/authentication errors.
|
||||
#
|
||||
# Usage:
|
||||
# ./smoke.sh --project <gcp-project>
|
||||
@@ -33,7 +33,7 @@ REGION="${GCP_REGION:-us-central1}"
|
||||
FIXTURE="${FIXTURE:-mp4-h264-sdr}"
|
||||
CHUNK_SIZES="${CHUNK_SIZES:-}"
|
||||
PSNR_THRESHOLD="${PSNR_THRESHOLD:-35}"
|
||||
PROTOCOLS="${PROTOCOLS:-v1}"
|
||||
PROTOCOLS="${PROTOCOLS:-v2}"
|
||||
OWNER="${HYPERFRAMES_SMOKE_OWNER:-}"
|
||||
AR_REPO="${AR_REPO:-}"
|
||||
AR_REPO_WAS_EXPLICIT=0
|
||||
|
||||
@@ -47,14 +47,13 @@ inside Step Functions' history budget (under 200 bytes per chunk).
|
||||
|
||||
### Plan transport selection
|
||||
|
||||
Plan v2 is recommended for new integrations. `renderToLambda` still defaults
|
||||
an omitted `planProtocol` to the existing monolithic v1 transport for
|
||||
backwards compatibility, so select v2 explicitly:
|
||||
Plan v2 is the default for new renders. When `planProtocol` is omitted,
|
||||
`renderToLambda` sends an explicit `PlanProtocol: "v2"` so the SDK and the
|
||||
deployed state machine agree:
|
||||
|
||||
```ts
|
||||
await renderToLambda({
|
||||
// ...bucket, state machine, project, and config...
|
||||
planProtocol: "v2",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -64,7 +63,24 @@ only manifest-selected chunk artifacts, while the assembler fetches its
|
||||
own metadata and audio subset. Blobs are immutable SHA-256-addressed
|
||||
objects, verified on upload and download, and the manifest is published
|
||||
last. Unknown protocols and digest mismatches are terminal Step Functions
|
||||
errors. Omit the selector—or use `"v1"`—to retain the prior wire contract.
|
||||
errors. The monolithic v1 transport remains available as deprecated
|
||||
compatibility by passing `planProtocol: "v1"` explicitly.
|
||||
|
||||
#### Upgrade order
|
||||
|
||||
This default changes application behavior and requires a coordinated
|
||||
infrastructure upgrade. Before upgrading an application that calls
|
||||
`renderToLambda`:
|
||||
|
||||
1. Pause new renders and let existing Step Functions executions drain.
|
||||
2. Redeploy the Lambda handler and SAM template or CDK construct from the
|
||||
same new package version.
|
||||
3. Resume renders, then upgrade the application/SDK dependency.
|
||||
|
||||
Older state machines can default missing protocol fields to v1 or lack v2
|
||||
branches, while the new SDK sends explicit v2. If infrastructure cannot be
|
||||
redeployed first, keep the application on its previous package version or
|
||||
pass `planProtocol: "v1"` explicitly until the redeploy is complete.
|
||||
|
||||
## Chrome runtime
|
||||
|
||||
|
||||
@@ -151,6 +151,18 @@ describe("HyperframesRenderStack — snapshot", () => {
|
||||
expect(actualStates.sort()).toEqual([...EXPECTED_STATE_NAMES].sort());
|
||||
});
|
||||
|
||||
it("defaults omitted plan protocol to v2 and preserves the explicit v1 branch", () => {
|
||||
for (const definition of [SYNTHED.definition, readSamDefinition()]) {
|
||||
const selection = requireRecord(
|
||||
definition.States.SelectPlanProtocol,
|
||||
"SelectPlanProtocol state",
|
||||
);
|
||||
expect(selection.Default).toBe("PlanV2");
|
||||
expect(JSON.stringify(selection)).toContain('"StringEquals":"v1"');
|
||||
expect(JSON.stringify(definition.States.Plan)).toContain('"PlanProtocol":"v1"');
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves every typed non-retryable error name across the three Lambda tasks", () => {
|
||||
const { definition } = SYNTHED;
|
||||
const collected = new Set<string>();
|
||||
|
||||
@@ -247,6 +247,7 @@ export class HyperframesRenderStack extends Construct {
|
||||
lambdaFunction: this.renderFunction,
|
||||
payload: sfn.TaskInput.fromObject({
|
||||
Action: "plan",
|
||||
PlanProtocol: "v1",
|
||||
"ProjectS3Uri.$": "$.ProjectS3Uri",
|
||||
"PlanOutputS3Prefix.$": "$.PlanOutputS3Prefix",
|
||||
"Config.$": "$.Config",
|
||||
@@ -319,6 +320,7 @@ export class HyperframesRenderStack extends Construct {
|
||||
lambdaFunction: this.renderFunction,
|
||||
payload: sfn.TaskInput.fromObject({
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
"ChunkIndex.$": "$.ChunkIndex",
|
||||
"PlanS3Uri.$": "$.PlanS3Uri",
|
||||
"PlanHash.$": "$.PlanHash",
|
||||
@@ -361,6 +363,7 @@ export class HyperframesRenderStack extends Construct {
|
||||
lambdaFunction: this.renderFunction,
|
||||
payload: sfn.TaskInput.fromObject({
|
||||
Action: "assemble",
|
||||
PlanProtocol: "v1",
|
||||
"PlanS3Uri.$": "$.Plan.PlanS3Uri",
|
||||
"ChunkS3Uris.$": "$.Chunks[*].ChunkS3Uri",
|
||||
"AudioS3Uri.$": "$.Plan.AudioS3Uri",
|
||||
@@ -473,13 +476,13 @@ export class HyperframesRenderStack extends Construct {
|
||||
|
||||
const unsupportedPlanProtocol = new sfn.Fail(this, "UnsupportedPlanProtocol", {
|
||||
error: "PLAN_PROTOCOL_UNSUPPORTED",
|
||||
cause: 'PlanProtocol must be "v1", "v2", or absent (defaults to v1).',
|
||||
cause: 'PlanProtocol must be "v1", "v2", or absent (defaults to v2).',
|
||||
});
|
||||
return new sfn.Choice(this, "SelectPlanProtocol")
|
||||
.when(sfn.Condition.stringEquals("$.PlanProtocol", "v2"), planV2)
|
||||
.when(sfn.Condition.stringEquals("$.PlanProtocol", "v1"), plan)
|
||||
.when(sfn.Condition.isPresent("$.PlanProtocol"), unsupportedPlanProtocol)
|
||||
.otherwise(plan);
|
||||
.otherwise(planV2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,17 +55,17 @@ interface PlanEventBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy/default plan transport. Absence is deliberately interpreted as v1.
|
||||
* Legacy plan transport. Callers must select it explicitly.
|
||||
*
|
||||
* @deprecated Use {@link PlanV2Event} for new integrations.
|
||||
*/
|
||||
export interface PlanV1Event extends PlanEventBase {
|
||||
PlanProtocol?: "v1";
|
||||
PlanProtocol: "v1";
|
||||
}
|
||||
|
||||
/** Explicit opt-in to the content-addressed v2 plan transport. */
|
||||
/** Default content-addressed v2 plan transport. */
|
||||
export interface PlanV2Event extends PlanEventBase {
|
||||
PlanProtocol: "v2";
|
||||
PlanProtocol?: "v2";
|
||||
}
|
||||
|
||||
export type PlanEvent = PlanV1Event | PlanV2Event;
|
||||
@@ -90,12 +90,12 @@ interface RenderChunkEventBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy/default chunk event.
|
||||
* Legacy chunk event. Callers must select it explicitly.
|
||||
*
|
||||
* @deprecated Use {@link RenderChunkV2Event} for new integrations.
|
||||
*/
|
||||
export interface RenderChunkV1Event extends RenderChunkEventBase {
|
||||
PlanProtocol?: "v1";
|
||||
PlanProtocol: "v1";
|
||||
/** S3 URI of the v1 plan tar produced by a PlanEvent invocation. */
|
||||
PlanS3Uri: string;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export interface RenderChunkV1Event extends RenderChunkEventBase {
|
||||
* describes the exact content-addressed artifacts needed by this chunk.
|
||||
*/
|
||||
export interface RenderChunkV2Event extends RenderChunkEventBase {
|
||||
PlanProtocol: "v2";
|
||||
PlanProtocol?: "v2";
|
||||
PlanV2ManifestS3Uri: string;
|
||||
PlanV2ArtifactS3Prefix: string;
|
||||
}
|
||||
@@ -136,19 +136,19 @@ interface AssembleEventBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy/default assemble event.
|
||||
* Legacy assemble event. Callers must select it explicitly.
|
||||
*
|
||||
* @deprecated Use {@link AssembleV2Event} for new integrations.
|
||||
*/
|
||||
export interface AssembleV1Event extends AssembleEventBase {
|
||||
PlanProtocol?: "v1";
|
||||
PlanProtocol: "v1";
|
||||
/** S3 URI of the v1 plan tar produced by a PlanEvent invocation. */
|
||||
PlanS3Uri: string;
|
||||
}
|
||||
|
||||
/** V2 assemble event, scoped to manifest-declared assembler artifacts. */
|
||||
export interface AssembleV2Event extends AssembleEventBase {
|
||||
PlanProtocol: "v2";
|
||||
PlanProtocol?: "v2";
|
||||
PlanV2ManifestS3Uri: string;
|
||||
PlanV2ArtifactS3Prefix: string;
|
||||
PlanHash: string;
|
||||
|
||||
@@ -145,6 +145,7 @@ describe("unwrapEvent", () => {
|
||||
it("unwraps a Step Functions { Payload } envelope", () => {
|
||||
const inner: RenderChunkEvent = {
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanS3Uri: "s3://bucket/plan.tar.gz",
|
||||
PlanHash: "deadbeef",
|
||||
ChunkIndex: 3,
|
||||
@@ -158,6 +159,7 @@ describe("unwrapEvent", () => {
|
||||
it("unwraps multiple levels of envelopes", () => {
|
||||
const inner: AssembleEvent = {
|
||||
Action: "assemble",
|
||||
PlanProtocol: "v1",
|
||||
PlanS3Uri: "s3://bucket/plan.tar.gz",
|
||||
ChunkS3Uris: ["s3://bucket/chunks/0001.mp4"],
|
||||
AudioS3Uri: null,
|
||||
@@ -176,7 +178,7 @@ describe("unwrapEvent", () => {
|
||||
});
|
||||
|
||||
describe("handler dispatch", () => {
|
||||
it("routes Action='plan' to the plan primitive", async () => {
|
||||
it("preserves explicit v1 plan compatibility", async () => {
|
||||
const tmpRoot = makeTmpRoot();
|
||||
const s3 = new FakeS3Client();
|
||||
// Seed a fake project tarball so the untar step has something to chew on.
|
||||
@@ -213,6 +215,7 @@ describe("handler dispatch", () => {
|
||||
|
||||
const event: PlanEvent = {
|
||||
Action: "plan",
|
||||
PlanProtocol: "v1",
|
||||
ProjectS3Uri: "s3://bucket/project.tar.gz",
|
||||
PlanOutputS3Prefix: "s3://bucket/renders/abc/",
|
||||
Config: { fps: 30, width: 1920, height: 1080, format: "mp4" },
|
||||
@@ -270,6 +273,7 @@ describe("handler dispatch", () => {
|
||||
handler(
|
||||
{
|
||||
Action: "plan",
|
||||
PlanProtocol: "v1",
|
||||
ProjectS3Uri: "s3://bucket/project.tar.gz",
|
||||
PlanOutputS3Prefix: "s3://bucket/renders/terminal/",
|
||||
Config: { fps: 30, width: 640, height: 360, format: "mp4" },
|
||||
@@ -332,6 +336,7 @@ describe("handler dispatch", () => {
|
||||
|
||||
const event: PlanEvent = {
|
||||
Action: "plan",
|
||||
PlanProtocol: "v1",
|
||||
ProjectS3Uri: "s3://bucket/project.tar.gz",
|
||||
PlanOutputS3Prefix: "s3://bucket/renders/abc/",
|
||||
Config: { fps: 30, width: 1920, height: 1080, format: "mp4" },
|
||||
@@ -406,6 +411,7 @@ describe("handler dispatch", () => {
|
||||
|
||||
const event: RenderChunkEvent = {
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanS3Uri: "s3://bucket/plan.tar.gz",
|
||||
PlanHash: "fakehash",
|
||||
ChunkIndex: 2,
|
||||
@@ -455,6 +461,7 @@ describe("handler dispatch", () => {
|
||||
|
||||
const event: RenderChunkEvent = {
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanS3Uri: "s3://bucket/plan.tar.gz",
|
||||
PlanHash: "not-the-real-hash",
|
||||
ChunkIndex: 0,
|
||||
@@ -511,6 +518,7 @@ describe("handler dispatch", () => {
|
||||
|
||||
const event: AssembleEvent = {
|
||||
Action: "assemble",
|
||||
PlanProtocol: "v1",
|
||||
PlanS3Uri: "s3://bucket/plan.tar.gz",
|
||||
ChunkS3Uris: ["s3://bucket/chunks/0001.mp4", "s3://bucket/chunks/0002.mp4"],
|
||||
AudioS3Uri: null,
|
||||
@@ -541,7 +549,7 @@ describe("handler dispatch", () => {
|
||||
expect(assembleMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("runs v2 plan → target-scoped chunk → assemble without a PlanS3Uri", async () => {
|
||||
it("defaults omitted plan protocol to v2 across plan → chunk → assemble", async () => {
|
||||
const tmpRoot = makeTmpRoot();
|
||||
const s3 = new FakeS3Client();
|
||||
s3.objects.set("s3://bucket/project.tar.gz", await makeMinimalProjectTar());
|
||||
@@ -614,7 +622,6 @@ describe("handler dispatch", () => {
|
||||
const planned = await handler(
|
||||
{
|
||||
Action: "plan",
|
||||
PlanProtocol: "v2",
|
||||
ProjectS3Uri: "s3://bucket/project.tar.gz",
|
||||
PlanOutputS3Prefix: "s3://bucket/renders/v2/",
|
||||
Config: { fps: 30, width: 640, height: 360, format: "mp4" },
|
||||
@@ -642,7 +649,6 @@ describe("handler dispatch", () => {
|
||||
const chunk = await handler(
|
||||
{
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v2",
|
||||
PlanV2ManifestS3Uri: planned.PlanV2ManifestS3Uri,
|
||||
PlanV2ArtifactS3Prefix: planned.PlanV2ArtifactS3Prefix,
|
||||
PlanHash: planned.PlanHash,
|
||||
@@ -661,7 +667,6 @@ describe("handler dispatch", () => {
|
||||
await handler(
|
||||
{
|
||||
Action: "assemble",
|
||||
PlanProtocol: "v2",
|
||||
PlanV2ManifestS3Uri: planned.PlanV2ManifestS3Uri,
|
||||
PlanV2ArtifactS3Prefix: planned.PlanV2ArtifactS3Prefix,
|
||||
PlanHash: planned.PlanHash,
|
||||
@@ -677,6 +682,56 @@ describe("handler dispatch", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unknown plan protocol values", async () => {
|
||||
const tmpRoot = makeTmpRoot();
|
||||
const s3 = new FakeS3Client();
|
||||
|
||||
await expect(
|
||||
handler(
|
||||
{
|
||||
Action: "plan",
|
||||
PlanProtocol: "v3",
|
||||
ProjectS3Uri: "s3://bucket/project.tar.gz",
|
||||
PlanOutputS3Prefix: "s3://bucket/renders/invalid/",
|
||||
Config: { fps: 30, width: 640, height: 360, format: "mp4" },
|
||||
} as unknown as LambdaEvent,
|
||||
{
|
||||
s3: s3 as unknown as import("@aws-sdk/client-s3").S3Client,
|
||||
tmpRoot,
|
||||
skipChromeResolution: true,
|
||||
},
|
||||
),
|
||||
).rejects.toMatchObject({ name: "PLAN_PROTOCOL_UNSUPPORTED" });
|
||||
expect(s3.ops).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects mixed v1/v2 plan locators at runtime", async () => {
|
||||
const tmpRoot = makeTmpRoot();
|
||||
const s3 = new FakeS3Client();
|
||||
|
||||
await expect(
|
||||
handler(
|
||||
{
|
||||
Action: "renderChunk",
|
||||
PlanS3Uri: "s3://bucket/plan.tar.gz",
|
||||
PlanHash: "fakehash",
|
||||
ChunkIndex: 0,
|
||||
ChunkOutputS3Prefix: "s3://bucket/renders/mixed/",
|
||||
Format: "mp4",
|
||||
} as unknown as LambdaEvent,
|
||||
{
|
||||
s3: s3 as unknown as import("@aws-sdk/client-s3").S3Client,
|
||||
tmpRoot,
|
||||
skipChromeResolution: true,
|
||||
},
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
name: "PLAN_PROTOCOL_UNSUPPORTED",
|
||||
message: expect.stringContaining("mixed or missing plan locators"),
|
||||
});
|
||||
expect(s3.ops).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects unknown actions", async () => {
|
||||
const tmpRoot = makeTmpRoot();
|
||||
await expect(
|
||||
@@ -735,6 +790,7 @@ describe("handler — S3 URI allowlist (security: F-004)", () => {
|
||||
|
||||
const event: AssembleEvent = {
|
||||
Action: "assemble",
|
||||
PlanProtocol: "v1",
|
||||
PlanS3Uri: "s3://good-bucket/plan.tar.gz",
|
||||
ChunkS3Uris: ["s3://good-bucket/chunks/0001.mp4", "s3://evil-bucket/chunks/0002.mp4"],
|
||||
AudioS3Uri: null,
|
||||
|
||||
@@ -44,9 +44,12 @@ import type {
|
||||
LambdaEvent,
|
||||
LambdaResult,
|
||||
PlanEvent,
|
||||
PlanV2Event,
|
||||
PlanLambdaResult,
|
||||
RenderChunkEvent,
|
||||
RenderChunkV2Event,
|
||||
RenderChunkLambdaResult,
|
||||
AssembleV2Event,
|
||||
} from "./events.js";
|
||||
import {
|
||||
downloadS3ObjectToFile,
|
||||
@@ -97,6 +100,7 @@ export interface HandlerDeps {
|
||||
*/
|
||||
export async function handler(event: LambdaEvent, deps?: HandlerDeps): Promise<LambdaResult> {
|
||||
const unwrapped = unwrapEvent(event);
|
||||
validatePlanProtocolShape(unwrapped);
|
||||
validateEventS3Uris(unwrapped);
|
||||
primeRuntimeEnv();
|
||||
// Single structured boot log line — CloudWatch Logs Insights queries
|
||||
@@ -201,6 +205,43 @@ function isLambdaAction(value: string): value is LambdaAction {
|
||||
return value === "plan" || value === "renderChunk" || value === "assemble";
|
||||
}
|
||||
|
||||
// This is the single fail-closed boundary for the wire union. Keeping all
|
||||
// forbidden locator combinations together makes mixed-protocol input auditable.
|
||||
// fallow-ignore-next-line complexity
|
||||
function validatePlanProtocolShape(event: PlanEvent | RenderChunkEvent | AssembleEvent): void {
|
||||
const raw = event as unknown as Record<string, unknown>;
|
||||
const protocol = raw.PlanProtocol;
|
||||
if (protocol !== undefined && protocol !== "v1" && protocol !== "v2") {
|
||||
const error = new Error(
|
||||
`[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected "v1", "v2", or absent`,
|
||||
);
|
||||
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
||||
throw error;
|
||||
}
|
||||
if (event.Action === "plan") return;
|
||||
|
||||
const effectiveProtocol = protocol ?? "v2";
|
||||
const hasV1Locator = typeof raw.PlanS3Uri === "string";
|
||||
const hasV2Manifest = typeof raw.PlanV2ManifestS3Uri === "string";
|
||||
const hasV2Prefix = typeof raw.PlanV2ArtifactS3Prefix === "string";
|
||||
const valid =
|
||||
effectiveProtocol === "v2"
|
||||
? !hasV1Locator && hasV2Manifest && hasV2Prefix
|
||||
: hasV1Locator && !hasV2Manifest && !hasV2Prefix;
|
||||
if (!valid) {
|
||||
const error = new Error(
|
||||
`[handler] ${effectiveProtocol} ${event.Action} event has mixed or missing plan locators`,
|
||||
);
|
||||
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
||||
throw error;
|
||||
}
|
||||
if (effectiveProtocol === "v2" && event.Action === "assemble" && event.AudioS3Uri !== null) {
|
||||
const error = new Error("[handler] v2 assemble audio must be materialized from the manifest");
|
||||
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a single JSON line to stdout. CloudWatch ingests each line as a
|
||||
* structured event; Logs Insights queries can `filter event="..."` and
|
||||
@@ -229,14 +270,14 @@ function summarizeEvent(
|
||||
return {
|
||||
projectS3Uri: event.ProjectS3Uri,
|
||||
planOutputS3Prefix: event.PlanOutputS3Prefix,
|
||||
planProtocol: event.PlanProtocol ?? "v1",
|
||||
planProtocol: event.PlanProtocol ?? "v2",
|
||||
format: event.Config.format,
|
||||
fps: event.Config.fps,
|
||||
};
|
||||
case "renderChunk":
|
||||
return {
|
||||
planProtocol: event.PlanProtocol ?? "v1",
|
||||
...(event.PlanProtocol === "v2"
|
||||
planProtocol: event.PlanProtocol ?? "v2",
|
||||
...(event.PlanProtocol !== "v1"
|
||||
? { planV2ManifestS3Uri: event.PlanV2ManifestS3Uri }
|
||||
: { planS3Uri: event.PlanS3Uri }),
|
||||
chunkIndex: event.ChunkIndex,
|
||||
@@ -244,8 +285,8 @@ function summarizeEvent(
|
||||
};
|
||||
case "assemble":
|
||||
return {
|
||||
planProtocol: event.PlanProtocol ?? "v1",
|
||||
...(event.PlanProtocol === "v2"
|
||||
planProtocol: event.PlanProtocol ?? "v2",
|
||||
...(event.PlanProtocol !== "v1"
|
||||
? { planV2ManifestS3Uri: event.PlanV2ManifestS3Uri }
|
||||
: { planS3Uri: event.PlanS3Uri }),
|
||||
chunkCount: event.ChunkS3Uris.length,
|
||||
@@ -278,7 +319,7 @@ function primeRuntimeEnv(): void {
|
||||
// The v1 handler owns one transactional download, plan, archive, upload, and cleanup lifecycle.
|
||||
// fallow-ignore-next-line complexity
|
||||
async function handlePlan(event: PlanEvent, deps?: HandlerDeps): Promise<PlanLambdaResult> {
|
||||
if (event.PlanProtocol === "v2") {
|
||||
if (event.PlanProtocol !== "v1") {
|
||||
return handlePlanV2(event, deps);
|
||||
}
|
||||
const started = Date.now();
|
||||
@@ -358,7 +399,7 @@ async function handlePlan(event: PlanEvent, deps?: HandlerDeps): Promise<PlanLam
|
||||
// and manifest-last publication must stay ordered and fail together.
|
||||
// fallow-ignore-next-line complexity
|
||||
async function handlePlanV2(
|
||||
event: Extract<PlanEvent, { PlanProtocol: "v2" }>,
|
||||
event: PlanV2Event,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<Extract<PlanLambdaResult, { PlanProtocol: "v2" }>> {
|
||||
const started = Date.now();
|
||||
@@ -412,7 +453,7 @@ async function handleRenderChunk(
|
||||
event: RenderChunkEvent,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<RenderChunkLambdaResult> {
|
||||
if (event.PlanProtocol === "v2") {
|
||||
if (event.PlanProtocol !== "v1") {
|
||||
return handleRenderChunkV2(event, deps);
|
||||
}
|
||||
const started = Date.now();
|
||||
@@ -481,7 +522,7 @@ async function handleRenderChunk(
|
||||
// render, and upload in one lifecycle so cleanup and errors remain atomic.
|
||||
// fallow-ignore-next-line complexity
|
||||
async function handleRenderChunkV2(
|
||||
event: Extract<RenderChunkEvent, { PlanProtocol: "v2" }>,
|
||||
event: RenderChunkV2Event,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<RenderChunkLambdaResult> {
|
||||
const started = Date.now();
|
||||
@@ -554,7 +595,7 @@ async function handleAssemble(
|
||||
event: AssembleEvent,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<AssembleLambdaResult> {
|
||||
if (event.PlanProtocol === "v2") {
|
||||
if (event.PlanProtocol !== "v1") {
|
||||
return handleAssembleV2(event, deps);
|
||||
}
|
||||
const started = Date.now();
|
||||
@@ -610,7 +651,7 @@ async function handleAssemble(
|
||||
// keeping the steps local makes its temporary-storage ownership explicit.
|
||||
// fallow-ignore-next-line complexity
|
||||
async function handleAssembleV2(
|
||||
event: Extract<AssembleEvent, { PlanProtocol: "v2" }>,
|
||||
event: AssembleV2Event,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<AssembleLambdaResult> {
|
||||
const started = Date.now();
|
||||
@@ -776,12 +817,12 @@ function getEventS3Uris(event: PlanEvent | RenderChunkEvent | AssembleEvent): st
|
||||
case "plan":
|
||||
return [event.ProjectS3Uri, event.PlanOutputS3Prefix];
|
||||
case "renderChunk":
|
||||
return event.PlanProtocol === "v2"
|
||||
return event.PlanProtocol !== "v1"
|
||||
? [event.PlanV2ManifestS3Uri, event.PlanV2ArtifactS3Prefix, event.ChunkOutputS3Prefix]
|
||||
: [event.PlanS3Uri, event.ChunkOutputS3Prefix];
|
||||
case "assemble":
|
||||
return [
|
||||
...(event.PlanProtocol === "v2"
|
||||
...(event.PlanProtocol !== "v1"
|
||||
? [event.PlanV2ManifestS3Uri, event.PlanV2ArtifactS3Prefix]
|
||||
: [event.PlanS3Uri]),
|
||||
...event.ChunkS3Uris,
|
||||
|
||||
@@ -98,11 +98,11 @@ describe("renderToLambda", () => {
|
||||
PlanOutputS3Prefix: "s3://test-bucket/renders/smoke-1/",
|
||||
OutputS3Uri: "s3://test-bucket/renders/smoke-1/output.mp4",
|
||||
Config: baseConfig,
|
||||
PlanProtocol: "v1",
|
||||
PlanProtocol: "v2",
|
||||
});
|
||||
});
|
||||
|
||||
it("opts the complete execution into plan protocol v2 explicitly", async () => {
|
||||
it("preserves explicit plan protocol v1 compatibility", async () => {
|
||||
const sfn = new FakeSFN();
|
||||
const s3 = new FakeS3();
|
||||
const handle = await renderToLambda({
|
||||
@@ -110,18 +110,18 @@ describe("renderToLambda", () => {
|
||||
bucketName: "test-bucket",
|
||||
stateMachineArn: "arn:aws:states:us-east-1:1234:stateMachine:hf",
|
||||
config: baseConfig,
|
||||
executionName: "smoke-v2",
|
||||
planProtocol: "v2",
|
||||
executionName: "smoke-v1",
|
||||
planProtocol: "v1",
|
||||
sfn: asSFNClient(sfn),
|
||||
s3: asS3Client(s3),
|
||||
});
|
||||
|
||||
expect(sfn.starts[0]?.input).toEqual({
|
||||
ProjectS3Uri: handle.projectS3Uri,
|
||||
PlanOutputS3Prefix: "s3://test-bucket/renders/smoke-v2/",
|
||||
OutputS3Uri: "s3://test-bucket/renders/smoke-v2/output.mp4",
|
||||
PlanOutputS3Prefix: "s3://test-bucket/renders/smoke-v1/",
|
||||
OutputS3Uri: "s3://test-bucket/renders/smoke-v1/output.mp4",
|
||||
Config: baseConfig,
|
||||
PlanProtocol: "v2",
|
||||
PlanProtocol: "v1",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ export interface RenderToLambdaOptions {
|
||||
/** Validated `SerializableDistributedRenderConfig` (no logger / abortSignal). */
|
||||
config: SerializableDistributedRenderConfig;
|
||||
/**
|
||||
* Distributed plan transport. Defaults to `"v1"` for backwards
|
||||
* compatibility. New integrations should explicitly select `"v2"`.
|
||||
* Distributed plan transport. Defaults to `"v2"`. Select `"v1"`
|
||||
* explicitly only for deprecated compatibility with the monolithic plan.
|
||||
*/
|
||||
planProtocol?: LambdaPlanProtocol;
|
||||
/** S3 bucket from the SAM stack output (`RenderBucketName`). */
|
||||
@@ -115,7 +115,7 @@ export async function renderToLambda(opts: RenderToLambdaOptions): Promise<Rende
|
||||
PlanOutputS3Prefix: planOutputS3Prefix,
|
||||
OutputS3Uri: outputS3Uri,
|
||||
Config: opts.config,
|
||||
PlanProtocol: opts.planProtocol ?? "v1",
|
||||
PlanProtocol: opts.planProtocol ?? "v2",
|
||||
};
|
||||
|
||||
// Reject oversize input client-side. Step Functions Standard caps the
|
||||
|
||||
@@ -45,14 +45,13 @@ per-step durations on success.
|
||||
|
||||
### Plan transport selection
|
||||
|
||||
Plan v2 is recommended for new integrations. `renderToCloudRun` still
|
||||
interprets an omitted `planProtocol` as `"v1"` for backwards compatibility,
|
||||
so new callers should select v2 explicitly:
|
||||
Plan v2 is the default for new renders. When `planProtocol` is omitted,
|
||||
`renderToCloudRun` sends an explicit `PlanProtocol: "v2"` so the SDK and
|
||||
the deployed workflow agree:
|
||||
|
||||
```ts
|
||||
await renderToCloudRun({
|
||||
// ...project, bucket, workflow, service, and config...
|
||||
planProtocol: "v2",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -60,6 +59,20 @@ V2 uses separate manifest and content-addressed artifact locators throughout
|
||||
the workflow. Unknown protocols and integrity failures fail closed; a render
|
||||
never mixes v1 and v2 artifacts.
|
||||
|
||||
The monolithic v1 transport remains available as deprecated compatibility by
|
||||
passing `planProtocol: "v1"` explicitly.
|
||||
|
||||
#### Upgrade order
|
||||
|
||||
Redeploy the Cloud Run image and Cloud Workflows definition from the same new
|
||||
package version before upgrading an application that calls
|
||||
`renderToCloudRun`. Pause new renders and drain active workflow executions
|
||||
during the infrastructure update. Older workflows can default omission to v1
|
||||
or lack the v2 branch, while the new SDK sends explicit v2. If infrastructure
|
||||
cannot be redeployed first, keep the previous SDK version or pass
|
||||
`planProtocol: "v1"` explicitly until the Terraform/workflow redeploy is
|
||||
complete.
|
||||
|
||||
## Chrome runtime
|
||||
|
||||
Unlike the Lambda adapter — which fights a 250 MB ZIP ceiling and
|
||||
|
||||
@@ -61,17 +61,17 @@ interface PlanEventBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy/default plan transport. Absence is deliberately interpreted as v1.
|
||||
* Legacy plan transport. Callers must select it explicitly.
|
||||
*
|
||||
* @deprecated Use {@link PlanV2Event} for new integrations.
|
||||
*/
|
||||
export interface PlanV1Event extends PlanEventBase {
|
||||
PlanProtocol?: "v1";
|
||||
PlanProtocol: "v1";
|
||||
}
|
||||
|
||||
/** Explicit opt-in to the content-addressed v2 plan transport. */
|
||||
/** Default content-addressed v2 plan transport. */
|
||||
export interface PlanV2Event extends PlanEventBase {
|
||||
PlanProtocol: "v2";
|
||||
PlanProtocol?: "v2";
|
||||
}
|
||||
|
||||
export type PlanEvent = PlanV1Event | PlanV2Event;
|
||||
@@ -96,12 +96,12 @@ interface RenderChunkEventBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy/default chunk event.
|
||||
* Legacy chunk event. Callers must select it explicitly.
|
||||
*
|
||||
* @deprecated Use {@link RenderChunkV2Event} for new integrations.
|
||||
*/
|
||||
export interface RenderChunkV1Event extends RenderChunkEventBase {
|
||||
PlanProtocol?: "v1";
|
||||
PlanProtocol: "v1";
|
||||
/** GCS URI of the v1 plan tar produced by a PlanEvent invocation. */
|
||||
PlanGcsUri: string;
|
||||
PlanV2ManifestGcsUri?: never;
|
||||
@@ -113,7 +113,7 @@ export interface RenderChunkV1Event extends RenderChunkEventBase {
|
||||
* describes the exact content-addressed artifacts needed by this chunk.
|
||||
*/
|
||||
export interface RenderChunkV2Event extends RenderChunkEventBase {
|
||||
PlanProtocol: "v2";
|
||||
PlanProtocol?: "v2";
|
||||
PlanV2ManifestGcsUri: string;
|
||||
PlanV2ArtifactGcsPrefix: string;
|
||||
PlanGcsUri?: never;
|
||||
@@ -143,12 +143,12 @@ interface AssembleEventBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy/default assemble event.
|
||||
* Legacy assemble event. Callers must select it explicitly.
|
||||
*
|
||||
* @deprecated Use {@link AssembleV2Event} for new integrations.
|
||||
*/
|
||||
export interface AssembleV1Event extends AssembleEventBase {
|
||||
PlanProtocol?: "v1";
|
||||
PlanProtocol: "v1";
|
||||
/** GCS URI of the v1 plan tar produced by a PlanEvent invocation. */
|
||||
PlanGcsUri: string;
|
||||
/** Legacy standalone audio locator; `null` when audio is embedded in the v1 plan tar. */
|
||||
@@ -159,7 +159,7 @@ export interface AssembleV1Event extends AssembleEventBase {
|
||||
|
||||
/** V2 assemble event, scoped to manifest-declared assembler artifacts. */
|
||||
export interface AssembleV2Event extends AssembleEventBase {
|
||||
PlanProtocol: "v2";
|
||||
PlanProtocol?: "v2";
|
||||
PlanV2ManifestGcsUri: string;
|
||||
PlanV2ArtifactGcsPrefix: string;
|
||||
PlanHash: string;
|
||||
|
||||
@@ -88,16 +88,16 @@ describe("renderToCloudRun", () => {
|
||||
OutputGcsUri: "gs://b/renders/hf-render-fixed/output.mp4",
|
||||
ServiceUrl: "https://render-abc.run.app",
|
||||
Config: config,
|
||||
PlanProtocol: "v1",
|
||||
PlanProtocol: "v2",
|
||||
});
|
||||
expect(fake.lastParent).toBe(
|
||||
"projects/proj/locations/us-central1/workflows/hyperframes-render",
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards an explicit v2 whole-render opt-in", async () => {
|
||||
it("preserves explicit plan protocol v1 compatibility", async () => {
|
||||
const fake = new FakeExecutions();
|
||||
await renderToCloudRun({ ...opts(fake), planProtocol: "v2" });
|
||||
await renderToCloudRun({ ...opts(fake), planProtocol: "v1" });
|
||||
const arg = JSON.parse(fake.lastArgument ?? "{}");
|
||||
expect(arg).toEqual({
|
||||
RenderId: "hf-render-fixed",
|
||||
@@ -106,7 +106,7 @@ describe("renderToCloudRun", () => {
|
||||
OutputGcsUri: "gs://b/renders/hf-render-fixed/output.mp4",
|
||||
ServiceUrl: "https://render-abc.run.app",
|
||||
Config: config,
|
||||
PlanProtocol: "v2",
|
||||
PlanProtocol: "v1",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ export interface RenderToCloudRunOptions {
|
||||
/** Validated `SerializableDistributedRenderConfig` (no logger / abortSignal). */
|
||||
config: SerializableDistributedRenderConfig;
|
||||
/**
|
||||
* Distributed plan transport. Defaults to `"v1"` for backwards
|
||||
* compatibility. New integrations should explicitly select `"v2"`.
|
||||
* Distributed plan transport. Defaults to `"v2"`. Select `"v1"`
|
||||
* explicitly only for deprecated compatibility with the monolithic plan.
|
||||
*/
|
||||
planProtocol?: CloudRunPlanProtocol;
|
||||
/** GCS bucket from the Terraform output (`render_bucket_name`). */
|
||||
@@ -149,7 +149,7 @@ export async function renderToCloudRun(opts: RenderToCloudRunOptions): Promise<R
|
||||
OutputGcsUri: outputGcsUri,
|
||||
ServiceUrl: opts.serviceUrl,
|
||||
Config: opts.config,
|
||||
PlanProtocol: opts.planProtocol ?? "v1",
|
||||
PlanProtocol: opts.planProtocol ?? "v2",
|
||||
};
|
||||
|
||||
// Reject oversize input client-side. Cloud Workflows caps the execution
|
||||
|
||||
@@ -178,11 +178,12 @@ describe("unwrapEvent", () => {
|
||||
});
|
||||
|
||||
describe("dispatch", () => {
|
||||
it("routes plan, uploads the plan tarball", async () => {
|
||||
it("preserves explicit v1 plan compatibility", async () => {
|
||||
const gcs = new FakeGcs();
|
||||
await seedProjectTar(gcs, "gs://b/sites/x/project.tar.gz");
|
||||
const event: PlanEvent = {
|
||||
Action: "plan",
|
||||
PlanProtocol: "v1",
|
||||
ProjectGcsUri: "gs://b/sites/x/project.tar.gz",
|
||||
PlanOutputGcsPrefix: "gs://b/renders/r1/",
|
||||
Config: { fps: 30, width: 1920, height: 1080, format: "mp4" } as PlanEvent["Config"],
|
||||
@@ -201,6 +202,7 @@ describe("dispatch", () => {
|
||||
await seedPlanTar(gcs, "gs://b/renders/r1/plan.tar.gz", PLAN_HASH);
|
||||
const event: RenderChunkEvent = {
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/plan.tar.gz",
|
||||
PlanHash: PLAN_HASH,
|
||||
ChunkIndex: 2,
|
||||
@@ -220,6 +222,7 @@ describe("dispatch", () => {
|
||||
await seedPlanTar(gcs, "gs://b/renders/r1/plan.tar.gz", PLAN_HASH);
|
||||
const event: RenderChunkEvent = {
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/plan.tar.gz",
|
||||
PlanHash: "WRONG_HASH",
|
||||
ChunkIndex: 0,
|
||||
@@ -236,6 +239,7 @@ describe("dispatch", () => {
|
||||
gcs.seed("gs://b/renders/r1/chunks/0001.mp4", Buffer.from("c1"));
|
||||
const event: AssembleEvent = {
|
||||
Action: "assemble",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/plan.tar.gz",
|
||||
ChunkGcsUris: ["gs://b/renders/r1/chunks/0000.mp4", "gs://b/renders/r1/chunks/0001.mp4"],
|
||||
AudioGcsUri: null,
|
||||
@@ -251,7 +255,7 @@ describe("dispatch", () => {
|
||||
// This end-to-end adapter contract is intentionally one narrative test: it
|
||||
// verifies ordering and target isolation across all three handler roles.
|
||||
// fallow-ignore-next-line complexity
|
||||
it("runs v2 plan → target-scoped chunk → assemble with manifest-last CAS", async () => {
|
||||
it("defaults omitted plan protocol to v2 across plan → chunk → assemble", async () => {
|
||||
const gcs = new FakeGcs();
|
||||
await seedProjectTar(gcs, "gs://b/sites/v2/project.tar.gz");
|
||||
const root = mkTmp("hf-v2-e2e-");
|
||||
@@ -306,7 +310,6 @@ describe("dispatch", () => {
|
||||
const planned = await dispatch(
|
||||
{
|
||||
Action: "plan",
|
||||
PlanProtocol: "v2",
|
||||
ProjectGcsUri: "gs://b/sites/v2/project.tar.gz",
|
||||
PlanOutputGcsPrefix: "gs://b/renders/v2/",
|
||||
Config: { fps: 30, width: 640, height: 360, format: "mp4" },
|
||||
@@ -326,7 +329,6 @@ describe("dispatch", () => {
|
||||
await dispatch(
|
||||
{
|
||||
Action: "plan",
|
||||
PlanProtocol: "v2",
|
||||
ProjectGcsUri: "gs://b/sites/v2/project.tar.gz",
|
||||
PlanOutputGcsPrefix: "gs://b/renders/v2/",
|
||||
Config: { fps: 30, width: 640, height: 360, format: "mp4" },
|
||||
@@ -342,7 +344,6 @@ describe("dispatch", () => {
|
||||
const chunk = await dispatch(
|
||||
{
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v2",
|
||||
PlanV2ManifestGcsUri: planned.PlanV2ManifestGcsUri,
|
||||
PlanV2ArtifactGcsPrefix: planned.PlanV2ArtifactGcsPrefix,
|
||||
PlanHash: planned.PlanHash,
|
||||
@@ -360,7 +361,6 @@ describe("dispatch", () => {
|
||||
await dispatch(
|
||||
{
|
||||
Action: "assemble",
|
||||
PlanProtocol: "v2",
|
||||
PlanV2ManifestGcsUri: planned.PlanV2ManifestGcsUri,
|
||||
PlanV2ArtifactGcsPrefix: planned.PlanV2ArtifactGcsPrefix,
|
||||
PlanHash: planned.PlanHash,
|
||||
@@ -409,6 +409,7 @@ describe("bucket allowlist guard", () => {
|
||||
try {
|
||||
const event: RenderChunkEvent = {
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://evil-bucket/plan.tar.gz",
|
||||
PlanHash: PLAN_HASH,
|
||||
ChunkIndex: 0,
|
||||
@@ -451,6 +452,7 @@ describe("bucket allowlist guard", () => {
|
||||
try {
|
||||
const event: RenderChunkEvent = {
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://any-bucket/renders/r1/plan.tar.gz",
|
||||
PlanHash: PLAN_HASH,
|
||||
ChunkIndex: 0,
|
||||
@@ -476,6 +478,7 @@ describe("createApp HTTP mapping", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/plan.tar.gz",
|
||||
PlanHash: PLAN_HASH,
|
||||
ChunkIndex: 0,
|
||||
@@ -497,6 +500,7 @@ describe("createApp HTTP mapping", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/plan.tar.gz",
|
||||
PlanHash: "WRONG",
|
||||
ChunkIndex: 0,
|
||||
@@ -524,6 +528,7 @@ describe("createApp HTTP mapping", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/plan.tar.gz",
|
||||
PlanHash: PLAN_HASH,
|
||||
ChunkIndex: 0,
|
||||
@@ -561,6 +566,7 @@ describe("createApp HTTP mapping", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/plan.tar.gz",
|
||||
PlanHash: PLAN_HASH,
|
||||
ChunkIndex: 0,
|
||||
@@ -598,6 +604,7 @@ describe("createApp HTTP mapping", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/plan.tar.gz",
|
||||
PlanHash: PLAN_HASH,
|
||||
ChunkIndex: 0,
|
||||
@@ -626,6 +633,7 @@ describe("createApp HTTP mapping", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "plan",
|
||||
PlanProtocol: "v1",
|
||||
ProjectGcsUri: "gs://b/sites/invalid-video-metadata/project.tar.gz",
|
||||
PlanOutputGcsPrefix: "gs://b/renders/invalid-video-metadata/",
|
||||
Config: { fps: 30, width: 640, height: 360, format: "mp4" },
|
||||
@@ -645,6 +653,7 @@ describe("createApp HTTP mapping", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "renderChunk",
|
||||
PlanProtocol: "v1",
|
||||
PlanGcsUri: "gs://b/renders/r1/missing.tar.gz",
|
||||
PlanHash: PLAN_HASH,
|
||||
ChunkIndex: 0,
|
||||
|
||||
@@ -46,13 +46,16 @@ import {
|
||||
import { resolveChromeExecutablePath } from "./chromium.js";
|
||||
import type {
|
||||
AssembleEvent,
|
||||
AssembleV2Event,
|
||||
AssembleResultBody,
|
||||
CloudRunAction,
|
||||
CloudRunEvent,
|
||||
CloudRunResult,
|
||||
PlanEvent,
|
||||
PlanV2Event,
|
||||
PlanResultBody,
|
||||
RenderChunkEvent,
|
||||
RenderChunkV2Event,
|
||||
RenderChunkResultBody,
|
||||
} from "./events.js";
|
||||
import { type DistributedFormat, formatExtension } from "./formatExtension.js";
|
||||
@@ -155,21 +158,22 @@ function validatePlanProtocolShape(event: PlanEvent | RenderChunkEvent | Assembl
|
||||
}
|
||||
if (event.Action === "plan") return;
|
||||
|
||||
const effectiveProtocol = protocol ?? "v2";
|
||||
const hasV1Locator = typeof raw.PlanGcsUri === "string";
|
||||
const hasV2Manifest = typeof raw.PlanV2ManifestGcsUri === "string";
|
||||
const hasV2Prefix = typeof raw.PlanV2ArtifactGcsPrefix === "string";
|
||||
const valid =
|
||||
protocol === "v2"
|
||||
effectiveProtocol === "v2"
|
||||
? !hasV1Locator && hasV2Manifest && hasV2Prefix
|
||||
: hasV1Locator && !hasV2Manifest && !hasV2Prefix;
|
||||
if (!valid) {
|
||||
const error = new Error(
|
||||
`[handler] ${protocol === "v2" ? "v2" : "v1"} ${event.Action} event has mixed or missing plan locators`,
|
||||
`[handler] ${effectiveProtocol} ${event.Action} event has mixed or missing plan locators`,
|
||||
);
|
||||
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
||||
throw error;
|
||||
}
|
||||
if (protocol === "v2" && event.Action === "assemble" && event.AudioGcsUri !== null) {
|
||||
if (effectiveProtocol === "v2" && event.Action === "assemble" && event.AudioGcsUri !== null) {
|
||||
const error = new Error("[handler] v2 assemble audio must be materialized from the manifest");
|
||||
error.name = "PLAN_PROTOCOL_UNSUPPORTED";
|
||||
throw error;
|
||||
@@ -254,14 +258,14 @@ function summarizeEvent(
|
||||
return {
|
||||
projectGcsUri: event.ProjectGcsUri,
|
||||
planOutputGcsPrefix: event.PlanOutputGcsPrefix,
|
||||
planProtocol: event.PlanProtocol ?? "v1",
|
||||
planProtocol: event.PlanProtocol ?? "v2",
|
||||
format: event.Config.format,
|
||||
fps: event.Config.fps,
|
||||
};
|
||||
case "renderChunk":
|
||||
return {
|
||||
planProtocol: event.PlanProtocol ?? "v1",
|
||||
...(event.PlanProtocol === "v2"
|
||||
planProtocol: event.PlanProtocol ?? "v2",
|
||||
...(event.PlanProtocol !== "v1"
|
||||
? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri }
|
||||
: { planGcsUri: event.PlanGcsUri }),
|
||||
chunkIndex: event.ChunkIndex,
|
||||
@@ -269,8 +273,8 @@ function summarizeEvent(
|
||||
};
|
||||
case "assemble":
|
||||
return {
|
||||
planProtocol: event.PlanProtocol ?? "v1",
|
||||
...(event.PlanProtocol === "v2"
|
||||
planProtocol: event.PlanProtocol ?? "v2",
|
||||
...(event.PlanProtocol !== "v1"
|
||||
? { planV2ManifestGcsUri: event.PlanV2ManifestGcsUri }
|
||||
: { planGcsUri: event.PlanGcsUri }),
|
||||
chunkCount: event.ChunkGcsUris.length,
|
||||
@@ -297,7 +301,7 @@ function primeChrome(deps?: HandlerDeps): void {
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
async function handlePlan(event: PlanEvent, deps?: HandlerDeps): Promise<PlanResultBody> {
|
||||
if (event.PlanProtocol === "v2") {
|
||||
if (event.PlanProtocol !== "v1") {
|
||||
return handlePlanV2(event, deps);
|
||||
}
|
||||
const started = Date.now();
|
||||
@@ -365,7 +369,7 @@ async function handlePlan(event: PlanEvent, deps?: HandlerDeps): Promise<PlanRes
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
async function handlePlanV2(
|
||||
event: Extract<PlanEvent, { PlanProtocol: "v2" }>,
|
||||
event: PlanV2Event,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<Extract<PlanResultBody, { PlanProtocol: "v2" }>> {
|
||||
const started = Date.now();
|
||||
@@ -418,7 +422,7 @@ async function handleRenderChunk(
|
||||
event: RenderChunkEvent,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<RenderChunkResultBody> {
|
||||
if (event.PlanProtocol === "v2") {
|
||||
if (event.PlanProtocol !== "v1") {
|
||||
return handleRenderChunkV2(event, deps);
|
||||
}
|
||||
const started = Date.now();
|
||||
@@ -475,7 +479,7 @@ async function handleRenderChunk(
|
||||
/** Materialize only this chunk's verified v2 dependencies before rendering. */
|
||||
// fallow-ignore-next-line complexity
|
||||
async function handleRenderChunkV2(
|
||||
event: Extract<RenderChunkEvent, { PlanProtocol: "v2" }>,
|
||||
event: RenderChunkV2Event,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<RenderChunkResultBody> {
|
||||
const started = Date.now();
|
||||
@@ -548,7 +552,7 @@ async function handleAssemble(
|
||||
event: AssembleEvent,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<AssembleResultBody> {
|
||||
if (event.PlanProtocol === "v2") {
|
||||
if (event.PlanProtocol !== "v1") {
|
||||
return handleAssembleV2(event, deps);
|
||||
}
|
||||
const started = Date.now();
|
||||
@@ -613,7 +617,7 @@ async function handleAssemble(
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
async function handleAssembleV2(
|
||||
event: Extract<AssembleEvent, { PlanProtocol: "v2" }>,
|
||||
event: AssembleV2Event,
|
||||
deps?: HandlerDeps,
|
||||
): Promise<AssembleResultBody> {
|
||||
const started = Date.now();
|
||||
@@ -781,12 +785,12 @@ function getEventGcsUris(event: PlanEvent | RenderChunkEvent | AssembleEvent): s
|
||||
case "plan":
|
||||
return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];
|
||||
case "renderChunk":
|
||||
return event.PlanProtocol === "v2"
|
||||
return event.PlanProtocol !== "v1"
|
||||
? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix, event.ChunkOutputGcsPrefix]
|
||||
: [event.PlanGcsUri, event.ChunkOutputGcsPrefix];
|
||||
case "assemble":
|
||||
return [
|
||||
...(event.PlanProtocol === "v2"
|
||||
...(event.PlanProtocol !== "v1"
|
||||
? [event.PlanV2ManifestGcsUri, event.PlanV2ArtifactGcsPrefix]
|
||||
: [event.PlanGcsUri]),
|
||||
...event.ChunkGcsUris,
|
||||
|
||||
@@ -7,8 +7,8 @@ const smoke = readFileSync(smokePath, "utf-8");
|
||||
const dockerfile = readFileSync(join(import.meta.dir, "../Dockerfile"), "utf-8");
|
||||
|
||||
describe("GCP smoke ownership and protocol safety", () => {
|
||||
it("defaults to v1 and requires an explicit v2 protocol argument", () => {
|
||||
expect(smoke).toContain('PROTOCOLS="${PROTOCOLS:-v1}"');
|
||||
it("defaults to v2 and retains explicit v1/v2 protocol arguments", () => {
|
||||
expect(smoke).toContain('PROTOCOLS="${PROTOCOLS:-v2}"');
|
||||
expect(smoke).toContain("--protocols)");
|
||||
expect(smoke).toContain("PlanProtocol: $protocol");
|
||||
expect(smoke).toContain("decodedFramesEqual");
|
||||
|
||||
@@ -77,8 +77,8 @@ describe("Cloud Workflows plan protocol routing", () => {
|
||||
expect(source.match(/max_retries: 4/g)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("keeps v1 as the default and rejects unknown protocols before plan", () => {
|
||||
expect(source).toContain('default(map.get(args, "PlanProtocol"), "v1")');
|
||||
it("defaults omitted protocol to v2 and rejects unknown protocols before plan", () => {
|
||||
expect(source).toContain('default(map.get(args, "PlanProtocol"), "v2")');
|
||||
expect(namedStep("selectPlanProtocol")).toMatchObject({
|
||||
next: "unsupportedPlanProtocol",
|
||||
});
|
||||
|
||||
@@ -26,9 +26,9 @@ main:
|
||||
- planOutputGcsPrefix: ${args.PlanOutputGcsPrefix}
|
||||
- outputGcsUri: ${args.OutputGcsUri}
|
||||
- config: ${args.Config}
|
||||
# Backward-compatible default. v2 is accepted only through an
|
||||
# explicit top-level PlanProtocol opt-in.
|
||||
- planProtocol: ${default(map.get(args, "PlanProtocol"), "v1")}
|
||||
# Plan v2 is the default. Explicit v1 remains available during the
|
||||
# deprecated monolithic-plan compatibility window.
|
||||
- planProtocol: ${default(map.get(args, "PlanProtocol"), "v2")}
|
||||
|
||||
# ── Plan (Activity A) ────────────────────────────────────────────────────
|
||||
- selectPlanProtocol:
|
||||
|
||||
Reference in New Issue
Block a user