cdk-nextjs 0.6.1 → 0.7.0
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/.jsii +2467 -2714
- package/.jsii.tabl.json +1 -1
- package/API.md +1725 -1909
- package/CLAUDE.md +2 -1
- package/README.md +429 -130
- package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +152 -170
- package/assets/lambdas/redeploy-stage/redeploy-stage.lambda/index.js +875 -0
- package/docs/adapter-runtime.md +185 -0
- package/docs/breaking-changes.md +687 -4
- package/docs/caching-guide.md +156 -112
- package/docs/cdk-nextjs-cfn-exec-policy.json +2 -1
- package/docs/development-guide.md +1 -1
- package/docs/harness-coverage.md +598 -0
- package/docs/init-cache-deployment.md +32 -1
- package/docs/next-build-output-guide.md +30 -20
- package/examples/README.md +5 -1
- package/lib/adapter/__fixtures__/app-playground-base-path.json +4926 -0
- package/lib/adapter/__fixtures__/app-playground.json +4917 -0
- package/lib/adapter/__fixtures__/pages-i18n.json +1728 -0
- package/lib/adapter/adapter.mjs +1548 -185
- package/lib/adapter/adapter.test.js +106 -0
- package/lib/adapter/aws-cache-store.d.ts +489 -0
- package/lib/adapter/aws-cache-store.js +1004 -0
- package/lib/adapter/aws-cache-store.test.d.ts +1 -0
- package/lib/adapter/aws-cache-store.test.js +361 -0
- package/lib/adapter/build-outputs.d.ts +80 -0
- package/lib/adapter/build-outputs.js +852 -0
- package/lib/adapter/build-outputs.test.d.ts +1 -0
- package/lib/adapter/build-outputs.test.js +861 -0
- package/lib/adapter/cache-handler.d.ts +9 -2
- package/lib/adapter/cache-handler.js +72 -96
- package/lib/adapter/cache-handler.mjs +18956 -17774
- package/lib/adapter/cache-handler.test.js +263 -31
- package/lib/adapter/cache-kinds.d.ts +32 -0
- package/lib/adapter/cache-kinds.js +35 -0
- package/lib/adapter/cache-kinds.test.d.ts +1 -0
- package/lib/adapter/cache-kinds.test.js +69 -0
- package/lib/adapter/cache-utils.d.ts +90 -0
- package/lib/adapter/cache-utils.js +199 -4
- package/lib/adapter/cache-utils.test.d.ts +1 -0
- package/lib/adapter/cache-utils.test.js +179 -0
- package/lib/adapter/function-groups.d.ts +215 -0
- package/lib/adapter/function-groups.js +1099 -0
- package/lib/adapter/function-groups.test.d.ts +1 -0
- package/lib/adapter/function-groups.test.js +743 -0
- package/lib/adapter/init-cache.d.ts +6 -0
- package/lib/adapter/init-cache.js +245 -0
- package/lib/adapter/init-cache.test.d.ts +1 -0
- package/lib/adapter/init-cache.test.js +137 -0
- package/lib/adapter/local-file-cache-handler.d.ts +23 -0
- package/lib/adapter/local-file-cache-handler.js +52 -1
- package/lib/adapter/memory-cache-handler.d.ts +26 -17
- package/lib/adapter/memory-cache-handler.js +32 -50
- package/lib/adapter/memory-cache-handler.test.js +105 -85
- package/lib/adapter/s3-cache-handler.d.ts +179 -40
- package/lib/adapter/s3-cache-handler.js +807 -277
- package/lib/adapter/s3-cache-handler.test.js +1186 -31
- package/lib/adapter/use-cache-common.d.ts +90 -0
- package/lib/adapter/use-cache-common.js +250 -0
- package/lib/adapter/use-cache-common.test.d.ts +1 -0
- package/lib/adapter/use-cache-common.test.js +646 -0
- package/lib/adapter/use-cache-default-handler.d.ts +24 -0
- package/lib/adapter/use-cache-default-handler.js +90 -0
- package/lib/adapter/use-cache-default-handler.mjs +38645 -0
- package/lib/adapter/use-cache-handlers.test.d.ts +1 -0
- package/lib/adapter/use-cache-handlers.test.js +349 -0
- package/lib/adapter/use-cache-remote-handler.d.ts +26 -0
- package/lib/adapter/use-cache-remote-handler.js +186 -0
- package/lib/{image-optimization/handler.mjs → adapter/use-cache-remote-handler.mjs} +33572 -38798
- package/lib/generated-structs/OptionalNextjsApiProps.d.ts +59 -0
- package/lib/generated-structs/OptionalNextjsApiProps.js +3 -0
- package/lib/generated-structs/OptionalNextjsCacheProps.d.ts +15 -6
- package/lib/generated-structs/OptionalNextjsCacheProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsContainersProps.d.ts +25 -11
- package/lib/generated-structs/OptionalNextjsContainersProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +31 -9
- package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsPostDeployProps.d.ts +13 -0
- package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
- package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +11 -2
- package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.js +1 -1
- package/lib/index.d.ts +6 -7
- package/lib/index.js +2 -4
- package/lib/lambdas/post-deploy/post-deploy.lambda.js +3 -2
- package/lib/lambdas/post-deploy/prune-cache-bucket.d.ts +7 -3
- package/lib/lambdas/post-deploy/prune-cache-bucket.js +57 -71
- package/lib/lambdas/post-deploy/prune-cache-bucket.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-cache-bucket.test.js +114 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.js +23 -31
- package/lib/lambdas/post-deploy/prune-revalidation-table.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +71 -0
- package/lib/lambdas/post-deploy/prune-s3.d.ts +19 -3
- package/lib/lambdas/post-deploy/prune-s3.js +54 -15
- package/lib/lambdas/post-deploy/prune-s3.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-s3.test.js +255 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage-function.d.ts +13 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage-function.js +55 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.d.ts +24 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.js +68 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.d.ts +20 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.js +24 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.test.d.ts +1 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.test.js +93 -0
- package/lib/nextjs-api.d.ts +107 -18
- package/lib/nextjs-api.js +349 -39
- package/lib/nextjs-api.test.js +582 -1
- package/lib/nextjs-build/global-containers.Dockerfile +15 -10
- package/lib/nextjs-build/nextjs-build.d.ts +339 -63
- package/lib/nextjs-build/nextjs-build.js +684 -327
- package/lib/nextjs-build/nextjs-build.test.d.ts +1 -0
- package/lib/nextjs-build/nextjs-build.test.js +412 -0
- package/lib/nextjs-build/patch-fetch.js +1 -1
- package/lib/nextjs-build/patch-fetch.test.js +166 -11
- package/lib/nextjs-build/regional-containers.Dockerfile +17 -10
- package/lib/nextjs-cache.d.ts +82 -4
- package/lib/nextjs-cache.js +186 -10
- package/lib/nextjs-cache.test.js +176 -1
- package/lib/nextjs-compute/nextjs-compute-base-props.d.ts +21 -8
- package/lib/nextjs-compute/nextjs-compute-base-props.js +41 -1
- package/lib/nextjs-compute/nextjs-compute-base-props.test.d.ts +1 -0
- package/lib/nextjs-compute/nextjs-compute-base-props.test.js +44 -0
- package/lib/nextjs-compute/nextjs-containers.d.ts +19 -0
- package/lib/nextjs-compute/nextjs-containers.js +57 -20
- package/lib/nextjs-compute/nextjs-functions.d.ts +122 -7
- package/lib/nextjs-compute/nextjs-functions.js +113 -67
- package/lib/nextjs-compute/nextjs-functions.test.d.ts +1 -0
- package/lib/nextjs-compute/nextjs-functions.test.js +99 -0
- package/lib/nextjs-distribution.d.ts +194 -23
- package/lib/nextjs-distribution.js +535 -62
- package/lib/nextjs-distribution.test.d.ts +1 -0
- package/lib/nextjs-distribution.test.js +697 -0
- package/lib/nextjs-post-deploy.d.ts +23 -3
- package/lib/nextjs-post-deploy.js +14 -10
- package/lib/nextjs-post-deploy.test.d.ts +1 -0
- package/lib/nextjs-post-deploy.test.js +34 -0
- package/lib/nextjs-static-assets.d.ts +23 -4
- package/lib/nextjs-static-assets.js +29 -6
- package/lib/nextjs-static-assets.test.js +20 -1
- package/lib/root-constructs/nextjs-base-construct.d.ts +105 -45
- package/lib/root-constructs/nextjs-base-construct.js +144 -34
- package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
- package/lib/root-constructs/nextjs-global-containers.js +20 -70
- package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
- package/lib/root-constructs/nextjs-global-functions.js +52 -69
- package/lib/root-constructs/nextjs-regional-containers.d.ts +25 -5
- package/lib/root-constructs/nextjs-regional-containers.js +17 -15
- package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
- package/lib/root-constructs/nextjs-regional-functions.js +8 -25
- package/lib/runtime/api-gateway-path.d.ts +34 -0
- package/lib/runtime/api-gateway-path.js +71 -0
- package/lib/runtime/api-gateway-path.test.d.ts +1 -0
- package/lib/runtime/api-gateway-path.test.js +65 -0
- package/lib/runtime/case-redirect.d.ts +22 -0
- package/lib/runtime/case-redirect.js +63 -0
- package/lib/runtime/case-redirect.test.d.ts +1 -0
- package/lib/runtime/case-redirect.test.js +32 -0
- package/lib/runtime/core.d.ts +234 -0
- package/lib/runtime/core.js +1213 -0
- package/lib/runtime/core.test.d.ts +1 -0
- package/lib/runtime/core.test.js +1678 -0
- package/lib/runtime/deployment-root.d.ts +13 -0
- package/lib/runtime/deployment-root.js +29 -0
- package/lib/runtime/dispatch.d.ts +331 -0
- package/lib/runtime/dispatch.js +751 -0
- package/lib/runtime/dispatch.test.d.ts +1 -0
- package/lib/runtime/dispatch.test.js +853 -0
- package/lib/runtime/entrypoints.d.ts +57 -0
- package/lib/runtime/entrypoints.js +115 -0
- package/lib/runtime/http/abort-signal.d.ts +6 -0
- package/lib/runtime/http/abort-signal.js +19 -0
- package/lib/runtime/http/abort-signal.test.d.ts +1 -0
- package/lib/runtime/http/abort-signal.test.js +49 -0
- package/lib/runtime/http/node-server.d.ts +38 -0
- package/lib/runtime/http/node-server.js +99 -0
- package/lib/runtime/http/node-server.test.d.ts +1 -0
- package/lib/runtime/http/node-server.test.js +96 -0
- package/lib/runtime/http/node-sink.d.ts +16 -0
- package/lib/runtime/http/node-sink.js +48 -0
- package/lib/runtime/http/node-sink.test.d.ts +1 -0
- package/lib/runtime/http/node-sink.test.js +66 -0
- package/lib/runtime/http/request.d.ts +62 -0
- package/lib/runtime/http/request.js +134 -0
- package/lib/runtime/http/request.test.d.ts +1 -0
- package/lib/runtime/http/request.test.js +103 -0
- package/lib/runtime/http/response.d.ts +77 -0
- package/lib/runtime/http/response.js +229 -0
- package/lib/runtime/http/response.test.d.ts +1 -0
- package/lib/runtime/http/response.test.js +178 -0
- package/lib/runtime/http/sink.d.ts +73 -0
- package/lib/runtime/http/sink.js +188 -0
- package/lib/runtime/http/sink.test.d.ts +1 -0
- package/lib/runtime/http/sink.test.js +257 -0
- package/lib/runtime/image-utils.d.ts +78 -0
- package/lib/runtime/image-utils.js +129 -0
- package/lib/runtime/image-utils.test.d.ts +1 -0
- package/lib/runtime/image-utils.test.js +237 -0
- package/lib/runtime/image.d.ts +76 -0
- package/lib/runtime/image.js +296 -0
- package/lib/runtime/image.test.d.ts +1 -0
- package/lib/runtime/image.test.js +405 -0
- package/lib/runtime/lambda-event.d.ts +4 -0
- package/lib/runtime/lambda-event.js +113 -0
- package/lib/runtime/lambda-event.test.d.ts +1 -0
- package/lib/runtime/lambda-event.test.js +119 -0
- package/lib/runtime/lambda.mjs +41595 -0
- package/lib/runtime/load-module.d.ts +8 -0
- package/lib/runtime/load-module.js +35 -0
- package/lib/runtime/manifest.d.ts +215 -0
- package/lib/runtime/manifest.js +118 -0
- package/lib/runtime/middleware.d.ts +43 -0
- package/lib/runtime/middleware.js +113 -0
- package/lib/runtime/middleware.test.d.ts +1 -0
- package/lib/runtime/middleware.test.js +259 -0
- package/lib/runtime/next-modules.d.ts +55 -0
- package/lib/runtime/next-modules.js +124 -0
- package/lib/runtime/next-modules.test.d.ts +1 -0
- package/lib/runtime/next-modules.test.js +34 -0
- package/lib/runtime/public-files.d.ts +32 -0
- package/lib/runtime/public-files.js +117 -0
- package/lib/runtime/public-files.test.d.ts +1 -0
- package/lib/runtime/public-files.test.js +97 -0
- package/lib/runtime/server.mjs +41588 -0
- package/lib/runtime/static-files.d.ts +50 -0
- package/lib/runtime/static-files.js +266 -0
- package/lib/runtime/static-files.test.d.ts +1 -0
- package/lib/runtime/static-files.test.js +118 -0
- package/lib/runtime/util.d.ts +28 -0
- package/lib/runtime/util.js +79 -0
- package/lib/utils/base-path.d.ts +123 -11
- package/lib/utils/base-path.js +276 -39
- package/lib/utils/base-path.test.js +275 -50
- package/lib/utils/get-architecture.d.ts +7 -8
- package/lib/utils/get-architecture.js +12 -13
- package/lib/utils/zip-directory.d.ts +14 -0
- package/lib/utils/zip-directory.js +121 -0
- package/lib/utils/zip-directory.test.d.ts +1 -0
- package/lib/utils/zip-directory.test.js +99 -0
- package/package.json +22 -3
- package/scripts/capture-adapter-fixture.mjs +325 -0
- package/scripts/e2e-deploy.sh +398 -0
- package/scripts/e2e-harness/README.md +707 -0
- package/scripts/e2e-harness/app.js +370 -0
- package/scripts/e2e-harness/common.sh +249 -0
- package/scripts/e2e-harness/screen.mjs +302 -0
- package/scripts/e2e-harness/stage-proxy.mjs +145 -0
- package/scripts/e2e-harness/type-filters.mjs +47 -0
- package/scripts/e2e-logs.sh +75 -0
- package/scripts/e2e-offline.sh +91 -0
- package/scripts/e2e-sweep.sh +370 -0
- package/scripts/e2e-warm.sh +114 -0
- package/scripts/zero-config-build.mjs +418 -0
- package/docs/1k-concurrent-users-executions.png +0 -0
- package/docs/plans/adapter-runtime-release.md +0 -362
- package/docs/plans/phase-0-image-function-opt-in.md +0 -295
- package/lib/generated-structs/OptionalDockerImageFunctionProps.d.ts +0 -425
- package/lib/generated-structs/OptionalDockerImageFunctionProps.js +0 -3
- package/lib/image-optimization/handler-utils.d.ts +0 -34
- package/lib/image-optimization/handler-utils.js +0 -83
- package/lib/image-optimization/handler-utils.test.js +0 -178
- package/lib/nextjs-build/functions.Dockerfile +0 -31
- package/lib/nextjs-compute/nextjs-image-function.d.ts +0 -53
- package/lib/nextjs-compute/nextjs-image-function.js +0 -59
- package/lib/utils/experimental-flags.d.ts +0 -24
- package/lib/utils/experimental-flags.js +0 -30
- /package/lib/{image-optimization/handler-utils.test.d.ts → adapter/adapter.test.d.ts} +0 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
# Next.js compatibility harness
|
|
2
|
+
|
|
3
|
+
Runs [vercel/next.js's own e2e suite][harness] against a real cdk-nextjs
|
|
4
|
+
deployment. It is a better correctness signal than any fixture app we would
|
|
5
|
+
write, because the tests were written by the people who define the behavior.
|
|
6
|
+
|
|
7
|
+
`examples/e2e-tests/` remains the per-commit gate on all four `NextjsType`s. This
|
|
8
|
+
is the scheduled one — every sixth day of the month at 14:00 UTC, so the day of
|
|
9
|
+
the week drifts — on `NextjsGlobalFunctions` only. The other three run by
|
|
10
|
+
`workflow_dispatch` (its `nextjs_type` input) or by hand; see "Running on the
|
|
11
|
+
other `NextjsType`s".
|
|
12
|
+
|
|
13
|
+
What has actually been run, what failed, and whether each failure is a bug or
|
|
14
|
+
acceptable: [`docs/harness-coverage.md`](../../docs/harness-coverage.md).
|
|
15
|
+
|
|
16
|
+
## Pieces
|
|
17
|
+
|
|
18
|
+
| Path | Role |
|
|
19
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
20
|
+
| `scripts/e2e-deploy.sh` | `NEXT_TEST_DEPLOY_SCRIPT_PATH`. Installs, builds through the adapter, deploys, invalidates, prints the URL. |
|
|
21
|
+
| `scripts/e2e-logs.sh` | `NEXT_TEST_DEPLOY_LOGS_SCRIPT_PATH`. Replays the build markers and the build log — and, under `HARNESS_VERBOSE_LOGS=1`, the deploy log and the Lambda's CloudWatch tail. |
|
|
22
|
+
| `scripts/e2e-warm.sh` | Creates this shard's shared stack before the suite starts, so no test file pays for it. Run it first. |
|
|
23
|
+
| `scripts/e2e-sweep.sh` | Deletes orphaned harness stacks, and a shard's own after its run. Dry run unless `--apply`. |
|
|
24
|
+
| `scripts/e2e-harness/app.js` | The CDK app the deploy script deploys. |
|
|
25
|
+
| `scripts/e2e-harness/stage-proxy.mjs` | Localhost front door for a `NextjsRegionalFunctions` stack: puts the stage back. Regional only. |
|
|
26
|
+
| `scripts/e2e-harness/common.sh` | Shared file names, stack naming, output reads, and the tag check that gates every delete. |
|
|
27
|
+
| `.github/actions/build-nextjs` | Checks out and builds vercel/next.js. The cache-miss path, shared by the `nextjs` job and a shard's fallback. |
|
|
28
|
+
| `test/deploy-tests-manifest.json` | Which next.js test files run (`NEXT_EXTERNAL_TESTS_FILTERS`). |
|
|
29
|
+
| `.github/workflows/e2e-harness.yml` | Every six days + `workflow_dispatch`. A matrix of `shard_total` jobs, one stack each. |
|
|
30
|
+
|
|
31
|
+
## One shared stack, not one per test file
|
|
32
|
+
|
|
33
|
+
The harness creates an isolated app per test file and runs the deploy script with
|
|
34
|
+
`cwd` set to it, so there is no way to deploy once and point the whole suite at
|
|
35
|
+
one deployment — every file genuinely has different code to ship. What there is a
|
|
36
|
+
way to do is ship it into infrastructure that already exists.
|
|
37
|
+
|
|
38
|
+
Every test file in a run deploys into the same stack (`hrns-shared` by default,
|
|
39
|
+
`hrns-shard-<n>` under the workflow's matrix — see "Sharding" below) with
|
|
40
|
+
`cdk deploy --hotswap-fallback`. Creating and propagating the CloudFront
|
|
41
|
+
distribution costs ~4 minutes once per stack; every test file reuses it. That is
|
|
42
|
+
the whole saving, and it is most of the cost of a run.
|
|
43
|
+
|
|
44
|
+
`scripts/e2e-warm.sh` pays that cost **before `run-tests.js` starts**, and
|
|
45
|
+
is not optional. The harness calls `createNext` inside jest's `beforeAll`, so a
|
|
46
|
+
stack create is charged against `NEXT_E2E_TEST_TIMEOUT` (240s in CI) and the
|
|
47
|
+
first test file fails wholesale, then passes on retry once the stack exists.
|
|
48
|
+
Measured: unwarmed, the first file failed all 11 tests at 242s and passed on
|
|
49
|
+
retry 1 in 213s; against an already-created stack, a file passes on its first
|
|
50
|
+
attempt in 107s. Retries make that survivable, not correct - it spends a retry
|
|
51
|
+
the next real failure needs, and a create slower than two retries goes red.
|
|
52
|
+
|
|
53
|
+
Measured against two different built apps (`cdk synth` twice into the same stack
|
|
54
|
+
name, then diffing the templates), exactly five resources differ between two
|
|
55
|
+
fixtures:
|
|
56
|
+
|
|
57
|
+
| Resource | Changed properties | Hotswappable |
|
|
58
|
+
| ------------------------------- | ----------------------------------- | ------------ |
|
|
59
|
+
| `AWS::Lambda::Function` | `Code`, `Environment` | yes |
|
|
60
|
+
| `Custom::CDKBucketDeployment` | `SourceObjectKeys` | yes |
|
|
61
|
+
| `Custom::CDKBucketDeployment` | `SourceObjectKeys`, `UserMetadata` | yes |
|
|
62
|
+
| `AWS::S3::Bucket` | `Tags` | **no** |
|
|
63
|
+
| `AWS::CloudFront::Distribution` | `DistributionConfig.CacheBehaviors` | **no** |
|
|
64
|
+
|
|
65
|
+
Both of the un-hotswappable rows fire in practice, and the bucket's `Tags` fired
|
|
66
|
+
far more often than the distribution. Measured over batch 9, a 23-file run with
|
|
67
|
+
22 deploy invocations:
|
|
68
|
+
|
|
69
|
+
| Deploy path | Count | Cost |
|
|
70
|
+
| --------------------------------------- | ----- | ----- |
|
|
71
|
+
| fallback, cache bucket `Tags` rejected | 19 | ~110s |
|
|
72
|
+
| fallback, `DistributionConfig` rejected | 3 | ~110s |
|
|
73
|
+
| hotswap | 0 | ~52s |
|
|
74
|
+
|
|
75
|
+
- The **cache bucket's `Tags`** were the dominant cost, at ~110s of a ~157s median
|
|
76
|
+
green file. `BucketDeployment` unconditionally tags its destination bucket
|
|
77
|
+
`aws-cdk:cr-owned:<destinationKeyPrefix>:<hash>`, and the init cache deployment
|
|
78
|
+
passed the build ID as its `destinationKeyPrefix` — so the tag key, and
|
|
79
|
+
therefore the bucket's `Tags`, changed on every fixture. The hotswap attempt
|
|
80
|
+
itself took 0.77s before falling back.
|
|
81
|
+
|
|
82
|
+
Fixed: the build ID is now part of the staged asset's own paths and the
|
|
83
|
+
deployment has no `destinationKeyPrefix` (`src/nextjs-cache.ts`). The S3 keys
|
|
84
|
+
are unchanged; the tag key is constant. An earlier 13-file measurement recorded
|
|
85
|
+
in this file claimed the tag never blocked a deploy — it was wrong, and batch 9
|
|
86
|
+
is the run that showed it.
|
|
87
|
+
|
|
88
|
+
- The **distribution** blocks the remainder. Its cache behaviors change when a
|
|
89
|
+
fixture's `public/` directory differs from the previous one's, since `public/`
|
|
90
|
+
entries become behaviors (`src/nextjs-distribution.ts`). CloudFront then has to
|
|
91
|
+
propagate, which is the irreducibly expensive case: 8-10 minutes per update in
|
|
92
|
+
the 2026-09-28 Global Functions run, not the ~110s above. That is why the
|
|
93
|
+
workflow gives the Global types a `NEXT_E2E_TEST_TIMEOUT` of 900000.
|
|
94
|
+
|
|
95
|
+
Either way the shared stack wins: even a full CloudFormation update that leaves
|
|
96
|
+
the distribution alone costs ~110s, against ~4 minutes for a stack of its own plus
|
|
97
|
+
a slow delete.
|
|
98
|
+
|
|
99
|
+
What that means for the timeout, because it is the single most common way to
|
|
100
|
+
misread a run: a ~52s hotswap is **not** the whole `beforeAll`. Isolating the
|
|
101
|
+
fixture, `pnpm install`, `next build` and the CloudFront invalidation wait are
|
|
102
|
+
charged against `NEXT_E2E_TEST_TIMEOUT` too, and the sum passes 120s for most
|
|
103
|
+
files even on the fast path. See "Running it locally".
|
|
104
|
+
|
|
105
|
+
What the shared stack is paid for in:
|
|
106
|
+
|
|
107
|
+
- **Test files must be serialized within a stack** (`run-tests.js -c 1`). Two
|
|
108
|
+
concurrent deploys into one stack would race. Parallelism comes from more
|
|
109
|
+
stacks — see "Sharding" below.
|
|
110
|
+
- **Nothing may delete the stack between files**, or the next file pays the
|
|
111
|
+
create again. So the harness gets no `NEXT_TEST_CLEANUP_SCRIPT_PATH` (it is
|
|
112
|
+
optional); `e2e-sweep.sh --apply --shared` deletes the stack once, after the
|
|
113
|
+
run. The workflow does this in an `always()` step; a local run has to
|
|
114
|
+
do it by hand.
|
|
115
|
+
- **Nothing may leak between files.** Server cache entries are keyed by
|
|
116
|
+
`CDK_NEXTJS_BUILD_ID` (`src/adapter/s3-cache-handler.ts`), which differs per
|
|
117
|
+
file and hotswaps with the function. The edge cache is invalidated by
|
|
118
|
+
`e2e-deploy.sh` before it reports the URL — which it has to do itself, since a
|
|
119
|
+
hotswap never runs CloudFormation and therefore never runs the post-deploy
|
|
120
|
+
custom resource that normally invalidates. `app.js` pins that resource's
|
|
121
|
+
properties for the same reason: its default `buildId` and invalidation caller
|
|
122
|
+
reference change on every synth, and a changed custom-resource property is not
|
|
123
|
+
hotswappable, so either one alone would drag the deploy back through
|
|
124
|
+
CloudFormation. The pin also drops the bucket and table names and the
|
|
125
|
+
`basePath`-derived key prefix, which makes the resource a no-op: a basePath
|
|
126
|
+
fixture following one without still forces a CloudFormation update, and an
|
|
127
|
+
Update that could prune would delete every `<buildId>/` prefix but
|
|
128
|
+
`"harness"` - the cache the fixture just seeded.
|
|
129
|
+
|
|
130
|
+
`test/deploy-tests-manifest.json` still lists test files explicitly rather than
|
|
131
|
+
taking next.js's `test/e2e/**` include rule, and this still runs on a schedule
|
|
132
|
+
rather than per-commit. Widen either deliberately - and only with files you have watched
|
|
133
|
+
pass, since a file can be unbuildable rather than merely failing (see below).
|
|
134
|
+
|
|
135
|
+
To debug a single file, or run two _suites_ at once, give each its own shared
|
|
136
|
+
stack with `HARNESS_SHARED_STACK_SUFFIX` (`dev-$USER`, say) — see "Running two
|
|
137
|
+
of them at once locally" — and delete it afterwards with
|
|
138
|
+
`e2e-sweep.sh --apply --shared` under the same suffix.
|
|
139
|
+
|
|
140
|
+
## Sharding: one stack per shard
|
|
141
|
+
|
|
142
|
+
A stack holds one app at a time, so the only way to run two test files at once is
|
|
143
|
+
to give them two stacks. `HARNESS_SHARED_STACK_SUFFIX` is that knob, and
|
|
144
|
+
`.github/workflows/e2e-harness.yml` turns it into a matrix: `shard_total` jobs
|
|
145
|
+
(10 by default), each with a shared stack named `hrns-shard-<n>` and each running
|
|
146
|
+
its own slice of the file list.
|
|
147
|
+
|
|
148
|
+
The slice comes from next.js's own `run-tests.js -g <n>/<N>`, which splits
|
|
149
|
+
**after** `NEXT_EXTERNAL_TESTS_FILTERS` has been applied — `run-tests.js` filters
|
|
150
|
+
the glob, dedupes, _then_ groups — so the shards partition
|
|
151
|
+
`test/deploy-tests-manifest.json`, not the whole of `test/e2e/**`. With no
|
|
152
|
+
`KV_REST_API_URL`/`KV_REST_API_TOKEN` configured (we have none) it warns and falls
|
|
153
|
+
back to round-robin over that list, which is deterministic and near enough: cost
|
|
154
|
+
per file here is dominated by build-and-deploy, which barely varies, not by how
|
|
155
|
+
long the tests take. If it ever stops being near enough, `run-tests.js` reads a
|
|
156
|
+
`test-timings.json` from its own cwd before it tries KV, and a `--timings` run
|
|
157
|
+
prints per-file durations to build one from.
|
|
158
|
+
|
|
159
|
+
Each shard is self-contained, which is what makes this safe:
|
|
160
|
+
|
|
161
|
+
- `e2e-warm.sh` warms _its_ stack, so the ~4-minute distribution create is paid
|
|
162
|
+
once per shard but in parallel — the same ~4 minutes of wall clock however many
|
|
163
|
+
shards there are.
|
|
164
|
+
- `e2e-sweep.sh --apply --shared` in an `always()` step deletes _its_ stack, since
|
|
165
|
+
`--shared` resolves the same suffix. Nothing lowers the age floor account-wide,
|
|
166
|
+
so one shard finishing early cannot delete another's stack out from under it.
|
|
167
|
+
- `fail-fast: false`, because one shard's failure says nothing about another's and
|
|
168
|
+
cancelling the others would leave their stacks to the next run's sweep - up
|
|
169
|
+
to six days later - instead of to their own cleanup step.
|
|
170
|
+
- `-c 1` stays mandatory _within_ a shard. Sharding adds stacks; it does not make
|
|
171
|
+
one stack safe to deploy into twice at once.
|
|
172
|
+
|
|
173
|
+
What sharding does **not** make cheaper is **total AWS spend**: the same number of
|
|
174
|
+
deploys happen, plus N-1 extra distribution creates and deletes. It is wall clock
|
|
175
|
+
that improves, not cost. Nor does it divide the next.js build — see below.
|
|
176
|
+
|
|
177
|
+
Two runs must never overlap, because they would reuse the same shard names; the
|
|
178
|
+
workflow's `concurrency` group is what guarantees that, and it queues rather than
|
|
179
|
+
cancels. It is one group for every type, not one per type: each run's
|
|
180
|
+
account-wide sweep could otherwise delete another type's idle-looking shard stack
|
|
181
|
+
as that run warms into it. GitHub keeps only one _pending_ run per group, so
|
|
182
|
+
dispatch a second type once the first has started, not while one is queued. A
|
|
183
|
+
pending run that gets cancelled that way, the scheduled one included, loses no
|
|
184
|
+
sweep: every run sweeps, not just the scheduled one.
|
|
185
|
+
|
|
186
|
+
## Caching the next.js build
|
|
187
|
+
|
|
188
|
+
Every shard needs a _built_ vercel/next.js checkout — `run-tests.js` and
|
|
189
|
+
`test/lib` are repo files, and the tests run against `packages/next/dist`. That is
|
|
190
|
+
a fixed per-shard cost which sharding **multiplies rather than divides**, and once
|
|
191
|
+
the test time is divided by ten it is the largest thing left in a run.
|
|
192
|
+
|
|
193
|
+
The shape of the fix is forced by two facts:
|
|
194
|
+
|
|
195
|
+
- **The shards start simultaneously.** Matrix jobs are concurrent, so within one
|
|
196
|
+
run no shard can benefit from another's cache save — all ten would miss
|
|
197
|
+
together, all ten would build, and nine saves would lose the race harmlessly.
|
|
198
|
+
A cache on the shard alone therefore does nothing for the run that writes it.
|
|
199
|
+
So the build belongs in a **`nextjs` job ahead of the matrix**, which every
|
|
200
|
+
shard then restores from. On a hit that job is a `lookup-only` restore and
|
|
201
|
+
nothing else (~15s — it does not download, since the shards are about to). On a
|
|
202
|
+
miss it builds and saves, costing the run what it used to cost _every_ shard.
|
|
203
|
+
- **GitHub evicts a cache entry not _accessed_ in 7 days.** A weekly schedule sits
|
|
204
|
+
exactly on that boundary, and past it as soon as a scheduled run is delayed
|
|
205
|
+
under load, which they routinely are. So the schedule is **every six days**
|
|
206
|
+
instead: `0 14 */6 * *`, by day-of-month rather than day-of-week. Every run's
|
|
207
|
+
shards restore the entry, so a cadence strictly under 7 days keeps it warm with
|
|
208
|
+
no extra machinery.
|
|
209
|
+
|
|
210
|
+
That second point is why the cron looks the way it does, and it is load-bearing.
|
|
211
|
+
`*/6` expands to days 1, 7, 13, 19, 25 and **31** — standard cron steps from the
|
|
212
|
+
range's start, so the 31st is included, which is what makes the month boundary a
|
|
213
|
+
1-day gap rather than a long one. Enumerated over 2026–2029 the gaps are 6 days
|
|
214
|
+
within a month and 1, 4 or 5 across a boundary; never 7. Dropping the 31st, or
|
|
215
|
+
reaching for `*/7`, puts the boundary straight back.
|
|
216
|
+
|
|
217
|
+
Two costs, both accepted deliberately: ~67 runs a year instead of 52, and a run
|
|
218
|
+
day that **drifts through the week** — so read a run's results whenever it lands
|
|
219
|
+
rather than on a fixed morning. In exchange there is no keep-alive job and no
|
|
220
|
+
schedule-gated `if:` on anything.
|
|
221
|
+
|
|
222
|
+
Eviction is in any case a speed problem and not a correctness one: if the entry is
|
|
223
|
+
gone, the `nextjs` job rebuilds it once and the shards still find it. The cadence
|
|
224
|
+
buys the fast path, not the working one.
|
|
225
|
+
|
|
226
|
+
`.github/actions/build-nextjs` holds the build itself, and only the `nextjs` job
|
|
227
|
+
runs it. A shard that misses fails its restore (`fail-on-cache-miss`) rather than rebuilding:
|
|
228
|
+
rebuilding would run next.js's install and build scripts in the job that holds
|
|
229
|
+
`id-token: write`. Re-run the workflow and the `nextjs` job rebuilds the entry.
|
|
230
|
+
|
|
231
|
+
What is cached is the whole `nextjs` directory plus `~/.cache/ms-playwright`,
|
|
232
|
+
keyed on the next.js ref and the runner image and node major its `node_modules`
|
|
233
|
+
were installed against (`plan` computes the key, so every job agrees on it; bump
|
|
234
|
+
the `v1` in it to discard every entry). Two things to know:
|
|
235
|
+
|
|
236
|
+
- **A repository gets 10 GB of cache in total, evicted least-recently-used.** An
|
|
237
|
+
entry this size can displace every other workflow's. The `nextjs` job prints
|
|
238
|
+
`du -sh` of what it is about to save for exactly that reason — the number worth
|
|
239
|
+
comparing against is whatever the other workflows are using at the time.
|
|
240
|
+
- **The Playwright _browser_ is cached; its system libraries are not.** Those are
|
|
241
|
+
apt packages outside any cacheable path, so
|
|
242
|
+
`playwright install --with-deps chromium` still runs in each shard. It is a
|
|
243
|
+
no-op for the download on a hit.
|
|
244
|
+
|
|
245
|
+
A `workflow_dispatch` from a branch writes to that branch's own cache scope, so
|
|
246
|
+
the first dispatched run on a new branch pays the build once. It can still _read_
|
|
247
|
+
the default branch's entry, so this only bites when the ref differs too.
|
|
248
|
+
|
|
249
|
+
## Running two of them at once locally
|
|
250
|
+
|
|
251
|
+
Set a suffix per session (`dev-$USER`, then `dev-$USER-2` for a second) and the
|
|
252
|
+
same rules apply:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
HARNESS_SHARED_STACK_SUFFIX=dev-$USER ADAPTER_DIR=$PWD ./scripts/e2e-warm.sh
|
|
256
|
+
# ... and export it for run-tests.js too, then afterwards:
|
|
257
|
+
HARNESS_SHARED_STACK_SUFFIX=dev-$USER ./scripts/e2e-sweep.sh --apply --shared
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
What does _not_ parallelize as easily is the next.js checkout. `run-tests.js`
|
|
261
|
+
writes its results and timings into its own cwd, and `e2e-offline.sh` stages the
|
|
262
|
+
app inside the checkout, so two concurrent sessions want two checkouts (`NEXTJS_DIR`
|
|
263
|
+
points `e2e-offline.sh` at one; `run-tests.js` has to be run from one). Two
|
|
264
|
+
sessions on _different_ fixtures can share a checkout through `e2e-offline.sh` —
|
|
265
|
+
its app directory is named after the fixture and it takes a port argument — but
|
|
266
|
+
two on the same fixture will `rm -rf` each other's.
|
|
267
|
+
|
|
268
|
+
Nor does the _fixing_ parallelize the way the running does. A fix lands in the
|
|
269
|
+
adapter and the runtime (`src/adapter/`, `src/runtime/`), which is where every
|
|
270
|
+
harness failure leads, so two sessions each on their own branch will conflict far
|
|
271
|
+
more often than two on separate features would. Parallelize the _diagnosis_ —
|
|
272
|
+
`e2e-offline.sh` is read-only against this repo, and several sessions can bisect
|
|
273
|
+
several failures at once — and land the fixes one at a time, each rebased on the
|
|
274
|
+
last. A session that must have its own working tree wants a `git worktree` _and_
|
|
275
|
+
its own next.js checkout _and_ its own stack suffix; three of those is usually the
|
|
276
|
+
point at which running one at a time is the cheaper answer.
|
|
277
|
+
|
|
278
|
+
## Why the schedule runs `NextjsGlobalFunctions`
|
|
279
|
+
|
|
280
|
+
All four types run (see "Running on the other `NextjsType`s"); the schedule picks
|
|
281
|
+
one. The harness builds every request URL as `new URL(path, deploymentUrl)` —
|
|
282
|
+
`getFullUrl` in `test/lib/next-test-utils.ts` assigns `pathname` outright — so any
|
|
283
|
+
prefix in the deployment URL is dropped. `NextjsRegionalFunctions`'s API Gateway
|
|
284
|
+
REST URL always ends in `/<stage>`, so it runs behind `stage-proxy.mjs`, a local
|
|
285
|
+
proxy that puts the stage back (see "Running on `NextjsRegionalFunctions`") — a
|
|
286
|
+
second front door, not the one users deploy. A CloudFront distribution is served
|
|
287
|
+
at the origin root.
|
|
288
|
+
|
|
289
|
+
It is also the front door the suite was written for. `NEXT_TEST_MODE=deploy` is
|
|
290
|
+
the mode Vercel validates edge-fronted deployments with, so its tests tolerate a
|
|
291
|
+
CDN in front of them, and the ones that cannot are already gated out of deploy
|
|
292
|
+
mode upstream. Serving through CloudFront means `_next/static` and `public/` are
|
|
293
|
+
answered by the `NextjsStaticAssets` bucket exactly as in production, rather than
|
|
294
|
+
by some harness-only arrangement.
|
|
295
|
+
|
|
296
|
+
What `app.js` reports as the deployment URL is the distribution's bare origin,
|
|
297
|
+
deliberately not `NextjsGlobalFunctions#url` — that property appends the app's
|
|
298
|
+
`basePath`, and the fixtures are written against a Vercel deployment URL, which
|
|
299
|
+
has neither a path nor a trailing slash. `new URL(path, deploymentUrl)` would not
|
|
300
|
+
care, but plenty of tests interpolate instead (`` `${next.url}${path}` `` with
|
|
301
|
+
`path` already carrying the basePath), and a prefix or a trailing slash there
|
|
302
|
+
shows up as `/base//base/refresh`. Reporting the origin took
|
|
303
|
+
`app-dir/app-basepath` from 7 failures to 3.
|
|
304
|
+
|
|
305
|
+
Two caveats worth knowing before reading a failure as a regression:
|
|
306
|
+
|
|
307
|
+
- The dynamic cache policy allowlists ~10 request headers, a CloudFront quota
|
|
308
|
+
limitation documented at `src/nextjs-distribution.ts`. A test that varies on a
|
|
309
|
+
header outside that list can be served a wrong cached response. That is a real
|
|
310
|
+
product limitation, not a harness artifact — and not a regression either.
|
|
311
|
+
- The other three types are not on the schedule; see "Running on the other
|
|
312
|
+
`NextjsType`s". `examples/e2e-tests` is the per-commit gate on all three.
|
|
313
|
+
|
|
314
|
+
## Running on the other `NextjsType`s
|
|
315
|
+
|
|
316
|
+
`HARNESS_NEXTJS_TYPE` picks the root construct: `global-functions` (the default),
|
|
317
|
+
`regional-functions`, `global-containers` or `regional-containers`. Each type but
|
|
318
|
+
the default gets a stack infix of its own — `hrns-rf-*`, `hrns-gc-*`,
|
|
319
|
+
`hrns-rc-*` — so no run can deploy into another type's stack. In CI, runs of two
|
|
320
|
+
types still queue behind each other (see "Sharding"). The workflow's
|
|
321
|
+
`nextjs_type` input sets it for every shard.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
ADAPTER_DIR=$PWD HARNESS_NEXTJS_TYPE=global-containers ./scripts/e2e-warm.sh
|
|
325
|
+
# ...then the usual run from the next.js checkout, with the same
|
|
326
|
+
# HARNESS_NEXTJS_TYPE exported, and afterwards:
|
|
327
|
+
HARNESS_NEXTJS_TYPE=global-containers ./scripts/e2e-sweep.sh --apply --shared
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
What a type cannot pass for a known reason goes in the manifest's
|
|
331
|
+
`rules.excludeByType`, keyed by type: a whole file, or with `failed`, only those
|
|
332
|
+
cases. The workflow turns the entry into one more manifest for
|
|
333
|
+
NEXT_EXTERNAL_TESTS_FILTERS (`type-filters.mjs`); by hand, do the same:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
node scripts/e2e-harness/type-filters.mjs test/deploy-tests-manifest.json \
|
|
337
|
+
"$HARNESS_NEXTJS_TYPE" /tmp/harness-type-filters.json
|
|
338
|
+
export NEXT_EXTERNAL_TESTS_FILTERS=../test/deploy-tests-manifest.json,/tmp/harness-type-filters.json
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Only harness limitations and documented unsupported features belong there, each
|
|
342
|
+
with its reason in `docs/harness-coverage.md`. A cdk-nextjs defect is a bug to
|
|
343
|
+
fix, not an exclusion.
|
|
344
|
+
|
|
345
|
+
A case-level entry is a `suites` entry in the overlay, and next.js runs every
|
|
346
|
+
`suites` file regardless of `rules.include`. On the full manifest that changes
|
|
347
|
+
nothing; with a narrower `test_filters`, those files run as well.
|
|
348
|
+
|
|
349
|
+
Shard counts: 10 fit the job's time caps for every type but
|
|
350
|
+
`NextjsGlobalContainers`, whose files average ~4.3 minutes and need ~15. Every
|
|
351
|
+
Global shard is a CloudFront distribution with three response headers policies,
|
|
352
|
+
out of an account quota of 50 by default; 15 shards need the raised quota. The
|
|
353
|
+
dev account's is 100 (raised 2026-09-29), which fits 15 Global shards beside the
|
|
354
|
+
per-PR example stacks.
|
|
355
|
+
|
|
356
|
+
Log groups outlive their stacks - Lambda's and Container Insights' are created
|
|
357
|
+
outside the stack - so an account-wide `e2e-sweep.sh` also deletes the `hrns-*`
|
|
358
|
+
log groups of stacks that no longer exist (dry run first, like the stacks).
|
|
359
|
+
|
|
360
|
+
### The Containers types
|
|
361
|
+
|
|
362
|
+
Both deploy through the same `--hotswap-fallback` path. A new image and a
|
|
363
|
+
changed environment are task-definition changes, which CDK hotswaps into the ECS
|
|
364
|
+
service, so every file still reuses its shard's distribution, load balancer and
|
|
365
|
+
VPC. What the harness does differently:
|
|
366
|
+
|
|
367
|
+
- **The health check accepts any answer.** Both types require a
|
|
368
|
+
`healthCheckPath` that answers 200, and no fixture has one. Rather than add a
|
|
369
|
+
route to the app under test, `app.js` points both checks at a path nothing
|
|
370
|
+
routes and accepts 200–499 (ALB) or any HTTP answer (the container's own
|
|
371
|
+
check). They only need to tell a listening server from one that is not.
|
|
372
|
+
- **The old task may be stopped at once** (`--hotswap-ecs-minimum-healthy-percent
|
|
373
|
+
0`). The default rolling deployment keeps the previous fixture's task serving
|
|
374
|
+
until the new one is healthy. The maximum has to stay above 100% — the service
|
|
375
|
+
has Availability Zone Rebalancing on, which rejects anything lower — so the two
|
|
376
|
+
can still overlap; the waits below are what keep the suite off the old one.
|
|
377
|
+
- **It waits for the target group, not just the service.** A hotswap returns
|
|
378
|
+
when the task is RUNNING, which is before the ALB routes to it (503 until its
|
|
379
|
+
health check passes), and a full CloudFormation update returns with the old
|
|
380
|
+
task still draining. `e2e-deploy.sh` waits until the target group holds exactly
|
|
381
|
+
one target and it is `healthy`. `NextjsGlobalContainers` is then invalidated
|
|
382
|
+
like `NextjsGlobalFunctions`.
|
|
383
|
+
- **One NAT gateway per stack**, not the default of one per AZ. Every shard has a
|
|
384
|
+
VPC of its own, and NAT gateways hold Elastic IPs out of a per-region quota.
|
|
385
|
+
- **`NEXT_E2E_TEST_TIMEOUT` is 480000 in the workflow**, not 240000. An image
|
|
386
|
+
build, a push and an ECS rollout are added to every file's deploy, all inside
|
|
387
|
+
`beforeAll`. `NextjsGlobalContainers` gets 900000, like `NextjsGlobalFunctions`,
|
|
388
|
+
for a CloudFront update (see "One shared stack, not one per test file").
|
|
389
|
+
|
|
390
|
+
`NextjsRegionalContainers` is served over plain HTTP at its ALB's origin — no
|
|
391
|
+
stage, so no proxy — which is what the construct deploys without a certificate.
|
|
392
|
+
A test that depends on HTTPS (a `Secure` cookie, say) fails there for that
|
|
393
|
+
reason alone.
|
|
394
|
+
|
|
395
|
+
## Running on `NextjsRegionalFunctions`
|
|
396
|
+
|
|
397
|
+
`HARNESS_NEXTJS_TYPE=regional-functions` deploys `NextjsRegionalFunctions`
|
|
398
|
+
instead, into `hrns-rf-<suffix>` stacks so it can never land in a Global run's.
|
|
399
|
+
By hand or `workflow_dispatch`; the schedule runs `NextjsGlobalFunctions` only.
|
|
400
|
+
|
|
401
|
+
The stage is the whole problem, and `stage-proxy.mjs` is the whole answer. The
|
|
402
|
+
suite discards any path in the deployment URL (above), and a REST API's is always
|
|
403
|
+
`.../<stage>`. So `e2e-deploy.sh` starts a localhost proxy per stack that adds the
|
|
404
|
+
stage to every request path and reports `http://127.0.0.1:<port>` as the
|
|
405
|
+
deployment URL. The fixture is deployed as built. API Gateway strips the stage
|
|
406
|
+
again, so the app sees what it would at a custom domain mapped at the root. The
|
|
407
|
+
proxy outlives each test file; `e2e-sweep.sh` stops it alongside the stack
|
|
408
|
+
delete.
|
|
409
|
+
|
|
410
|
+
Three things the regional deploy does that the Global one does not:
|
|
411
|
+
|
|
412
|
+
- **The fixture's `basePath` becomes the construct's `basePath` prop.** Synth
|
|
413
|
+
would derive the same resource path, but it would also warn that the app's
|
|
414
|
+
links miss the stage, which is true of the execute-api URL and not of the
|
|
415
|
+
proxy. Before derivation existed (see `docs/harness-coverage.md`), leaving it
|
|
416
|
+
unset 404'd every `_next/static` request.
|
|
417
|
+
- **The proxy sends `x-forwarded-host: 127.0.0.1:<port>`**, because it has to
|
|
418
|
+
send the execute-api `Host` for API Gateway to route. Without it every server
|
|
419
|
+
action fails Next.js's CSRF check (`host` … does not match `origin`).
|
|
420
|
+
- **It waits for the stage to settle** before reporting the URL. A fixture whose
|
|
421
|
+
`basePath` differs from the previous one's changes the resource tree, and API
|
|
422
|
+
Gateway kept answering the replaced tree (403 `MissingAuthenticationToken`,
|
|
423
|
+
then 500s) for ~90s after CloudFormation reported `UPDATE_COMPLETE`, unevenly
|
|
424
|
+
across requests. So it waits until the base path, `_next/static` and the
|
|
425
|
+
catch-all have all stayed healthy for five rounds in a row.
|
|
426
|
+
|
|
427
|
+
What it cannot do: a fetch the **server** makes to its own origin. The origin is
|
|
428
|
+
`127.0.0.1` on the machine running the suite, and the Lambda cannot reach that.
|
|
429
|
+
Next.js does this to stream a server action's `redirect()` target in one round
|
|
430
|
+
trip, and some fixtures do it themselves. Such a case fails here with
|
|
431
|
+
`ECONNREFUSED 127.0.0.1` in the function's log. That's the harness, not
|
|
432
|
+
cdk-nextjs: a real deployment's origin is its own public URL.
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
ADAPTER_DIR=$PWD HARNESS_NEXTJS_TYPE=regional-functions ./scripts/e2e-warm.sh
|
|
436
|
+
# ...then the usual run from the next.js checkout, with the same
|
|
437
|
+
# HARNESS_NEXTJS_TYPE exported, and afterwards:
|
|
438
|
+
HARNESS_NEXTJS_TYPE=regional-functions ./scripts/e2e-sweep.sh --apply --shared
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Which test files can run at all
|
|
442
|
+
|
|
443
|
+
cdk-nextjs rejects any build output whose runtime is not `nodejs`
|
|
444
|
+
(`assertNodeRuntimes` in `src/adapter/build-outputs.ts`), and it throws during
|
|
445
|
+
`next build`. So a fixture containing even one edge route, or a legacy edge
|
|
446
|
+
`middleware.ts`, does not fail some tests - it fails to build, and no per-case
|
|
447
|
+
`failed` entry in the manifest can rescue it. Such files have to stay out of
|
|
448
|
+
`rules.include`.
|
|
449
|
+
|
|
450
|
+
That is a product limitation and a deliberate one: the edge runtime is deprecated
|
|
451
|
+
in Next.js, and cdk-nextjs supports Next.js 16's Node-runtime `proxy.ts` instead.
|
|
452
|
+
931 of next.js's 1134 e2e files are edge-free, so it barely constrains widening
|
|
453
|
+
the list - and `scripts/e2e-harness/screen.mjs` applies this screen and the three
|
|
454
|
+
below for you, recording the funnel in the manifest's `screening` block:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
node scripts/e2e-harness/screen.mjs --next ../next.js # print the funnel
|
|
458
|
+
node scripts/e2e-harness/screen.mjs --next ../next.js --check # fail if stale
|
|
459
|
+
node scripts/e2e-harness/screen.mjs --next ../next.js --list cache # candidates
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
To check one candidate by hand instead:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# in the next.js checkout, against the fixture root (usually the test file's dir
|
|
466
|
+
# or its parent)
|
|
467
|
+
find <fixture> -name "middleware.*"
|
|
468
|
+
grep -rlE "runtime\s*[:=]\s*.(experimental-)?edge." <fixture>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Both spellings matter — app router's `export const runtime = 'edge'` and pages
|
|
472
|
+
router's `export const config = { runtime: 'experimental-edge' }`. Matching only
|
|
473
|
+
the first undercounts by ~17 files.
|
|
474
|
+
|
|
475
|
+
Mind the "or its parent": for a `test/e2e/<name>/test/index.test.ts` the fixture
|
|
476
|
+
lives a directory _above_ the test file, and screening only the test file's own
|
|
477
|
+
directory quietly misses its `middleware.js`.
|
|
478
|
+
|
|
479
|
+
Six more screens are worth running before spending a deploy on a candidate, all
|
|
480
|
+
against the test file rather than the fixture:
|
|
481
|
+
|
|
482
|
+
- `skipDeployment: true` in the `nextTestSetup` call — next.js replaces the whole
|
|
483
|
+
file with `it.only('should skip next deploy')` and sets `skipped`, so the body
|
|
484
|
+
early-returns (`test/lib/e2e-utils/index.ts`). The costliest screen to have been
|
|
485
|
+
missing: it accounts for a third of what was previously counted as the candidate
|
|
486
|
+
pool, and two files reached `rules.include` on the strength of a 4s "pass".
|
|
487
|
+
- `describe.skip` / `(isNextDev ? describe : describe.skip)` — a file skipped
|
|
488
|
+
upstream reports as passing in a few seconds without deploying anything, and
|
|
489
|
+
adding it to `rules.include` claims coverage that does not exist. This screen
|
|
490
|
+
over-reports: the match is textual, so a skip conditional on something the
|
|
491
|
+
harness does not set is caught too. `incremental-cache-path-traversal` is one
|
|
492
|
+
(`__NEXT_CACHE_COMPONENTS`), it runs fine, and it found a real defect. Re-read
|
|
493
|
+
any file this screen alone disqualifies.
|
|
494
|
+
- An empty `it('should skip …', () => {})` — the stub next.js writes for the modes
|
|
495
|
+
a _mode-gated_ file does not cover, its real cases sitting inside
|
|
496
|
+
`if (isNextStart)` or `if (isNextDev)`. `app-fetch-deduping` reported a pass in
|
|
497
|
+
5.9s having deployed nothing. A stub whose title names _dev_ is the opposite
|
|
498
|
+
case — the file runs everywhere but dev — so those are left in
|
|
499
|
+
(`app-prefetch-static`).
|
|
500
|
+
- `isNextDeploy` — usually next.js itself gating out what cannot work behind a
|
|
501
|
+
CDN. Not always disqualifying, but read the gate before adding the file.
|
|
502
|
+
- `output: 'export'` in the fixture — a static export is not what any
|
|
503
|
+
`NextjsType` deploys.
|
|
504
|
+
- An entry in the manifest's `excluded-notes` — the file already has a written
|
|
505
|
+
verdict, so it has been decided and should not reappear as a candidate. Keys that
|
|
506
|
+
start with `test/e2e/` are matched (`…/**` matches the tree); the prose keys like
|
|
507
|
+
`"edge runtime, generally"` are not. Without this screen a decided file stays a
|
|
508
|
+
candidate forever: the `next-config-ts-native-ts` family would have been picked
|
|
509
|
+
for a second batch and burned 18 more deploy slots on a build that cannot
|
|
510
|
+
succeed.
|
|
511
|
+
|
|
512
|
+
None of these is watertight — each is a textual match on a convention next.js is
|
|
513
|
+
under no obligation to keep. The measured backstop covers all of them: **a file
|
|
514
|
+
that passes in under ~10 seconds deployed nothing.** Check the `--timings`
|
|
515
|
+
duration before believing a pass. That is how the mode-gated screen above came to
|
|
516
|
+
be written, after `app-fetch-deduping` spent a deploy slot to report a 5.9s
|
|
517
|
+
pass.
|
|
518
|
+
|
|
519
|
+
`excluded-notes` in the manifest records every file left out and why, and its
|
|
520
|
+
`screening` block records the funnel — how many files there are, how many survive
|
|
521
|
+
each screen, and how many are already included. Regenerate it with `--write`
|
|
522
|
+
whenever `next` is upgraded; `--check` exits nonzero if it has drifted, which is
|
|
523
|
+
the only thing that keeps those numbers worth quoting. The workflow runs it in
|
|
524
|
+
shard 1 of every run against the default next.js ref.
|
|
525
|
+
|
|
526
|
+
Passing every screen makes a file a _candidate_, not a pass. It still has to
|
|
527
|
+
be deployed and watched, and anything that fails gets root-caused and given a
|
|
528
|
+
verdict in `docs/harness-coverage.md` before it is either fixed or written off.
|
|
529
|
+
|
|
530
|
+
## Keeping the passing part of a partly-failing file
|
|
531
|
+
|
|
532
|
+
Written off does not have to mean the whole file. A file listed in the manifest's
|
|
533
|
+
`suites` is **included**, with the cases named in its `failed` array skipped —
|
|
534
|
+
next.js's `test/get-test-filter.js` turns them into `excludedCases` and
|
|
535
|
+
`run-tests.js` passes them as a negative `--testNamePattern`. That is the right
|
|
536
|
+
home for a file whose residual failures already have an _acceptable_ verdict, and
|
|
537
|
+
it is how `trailingslash` contributes 6 of its 8 cases:
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
"suites": {
|
|
541
|
+
"test/e2e/app-dir/trailingslash/trailingslash.test.ts": {
|
|
542
|
+
"failed": ["app-dir trailingSlash handling should revalidate a page with generated static params (withSlash=true)"],
|
|
543
|
+
"flakey": []
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Two things to get right. The names are full jest names — every enclosing
|
|
549
|
+
`describe` title, space-joined, and for `it.each` the interpolated title, so
|
|
550
|
+
`(withSlash=$withSlash)` has to be spelled out per case. And a `failed` entry is
|
|
551
|
+
never how a cdk-nextjs bug gets handled: fix it, or give it a verdict first.
|
|
552
|
+
Add a `comment` array to the entry naming the verdict it stands on.
|
|
553
|
+
|
|
554
|
+
## Running it locally
|
|
555
|
+
|
|
556
|
+
Needs a next.js checkout at the tag matching this repo's `@next/routing` pin
|
|
557
|
+
(which the workflow's `nextjs_ref` defaults to), built
|
|
558
|
+
once (`pnpm install && pnpm build && pnpm install` in it — `run-tests.js` and
|
|
559
|
+
`test/lib` are repo files, not published ones), and AWS credentials that can
|
|
560
|
+
deploy.
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
# in the cdk-nextjs checkout
|
|
564
|
+
pnpm i && pnpm bundle && pnpm compile
|
|
565
|
+
|
|
566
|
+
# create the shared stack before the suite runs; ~4 minutes, once
|
|
567
|
+
ADAPTER_DIR=$PWD ./scripts/e2e-warm.sh
|
|
568
|
+
|
|
569
|
+
# in the next.js checkout
|
|
570
|
+
export ADAPTER_DIR=/path/to/cdk-nextjs
|
|
571
|
+
export NEXT_TEST_MODE=deploy
|
|
572
|
+
export NEXT_EXTERNAL_TESTS_FILTERS="$ADAPTER_DIR/test/deploy-tests-manifest.json"
|
|
573
|
+
export NEXT_TEST_DEPLOY_SCRIPT_PATH="$ADAPTER_DIR/scripts/e2e-deploy.sh"
|
|
574
|
+
export NEXT_TEST_DEPLOY_LOGS_SCRIPT_PATH="$ADAPTER_DIR/scripts/e2e-logs.sh"
|
|
575
|
+
export IS_TURBOPACK_TEST=1 NEXT_TELEMETRY_DISABLED=1
|
|
576
|
+
# Not optional either. cdk-nextjs is an adapter deployment and ten e2e files ask,
|
|
577
|
+
# most of them as `skipDeployment: !isAdapterTest` - left unset they report a pass
|
|
578
|
+
# in ~5s having deployed nothing. The only other thing that reads it is the Vercel
|
|
579
|
+
# deploy path (a team and token), which a custom deploy script never reaches.
|
|
580
|
+
export NEXT_ENABLE_ADAPTER=1
|
|
581
|
+
# Not optional. next.js defaults this to 120s and `createNext` runs inside jest's
|
|
582
|
+
# `beforeAll`, so everything below is charged against it: isolating the fixture,
|
|
583
|
+
# `pnpm install`, `next build`, `cdk deploy` (~52s even when it hotswaps), and the
|
|
584
|
+
# CloudFront invalidation wait. Measured, that sums past 120s for most files, and
|
|
585
|
+
# a file over budget fails *wholesale* on every retry with `Exceeded timeout of
|
|
586
|
+
# 120000 ms for a hook` - which is indistinguishable from a real failure unless
|
|
587
|
+
# you read the elapsed time. CI uses this same value
|
|
588
|
+
# (.github/workflows/e2e-harness.yml).
|
|
589
|
+
export NEXT_E2E_TEST_TIMEOUT=240000
|
|
590
|
+
node run-tests.js --timings -c 1 --retries 1 --type e2e
|
|
591
|
+
|
|
592
|
+
# back in cdk-nextjs: the shared stack is still up by design. Look, then delete.
|
|
593
|
+
./scripts/e2e-sweep.sh
|
|
594
|
+
./scripts/e2e-sweep.sh --apply --shared
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
To exercise just the deploy/logs contract without the next.js suite,
|
|
598
|
+
point them at any built Next.js app:
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
cd /path/to/some/nextjs/app
|
|
602
|
+
ADAPTER_DIR=/path/to/cdk-nextjs /path/to/cdk-nextjs/scripts/e2e-deploy.sh
|
|
603
|
+
ADAPTER_DIR=/path/to/cdk-nextjs /path/to/cdk-nextjs/scripts/e2e-logs.sh
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## Debugging one failing file without deploying
|
|
607
|
+
|
|
608
|
+
A deployed iteration costs 2–3 minutes and gives you a log. `e2e-offline.sh` costs
|
|
609
|
+
~90 seconds and gives you a server you can instrument:
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
scripts/e2e-offline.sh app-dir/layout-params # then curl :3112 and :3113
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
It builds the fixture through the adapter and serves the same build twice — once
|
|
616
|
+
by our container shell, once by `next start` — so "why does the deployment
|
|
617
|
+
disagree with next.js" becomes one `diff` of two responses. Defects 11, 13 and 14
|
|
618
|
+
in `docs/harness-coverage.md`'s table were all found this way; 11 in particular had
|
|
619
|
+
survived one wrong fix because the value in question only shows up under a
|
|
620
|
+
debugger (`JSON.stringify` erases it).
|
|
621
|
+
|
|
622
|
+
Two limits. The fixture is staged _inside_ the next.js checkout, because module
|
|
623
|
+
resolution has to walk up to its `node_modules/next` — the script handles that, but
|
|
624
|
+
it means the app directory is not in this repo and you should delete it when done.
|
|
625
|
+
And the runtime cache reads S3 only, so a route served entirely from a build-time
|
|
626
|
+
prerender answers `invariant: cache entry required but not generated` offline; for
|
|
627
|
+
those, read the seed directory (`.next/cdk-nextjs-init-cache`) instead of the
|
|
628
|
+
response, or deploy.
|
|
629
|
+
|
|
630
|
+
## The logs script's output _is_ `next.cliOutput`
|
|
631
|
+
|
|
632
|
+
Worth knowing before adding anything to `e2e-logs.sh`. In deploy mode next.js sets
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
this._cliOutput = await this.fetchBuildLogsUsingCustomScript();
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
(`test/lib/next-modes/next-deploy.ts`), so the script's stdout is not just what
|
|
639
|
+
gets printed when a deployment fails — it is the string every test that reads
|
|
640
|
+
`next.cliOutput` asserts against. Vercel's own deploy mode puts the **build** logs
|
|
641
|
+
there and nothing else.
|
|
642
|
+
|
|
643
|
+
That is why the deploy log and the CloudWatch tail are behind
|
|
644
|
+
`HARNESS_VERBOSE_LOGS=1`. `test/e2e/deprecation-warnings` asserts
|
|
645
|
+
`expect(next.cliOutput).not.toContain('deprecated')`, and it failed on
|
|
646
|
+
|
|
647
|
+
```
|
|
648
|
+
[WARNING] aws-cdk-lib.aws_dynamodb.TableGrantsProps#encryptedResource is deprecated.
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
which the CDK CLI printed during synth — aws-cdk-lib 2.261.0 emits it from inside
|
|
652
|
+
its own `TableV2` constructor, so no consumer can avoid it. Nothing about the app
|
|
653
|
+
under test was wrong; our diagnostics were being read as the app's output.
|
|
654
|
+
|
|
655
|
+
When a deployment does fail, set the flag and re-run the one file.
|
|
656
|
+
|
|
657
|
+
## Cleanup and safety
|
|
658
|
+
|
|
659
|
+
A CloudFront distribution that outlives its run is the thing to avoid, so there
|
|
660
|
+
are two layers:
|
|
661
|
+
|
|
662
|
+
1. `e2e-sweep.sh --apply --shared` deletes the shared stack after a run — the
|
|
663
|
+
workflow's `always()` step, or by hand locally. Nothing deletes it between
|
|
664
|
+
test files: the harness is deliberately given no cleanup script.
|
|
665
|
+
2. An account-wide `e2e-sweep.sh --apply` deletes any leftovers a cancelled or
|
|
666
|
+
timed-out shard left behind.
|
|
667
|
+
|
|
668
|
+
The sweeper refuses to delete a stack unless it is named `hrns-*` **and** tagged
|
|
669
|
+
`cdk-nextjs:harness=1`, re-checked immediately before the delete (after any
|
|
670
|
+
wait for the stack to settle). It is
|
|
671
|
+
additionally a dry run unless given `--apply`, and ignores any stack used within
|
|
672
|
+
the last `HARNESS_SWEEP_MAX_AGE_HOURS` (default 6). "Used" is the latest of the
|
|
673
|
+
stack's `CreationTime`, its `LastUpdatedTime`, and its server function's
|
|
674
|
+
`LastModified` — the last because a shared stack is reused for hours by hotswap,
|
|
675
|
+
which never touches CloudFormation's timestamps but does rewrite the function.
|
|
676
|
+
That is re-read right before each delete as well.
|
|
677
|
+
|
|
678
|
+
The floor protects a stack that was deployed into recently. It cannot protect one
|
|
679
|
+
that has sat idle for six hours and is *about* to be deployed into again — a
|
|
680
|
+
reused shared or shard name at the start of a run — so don't run an
|
|
681
|
+
account-wide `--apply` sweep while a run is starting. CI's sweep waits
|
|
682
|
+
for the `harness` job for this reason, and the workflow's single `concurrency`
|
|
683
|
+
group keeps other types' runs from starting under it. An account-wide sweep also
|
|
684
|
+
keeps any stack with an operation in flight (`*_IN_PROGRESS`); a named one
|
|
685
|
+
(`--shared`, `--stack`) waits up to 20 minutes for it to settle, then deletes.
|
|
686
|
+
|
|
687
|
+
A stack left in `DELETE_FAILED` is retried by the next sweep, through the same
|
|
688
|
+
gates. `--wait` blocks until each delete finishes and exits non-zero if one ends
|
|
689
|
+
in `DELETE_FAILED`, printing the failing resources; CI's per-shard delete uses it
|
|
690
|
+
so the next run never deploys into a stack that is still being deleted. To delete
|
|
691
|
+
a stack you just created, name it — `--shared` or `--stack NAME`, which narrows
|
|
692
|
+
the sweep to that one stack instead of lowering the age floor for every stack in
|
|
693
|
+
the account.
|
|
694
|
+
|
|
695
|
+
## Environment knobs
|
|
696
|
+
|
|
697
|
+
| Variable | Default | Effect |
|
|
698
|
+
| ----------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------ |
|
|
699
|
+
| `ADAPTER_DIR` | _required_ | This checkout. Every harness script resolves everything from it. |
|
|
700
|
+
| `CDK_BIN` | `$ADAPTER_DIR/node_modules/.bin/cdk` | CDK CLI to deploy with. |
|
|
701
|
+
| `HARNESS_NEXTJS_TYPE` | `global-functions` | Or `regional-functions` (behind `stage-proxy.mjs`, `hrns-rf-*`), `global-containers` (`hrns-gc-*`) or `regional-containers` (`hrns-rc-*`). See "Running on the other `NextjsType`s". |
|
|
702
|
+
| `HARNESS_SHARED_STACK_SUFFIX` | `shared` | Shared stack name, after the `hrns-` prefix. One per shard, and per local session (`dev-$USER`). |
|
|
703
|
+
| `HARNESS_VERBOSE_LOGS` | `0` | Add the deploy log and CloudWatch tail to `e2e-logs.sh`. Off by default because that output _is_ `next.cliOutput` — see below. |
|
|
704
|
+
| `HARNESS_SWEEP_MAX_AGE_HOURS` | `6` | Age floor for the sweeper. Ignored when a stack is named. |
|
|
705
|
+
| `NEXT_E2E_TEST_TIMEOUT` | next.js's 120000 | next.js's own knob, but effectively required here: the deploy runs inside `beforeAll`. Use `240000`. |
|
|
706
|
+
|
|
707
|
+
[harness]: https://nextjs.org/docs/app/api-reference/adapters/testing-adapters
|