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.
Files changed (266) hide show
  1. package/.jsii +2467 -2714
  2. package/.jsii.tabl.json +1 -1
  3. package/API.md +1725 -1909
  4. package/CLAUDE.md +2 -1
  5. package/README.md +429 -130
  6. package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +152 -170
  7. package/assets/lambdas/redeploy-stage/redeploy-stage.lambda/index.js +875 -0
  8. package/docs/adapter-runtime.md +185 -0
  9. package/docs/breaking-changes.md +687 -4
  10. package/docs/caching-guide.md +156 -112
  11. package/docs/cdk-nextjs-cfn-exec-policy.json +2 -1
  12. package/docs/development-guide.md +1 -1
  13. package/docs/harness-coverage.md +598 -0
  14. package/docs/init-cache-deployment.md +32 -1
  15. package/docs/next-build-output-guide.md +30 -20
  16. package/examples/README.md +5 -1
  17. package/lib/adapter/__fixtures__/app-playground-base-path.json +4926 -0
  18. package/lib/adapter/__fixtures__/app-playground.json +4917 -0
  19. package/lib/adapter/__fixtures__/pages-i18n.json +1728 -0
  20. package/lib/adapter/adapter.mjs +1548 -185
  21. package/lib/adapter/adapter.test.js +106 -0
  22. package/lib/adapter/aws-cache-store.d.ts +489 -0
  23. package/lib/adapter/aws-cache-store.js +1004 -0
  24. package/lib/adapter/aws-cache-store.test.d.ts +1 -0
  25. package/lib/adapter/aws-cache-store.test.js +361 -0
  26. package/lib/adapter/build-outputs.d.ts +80 -0
  27. package/lib/adapter/build-outputs.js +852 -0
  28. package/lib/adapter/build-outputs.test.d.ts +1 -0
  29. package/lib/adapter/build-outputs.test.js +861 -0
  30. package/lib/adapter/cache-handler.d.ts +9 -2
  31. package/lib/adapter/cache-handler.js +72 -96
  32. package/lib/adapter/cache-handler.mjs +18956 -17774
  33. package/lib/adapter/cache-handler.test.js +263 -31
  34. package/lib/adapter/cache-kinds.d.ts +32 -0
  35. package/lib/adapter/cache-kinds.js +35 -0
  36. package/lib/adapter/cache-kinds.test.d.ts +1 -0
  37. package/lib/adapter/cache-kinds.test.js +69 -0
  38. package/lib/adapter/cache-utils.d.ts +90 -0
  39. package/lib/adapter/cache-utils.js +199 -4
  40. package/lib/adapter/cache-utils.test.d.ts +1 -0
  41. package/lib/adapter/cache-utils.test.js +179 -0
  42. package/lib/adapter/function-groups.d.ts +215 -0
  43. package/lib/adapter/function-groups.js +1099 -0
  44. package/lib/adapter/function-groups.test.d.ts +1 -0
  45. package/lib/adapter/function-groups.test.js +743 -0
  46. package/lib/adapter/init-cache.d.ts +6 -0
  47. package/lib/adapter/init-cache.js +245 -0
  48. package/lib/adapter/init-cache.test.d.ts +1 -0
  49. package/lib/adapter/init-cache.test.js +137 -0
  50. package/lib/adapter/local-file-cache-handler.d.ts +23 -0
  51. package/lib/adapter/local-file-cache-handler.js +52 -1
  52. package/lib/adapter/memory-cache-handler.d.ts +26 -17
  53. package/lib/adapter/memory-cache-handler.js +32 -50
  54. package/lib/adapter/memory-cache-handler.test.js +105 -85
  55. package/lib/adapter/s3-cache-handler.d.ts +179 -40
  56. package/lib/adapter/s3-cache-handler.js +807 -277
  57. package/lib/adapter/s3-cache-handler.test.js +1186 -31
  58. package/lib/adapter/use-cache-common.d.ts +90 -0
  59. package/lib/adapter/use-cache-common.js +250 -0
  60. package/lib/adapter/use-cache-common.test.d.ts +1 -0
  61. package/lib/adapter/use-cache-common.test.js +646 -0
  62. package/lib/adapter/use-cache-default-handler.d.ts +24 -0
  63. package/lib/adapter/use-cache-default-handler.js +90 -0
  64. package/lib/adapter/use-cache-default-handler.mjs +38645 -0
  65. package/lib/adapter/use-cache-handlers.test.d.ts +1 -0
  66. package/lib/adapter/use-cache-handlers.test.js +349 -0
  67. package/lib/adapter/use-cache-remote-handler.d.ts +26 -0
  68. package/lib/adapter/use-cache-remote-handler.js +186 -0
  69. package/lib/{image-optimization/handler.mjs → adapter/use-cache-remote-handler.mjs} +33572 -38798
  70. package/lib/generated-structs/OptionalNextjsApiProps.d.ts +59 -0
  71. package/lib/generated-structs/OptionalNextjsApiProps.js +3 -0
  72. package/lib/generated-structs/OptionalNextjsCacheProps.d.ts +15 -6
  73. package/lib/generated-structs/OptionalNextjsCacheProps.js +1 -1
  74. package/lib/generated-structs/OptionalNextjsContainersProps.d.ts +25 -11
  75. package/lib/generated-structs/OptionalNextjsContainersProps.js +1 -1
  76. package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +31 -9
  77. package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
  78. package/lib/generated-structs/OptionalNextjsPostDeployProps.d.ts +13 -0
  79. package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
  80. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +11 -2
  81. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.js +1 -1
  82. package/lib/index.d.ts +6 -7
  83. package/lib/index.js +2 -4
  84. package/lib/lambdas/post-deploy/post-deploy.lambda.js +3 -2
  85. package/lib/lambdas/post-deploy/prune-cache-bucket.d.ts +7 -3
  86. package/lib/lambdas/post-deploy/prune-cache-bucket.js +57 -71
  87. package/lib/lambdas/post-deploy/prune-cache-bucket.test.d.ts +1 -0
  88. package/lib/lambdas/post-deploy/prune-cache-bucket.test.js +114 -0
  89. package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +1 -0
  90. package/lib/lambdas/post-deploy/prune-revalidation-table.js +23 -31
  91. package/lib/lambdas/post-deploy/prune-revalidation-table.test.d.ts +1 -0
  92. package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +71 -0
  93. package/lib/lambdas/post-deploy/prune-s3.d.ts +19 -3
  94. package/lib/lambdas/post-deploy/prune-s3.js +54 -15
  95. package/lib/lambdas/post-deploy/prune-s3.test.d.ts +1 -0
  96. package/lib/lambdas/post-deploy/prune-s3.test.js +255 -0
  97. package/lib/lambdas/redeploy-stage/redeploy-stage-function.d.ts +13 -0
  98. package/lib/lambdas/redeploy-stage/redeploy-stage-function.js +55 -0
  99. package/lib/lambdas/redeploy-stage/redeploy-stage.d.ts +24 -0
  100. package/lib/lambdas/redeploy-stage/redeploy-stage.js +68 -0
  101. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.d.ts +20 -0
  102. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.js +24 -0
  103. package/lib/lambdas/redeploy-stage/redeploy-stage.test.d.ts +1 -0
  104. package/lib/lambdas/redeploy-stage/redeploy-stage.test.js +93 -0
  105. package/lib/nextjs-api.d.ts +107 -18
  106. package/lib/nextjs-api.js +349 -39
  107. package/lib/nextjs-api.test.js +582 -1
  108. package/lib/nextjs-build/global-containers.Dockerfile +15 -10
  109. package/lib/nextjs-build/nextjs-build.d.ts +339 -63
  110. package/lib/nextjs-build/nextjs-build.js +684 -327
  111. package/lib/nextjs-build/nextjs-build.test.d.ts +1 -0
  112. package/lib/nextjs-build/nextjs-build.test.js +412 -0
  113. package/lib/nextjs-build/patch-fetch.js +1 -1
  114. package/lib/nextjs-build/patch-fetch.test.js +166 -11
  115. package/lib/nextjs-build/regional-containers.Dockerfile +17 -10
  116. package/lib/nextjs-cache.d.ts +82 -4
  117. package/lib/nextjs-cache.js +186 -10
  118. package/lib/nextjs-cache.test.js +176 -1
  119. package/lib/nextjs-compute/nextjs-compute-base-props.d.ts +21 -8
  120. package/lib/nextjs-compute/nextjs-compute-base-props.js +41 -1
  121. package/lib/nextjs-compute/nextjs-compute-base-props.test.d.ts +1 -0
  122. package/lib/nextjs-compute/nextjs-compute-base-props.test.js +44 -0
  123. package/lib/nextjs-compute/nextjs-containers.d.ts +19 -0
  124. package/lib/nextjs-compute/nextjs-containers.js +57 -20
  125. package/lib/nextjs-compute/nextjs-functions.d.ts +122 -7
  126. package/lib/nextjs-compute/nextjs-functions.js +113 -67
  127. package/lib/nextjs-compute/nextjs-functions.test.d.ts +1 -0
  128. package/lib/nextjs-compute/nextjs-functions.test.js +99 -0
  129. package/lib/nextjs-distribution.d.ts +194 -23
  130. package/lib/nextjs-distribution.js +535 -62
  131. package/lib/nextjs-distribution.test.d.ts +1 -0
  132. package/lib/nextjs-distribution.test.js +697 -0
  133. package/lib/nextjs-post-deploy.d.ts +23 -3
  134. package/lib/nextjs-post-deploy.js +14 -10
  135. package/lib/nextjs-post-deploy.test.d.ts +1 -0
  136. package/lib/nextjs-post-deploy.test.js +34 -0
  137. package/lib/nextjs-static-assets.d.ts +23 -4
  138. package/lib/nextjs-static-assets.js +29 -6
  139. package/lib/nextjs-static-assets.test.js +20 -1
  140. package/lib/root-constructs/nextjs-base-construct.d.ts +105 -45
  141. package/lib/root-constructs/nextjs-base-construct.js +144 -34
  142. package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
  143. package/lib/root-constructs/nextjs-global-containers.js +20 -70
  144. package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
  145. package/lib/root-constructs/nextjs-global-functions.js +52 -69
  146. package/lib/root-constructs/nextjs-regional-containers.d.ts +25 -5
  147. package/lib/root-constructs/nextjs-regional-containers.js +17 -15
  148. package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
  149. package/lib/root-constructs/nextjs-regional-functions.js +8 -25
  150. package/lib/runtime/api-gateway-path.d.ts +34 -0
  151. package/lib/runtime/api-gateway-path.js +71 -0
  152. package/lib/runtime/api-gateway-path.test.d.ts +1 -0
  153. package/lib/runtime/api-gateway-path.test.js +65 -0
  154. package/lib/runtime/case-redirect.d.ts +22 -0
  155. package/lib/runtime/case-redirect.js +63 -0
  156. package/lib/runtime/case-redirect.test.d.ts +1 -0
  157. package/lib/runtime/case-redirect.test.js +32 -0
  158. package/lib/runtime/core.d.ts +234 -0
  159. package/lib/runtime/core.js +1213 -0
  160. package/lib/runtime/core.test.d.ts +1 -0
  161. package/lib/runtime/core.test.js +1678 -0
  162. package/lib/runtime/deployment-root.d.ts +13 -0
  163. package/lib/runtime/deployment-root.js +29 -0
  164. package/lib/runtime/dispatch.d.ts +331 -0
  165. package/lib/runtime/dispatch.js +751 -0
  166. package/lib/runtime/dispatch.test.d.ts +1 -0
  167. package/lib/runtime/dispatch.test.js +853 -0
  168. package/lib/runtime/entrypoints.d.ts +57 -0
  169. package/lib/runtime/entrypoints.js +115 -0
  170. package/lib/runtime/http/abort-signal.d.ts +6 -0
  171. package/lib/runtime/http/abort-signal.js +19 -0
  172. package/lib/runtime/http/abort-signal.test.d.ts +1 -0
  173. package/lib/runtime/http/abort-signal.test.js +49 -0
  174. package/lib/runtime/http/node-server.d.ts +38 -0
  175. package/lib/runtime/http/node-server.js +99 -0
  176. package/lib/runtime/http/node-server.test.d.ts +1 -0
  177. package/lib/runtime/http/node-server.test.js +96 -0
  178. package/lib/runtime/http/node-sink.d.ts +16 -0
  179. package/lib/runtime/http/node-sink.js +48 -0
  180. package/lib/runtime/http/node-sink.test.d.ts +1 -0
  181. package/lib/runtime/http/node-sink.test.js +66 -0
  182. package/lib/runtime/http/request.d.ts +62 -0
  183. package/lib/runtime/http/request.js +134 -0
  184. package/lib/runtime/http/request.test.d.ts +1 -0
  185. package/lib/runtime/http/request.test.js +103 -0
  186. package/lib/runtime/http/response.d.ts +77 -0
  187. package/lib/runtime/http/response.js +229 -0
  188. package/lib/runtime/http/response.test.d.ts +1 -0
  189. package/lib/runtime/http/response.test.js +178 -0
  190. package/lib/runtime/http/sink.d.ts +73 -0
  191. package/lib/runtime/http/sink.js +188 -0
  192. package/lib/runtime/http/sink.test.d.ts +1 -0
  193. package/lib/runtime/http/sink.test.js +257 -0
  194. package/lib/runtime/image-utils.d.ts +78 -0
  195. package/lib/runtime/image-utils.js +129 -0
  196. package/lib/runtime/image-utils.test.d.ts +1 -0
  197. package/lib/runtime/image-utils.test.js +237 -0
  198. package/lib/runtime/image.d.ts +76 -0
  199. package/lib/runtime/image.js +296 -0
  200. package/lib/runtime/image.test.d.ts +1 -0
  201. package/lib/runtime/image.test.js +405 -0
  202. package/lib/runtime/lambda-event.d.ts +4 -0
  203. package/lib/runtime/lambda-event.js +113 -0
  204. package/lib/runtime/lambda-event.test.d.ts +1 -0
  205. package/lib/runtime/lambda-event.test.js +119 -0
  206. package/lib/runtime/lambda.mjs +41595 -0
  207. package/lib/runtime/load-module.d.ts +8 -0
  208. package/lib/runtime/load-module.js +35 -0
  209. package/lib/runtime/manifest.d.ts +215 -0
  210. package/lib/runtime/manifest.js +118 -0
  211. package/lib/runtime/middleware.d.ts +43 -0
  212. package/lib/runtime/middleware.js +113 -0
  213. package/lib/runtime/middleware.test.d.ts +1 -0
  214. package/lib/runtime/middleware.test.js +259 -0
  215. package/lib/runtime/next-modules.d.ts +55 -0
  216. package/lib/runtime/next-modules.js +124 -0
  217. package/lib/runtime/next-modules.test.d.ts +1 -0
  218. package/lib/runtime/next-modules.test.js +34 -0
  219. package/lib/runtime/public-files.d.ts +32 -0
  220. package/lib/runtime/public-files.js +117 -0
  221. package/lib/runtime/public-files.test.d.ts +1 -0
  222. package/lib/runtime/public-files.test.js +97 -0
  223. package/lib/runtime/server.mjs +41588 -0
  224. package/lib/runtime/static-files.d.ts +50 -0
  225. package/lib/runtime/static-files.js +266 -0
  226. package/lib/runtime/static-files.test.d.ts +1 -0
  227. package/lib/runtime/static-files.test.js +118 -0
  228. package/lib/runtime/util.d.ts +28 -0
  229. package/lib/runtime/util.js +79 -0
  230. package/lib/utils/base-path.d.ts +123 -11
  231. package/lib/utils/base-path.js +276 -39
  232. package/lib/utils/base-path.test.js +275 -50
  233. package/lib/utils/get-architecture.d.ts +7 -8
  234. package/lib/utils/get-architecture.js +12 -13
  235. package/lib/utils/zip-directory.d.ts +14 -0
  236. package/lib/utils/zip-directory.js +121 -0
  237. package/lib/utils/zip-directory.test.d.ts +1 -0
  238. package/lib/utils/zip-directory.test.js +99 -0
  239. package/package.json +22 -3
  240. package/scripts/capture-adapter-fixture.mjs +325 -0
  241. package/scripts/e2e-deploy.sh +398 -0
  242. package/scripts/e2e-harness/README.md +707 -0
  243. package/scripts/e2e-harness/app.js +370 -0
  244. package/scripts/e2e-harness/common.sh +249 -0
  245. package/scripts/e2e-harness/screen.mjs +302 -0
  246. package/scripts/e2e-harness/stage-proxy.mjs +145 -0
  247. package/scripts/e2e-harness/type-filters.mjs +47 -0
  248. package/scripts/e2e-logs.sh +75 -0
  249. package/scripts/e2e-offline.sh +91 -0
  250. package/scripts/e2e-sweep.sh +370 -0
  251. package/scripts/e2e-warm.sh +114 -0
  252. package/scripts/zero-config-build.mjs +418 -0
  253. package/docs/1k-concurrent-users-executions.png +0 -0
  254. package/docs/plans/adapter-runtime-release.md +0 -362
  255. package/docs/plans/phase-0-image-function-opt-in.md +0 -295
  256. package/lib/generated-structs/OptionalDockerImageFunctionProps.d.ts +0 -425
  257. package/lib/generated-structs/OptionalDockerImageFunctionProps.js +0 -3
  258. package/lib/image-optimization/handler-utils.d.ts +0 -34
  259. package/lib/image-optimization/handler-utils.js +0 -83
  260. package/lib/image-optimization/handler-utils.test.js +0 -178
  261. package/lib/nextjs-build/functions.Dockerfile +0 -31
  262. package/lib/nextjs-compute/nextjs-image-function.d.ts +0 -53
  263. package/lib/nextjs-compute/nextjs-image-function.js +0 -59
  264. package/lib/utils/experimental-flags.d.ts +0 -24
  265. package/lib/utils/experimental-flags.js +0 -30
  266. /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