cdk-nextjs 0.6.2 → 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 (263) hide show
  1. package/.jsii +2404 -2723
  2. package/.jsii.tabl.json +1 -1
  3. package/API.md +1681 -1930
  4. package/CLAUDE.md +2 -1
  5. package/README.md +429 -130
  6. package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +148 -169
  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 +667 -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 +8 -0
  79. package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
  80. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +6 -4
  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/prune-cache-bucket.d.ts +7 -3
  85. package/lib/lambdas/post-deploy/prune-cache-bucket.js +57 -71
  86. package/lib/lambdas/post-deploy/prune-cache-bucket.test.d.ts +1 -0
  87. package/lib/lambdas/post-deploy/prune-cache-bucket.test.js +114 -0
  88. package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +1 -0
  89. package/lib/lambdas/post-deploy/prune-revalidation-table.js +23 -31
  90. package/lib/lambdas/post-deploy/prune-revalidation-table.test.d.ts +1 -0
  91. package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +71 -0
  92. package/lib/lambdas/post-deploy/prune-s3.d.ts +14 -5
  93. package/lib/lambdas/post-deploy/prune-s3.js +41 -12
  94. package/lib/lambdas/post-deploy/prune-s3.test.js +97 -18
  95. package/lib/lambdas/redeploy-stage/redeploy-stage-function.d.ts +13 -0
  96. package/lib/lambdas/redeploy-stage/redeploy-stage-function.js +55 -0
  97. package/lib/lambdas/redeploy-stage/redeploy-stage.d.ts +24 -0
  98. package/lib/lambdas/redeploy-stage/redeploy-stage.js +68 -0
  99. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.d.ts +20 -0
  100. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.js +24 -0
  101. package/lib/lambdas/redeploy-stage/redeploy-stage.test.d.ts +1 -0
  102. package/lib/lambdas/redeploy-stage/redeploy-stage.test.js +93 -0
  103. package/lib/nextjs-api.d.ts +102 -18
  104. package/lib/nextjs-api.js +338 -38
  105. package/lib/nextjs-api.test.js +537 -1
  106. package/lib/nextjs-build/global-containers.Dockerfile +15 -10
  107. package/lib/nextjs-build/nextjs-build.d.ts +339 -63
  108. package/lib/nextjs-build/nextjs-build.js +684 -327
  109. package/lib/nextjs-build/nextjs-build.test.d.ts +1 -0
  110. package/lib/nextjs-build/nextjs-build.test.js +412 -0
  111. package/lib/nextjs-build/patch-fetch.js +1 -1
  112. package/lib/nextjs-build/patch-fetch.test.js +166 -11
  113. package/lib/nextjs-build/regional-containers.Dockerfile +17 -10
  114. package/lib/nextjs-cache.d.ts +82 -4
  115. package/lib/nextjs-cache.js +186 -10
  116. package/lib/nextjs-cache.test.js +176 -1
  117. package/lib/nextjs-compute/nextjs-compute-base-props.d.ts +21 -8
  118. package/lib/nextjs-compute/nextjs-compute-base-props.js +41 -1
  119. package/lib/nextjs-compute/nextjs-compute-base-props.test.d.ts +1 -0
  120. package/lib/nextjs-compute/nextjs-compute-base-props.test.js +44 -0
  121. package/lib/nextjs-compute/nextjs-containers.d.ts +19 -0
  122. package/lib/nextjs-compute/nextjs-containers.js +57 -20
  123. package/lib/nextjs-compute/nextjs-functions.d.ts +122 -7
  124. package/lib/nextjs-compute/nextjs-functions.js +113 -67
  125. package/lib/nextjs-compute/nextjs-functions.test.d.ts +1 -0
  126. package/lib/nextjs-compute/nextjs-functions.test.js +99 -0
  127. package/lib/nextjs-distribution.d.ts +194 -23
  128. package/lib/nextjs-distribution.js +535 -62
  129. package/lib/nextjs-distribution.test.d.ts +1 -0
  130. package/lib/nextjs-distribution.test.js +697 -0
  131. package/lib/nextjs-post-deploy.d.ts +13 -5
  132. package/lib/nextjs-post-deploy.js +13 -10
  133. package/lib/nextjs-post-deploy.test.d.ts +1 -0
  134. package/lib/nextjs-post-deploy.test.js +34 -0
  135. package/lib/nextjs-static-assets.d.ts +17 -4
  136. package/lib/nextjs-static-assets.js +22 -5
  137. package/lib/root-constructs/nextjs-base-construct.d.ts +102 -58
  138. package/lib/root-constructs/nextjs-base-construct.js +139 -47
  139. package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
  140. package/lib/root-constructs/nextjs-global-containers.js +20 -71
  141. package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
  142. package/lib/root-constructs/nextjs-global-functions.js +52 -70
  143. package/lib/root-constructs/nextjs-regional-containers.d.ts +18 -5
  144. package/lib/root-constructs/nextjs-regional-containers.js +8 -15
  145. package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
  146. package/lib/root-constructs/nextjs-regional-functions.js +8 -26
  147. package/lib/runtime/api-gateway-path.d.ts +34 -0
  148. package/lib/runtime/api-gateway-path.js +71 -0
  149. package/lib/runtime/api-gateway-path.test.d.ts +1 -0
  150. package/lib/runtime/api-gateway-path.test.js +65 -0
  151. package/lib/runtime/case-redirect.d.ts +22 -0
  152. package/lib/runtime/case-redirect.js +63 -0
  153. package/lib/runtime/case-redirect.test.d.ts +1 -0
  154. package/lib/runtime/case-redirect.test.js +32 -0
  155. package/lib/runtime/core.d.ts +234 -0
  156. package/lib/runtime/core.js +1213 -0
  157. package/lib/runtime/core.test.d.ts +1 -0
  158. package/lib/runtime/core.test.js +1678 -0
  159. package/lib/runtime/deployment-root.d.ts +13 -0
  160. package/lib/runtime/deployment-root.js +29 -0
  161. package/lib/runtime/dispatch.d.ts +331 -0
  162. package/lib/runtime/dispatch.js +751 -0
  163. package/lib/runtime/dispatch.test.d.ts +1 -0
  164. package/lib/runtime/dispatch.test.js +853 -0
  165. package/lib/runtime/entrypoints.d.ts +57 -0
  166. package/lib/runtime/entrypoints.js +115 -0
  167. package/lib/runtime/http/abort-signal.d.ts +6 -0
  168. package/lib/runtime/http/abort-signal.js +19 -0
  169. package/lib/runtime/http/abort-signal.test.d.ts +1 -0
  170. package/lib/runtime/http/abort-signal.test.js +49 -0
  171. package/lib/runtime/http/node-server.d.ts +38 -0
  172. package/lib/runtime/http/node-server.js +99 -0
  173. package/lib/runtime/http/node-server.test.d.ts +1 -0
  174. package/lib/runtime/http/node-server.test.js +96 -0
  175. package/lib/runtime/http/node-sink.d.ts +16 -0
  176. package/lib/runtime/http/node-sink.js +48 -0
  177. package/lib/runtime/http/node-sink.test.d.ts +1 -0
  178. package/lib/runtime/http/node-sink.test.js +66 -0
  179. package/lib/runtime/http/request.d.ts +62 -0
  180. package/lib/runtime/http/request.js +134 -0
  181. package/lib/runtime/http/request.test.d.ts +1 -0
  182. package/lib/runtime/http/request.test.js +103 -0
  183. package/lib/runtime/http/response.d.ts +77 -0
  184. package/lib/runtime/http/response.js +229 -0
  185. package/lib/runtime/http/response.test.d.ts +1 -0
  186. package/lib/runtime/http/response.test.js +178 -0
  187. package/lib/runtime/http/sink.d.ts +73 -0
  188. package/lib/runtime/http/sink.js +188 -0
  189. package/lib/runtime/http/sink.test.d.ts +1 -0
  190. package/lib/runtime/http/sink.test.js +257 -0
  191. package/lib/runtime/image-utils.d.ts +78 -0
  192. package/lib/runtime/image-utils.js +129 -0
  193. package/lib/runtime/image-utils.test.d.ts +1 -0
  194. package/lib/runtime/image-utils.test.js +237 -0
  195. package/lib/runtime/image.d.ts +76 -0
  196. package/lib/runtime/image.js +296 -0
  197. package/lib/runtime/image.test.d.ts +1 -0
  198. package/lib/runtime/image.test.js +405 -0
  199. package/lib/runtime/lambda-event.d.ts +4 -0
  200. package/lib/runtime/lambda-event.js +113 -0
  201. package/lib/runtime/lambda-event.test.d.ts +1 -0
  202. package/lib/runtime/lambda-event.test.js +119 -0
  203. package/lib/runtime/lambda.mjs +41595 -0
  204. package/lib/runtime/load-module.d.ts +8 -0
  205. package/lib/runtime/load-module.js +35 -0
  206. package/lib/runtime/manifest.d.ts +215 -0
  207. package/lib/runtime/manifest.js +118 -0
  208. package/lib/runtime/middleware.d.ts +43 -0
  209. package/lib/runtime/middleware.js +113 -0
  210. package/lib/runtime/middleware.test.d.ts +1 -0
  211. package/lib/runtime/middleware.test.js +259 -0
  212. package/lib/runtime/next-modules.d.ts +55 -0
  213. package/lib/runtime/next-modules.js +124 -0
  214. package/lib/runtime/next-modules.test.d.ts +1 -0
  215. package/lib/runtime/next-modules.test.js +34 -0
  216. package/lib/runtime/public-files.d.ts +32 -0
  217. package/lib/runtime/public-files.js +117 -0
  218. package/lib/runtime/public-files.test.d.ts +1 -0
  219. package/lib/runtime/public-files.test.js +97 -0
  220. package/lib/runtime/server.mjs +41588 -0
  221. package/lib/runtime/static-files.d.ts +50 -0
  222. package/lib/runtime/static-files.js +266 -0
  223. package/lib/runtime/static-files.test.d.ts +1 -0
  224. package/lib/runtime/static-files.test.js +118 -0
  225. package/lib/runtime/util.d.ts +28 -0
  226. package/lib/runtime/util.js +79 -0
  227. package/lib/utils/base-path.d.ts +106 -13
  228. package/lib/utils/base-path.js +254 -41
  229. package/lib/utils/base-path.test.js +219 -51
  230. package/lib/utils/get-architecture.d.ts +7 -8
  231. package/lib/utils/get-architecture.js +12 -13
  232. package/lib/utils/zip-directory.d.ts +14 -0
  233. package/lib/utils/zip-directory.js +121 -0
  234. package/lib/utils/zip-directory.test.d.ts +1 -0
  235. package/lib/utils/zip-directory.test.js +99 -0
  236. package/package.json +22 -3
  237. package/scripts/capture-adapter-fixture.mjs +325 -0
  238. package/scripts/e2e-deploy.sh +398 -0
  239. package/scripts/e2e-harness/README.md +707 -0
  240. package/scripts/e2e-harness/app.js +370 -0
  241. package/scripts/e2e-harness/common.sh +249 -0
  242. package/scripts/e2e-harness/screen.mjs +302 -0
  243. package/scripts/e2e-harness/stage-proxy.mjs +145 -0
  244. package/scripts/e2e-harness/type-filters.mjs +47 -0
  245. package/scripts/e2e-logs.sh +75 -0
  246. package/scripts/e2e-offline.sh +91 -0
  247. package/scripts/e2e-sweep.sh +370 -0
  248. package/scripts/e2e-warm.sh +114 -0
  249. package/scripts/zero-config-build.mjs +418 -0
  250. package/docs/1k-concurrent-users-executions.png +0 -0
  251. package/docs/plans/adapter-runtime-release.md +0 -362
  252. package/docs/plans/phase-0-image-function-opt-in.md +0 -295
  253. package/lib/generated-structs/OptionalDockerImageFunctionProps.d.ts +0 -425
  254. package/lib/generated-structs/OptionalDockerImageFunctionProps.js +0 -3
  255. package/lib/image-optimization/handler-utils.d.ts +0 -34
  256. package/lib/image-optimization/handler-utils.js +0 -83
  257. package/lib/image-optimization/handler-utils.test.js +0 -178
  258. package/lib/nextjs-build/functions.Dockerfile +0 -31
  259. package/lib/nextjs-compute/nextjs-image-function.d.ts +0 -53
  260. package/lib/nextjs-compute/nextjs-image-function.js +0 -59
  261. package/lib/utils/experimental-flags.d.ts +0 -24
  262. package/lib/utils/experimental-flags.js +0 -30
  263. /package/lib/{image-optimization/handler-utils.test.d.ts → adapter/adapter.test.d.ts} +0 -0
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Deletes compatibility-harness stacks: a shard's own shared stack after its run
4
+ # (`--shared`), and orphans a cancelled, timed-out or dead shard left behind.
5
+ #
6
+ # Dry run by default: prints what it would delete and exits 0. Pass `--apply`
7
+ # to actually delete. Only stacks that are named
8
+ # `hrns-*` *and* tagged `cdk-nextjs:harness=1` *and* older than
9
+ # HARNESS_SWEEP_MAX_AGE_HOURS (default 6) are ever considered. "Older" is
10
+ # measured from the stack's last sign of use, not its creation: see
11
+ # `idle_hours` below.
12
+ #
13
+ # A stack in DELETE_FAILED is a candidate like any other - it is still holding
14
+ # whatever it failed to delete - and goes through the same gates.
15
+ #
16
+ # `--wait` blocks until every delete finishes and exits non-zero if any ends in
17
+ # DELETE_FAILED, so a caller that deploys into the same name next (a CI shard)
18
+ # never starts while the old stack is still DELETE_IN_PROGRESS.
19
+ #
20
+ # `--stack NAME` (or `--shared`) narrows the sweep to one stack by name and drops
21
+ # the age floor. That is the *only* safe way to delete a stack minutes after
22
+ # creating it: `HARNESS_SWEEP_MAX_AGE_HOURS=0` alone would also match every other
23
+ # `hrns-*` stack in the account, including another shard's, or one a developer
24
+ # is running against right then (`HARNESS_SHARED_STACK_SUFFIX=dev-$USER`).
25
+ #
26
+ # Usage:
27
+ # scripts/e2e-sweep.sh # list candidates
28
+ # scripts/e2e-sweep.sh --apply # delete them
29
+ # scripts/e2e-sweep.sh --apply --shared # delete just the shared stack
30
+ # scripts/e2e-sweep.sh --apply --stack hrns-app-1a2b3c4d
31
+ # scripts/e2e-sweep.sh --apply --shared --wait # and block until it is gone
32
+ set -euo pipefail
33
+
34
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ # shellcheck source=scripts/e2e-harness/common.sh
36
+ source "$SCRIPT_DIR/e2e-harness/common.sh"
37
+
38
+ APPLY=0
39
+ ONLY_STACK=""
40
+ WAIT=0
41
+ while [ "$#" -gt 0 ]; do
42
+ case "$1" in
43
+ --apply) APPLY=1 ;;
44
+ --wait) WAIT=1 ;;
45
+ --shared) ONLY_STACK="$(harness_stack_name)" ;;
46
+ --stack)
47
+ ONLY_STACK="${2:-}"
48
+ if [ -z "$ONLY_STACK" ]; then
49
+ echo "$0: --stack needs a stack name" >&2
50
+ exit 2
51
+ fi
52
+ shift
53
+ ;;
54
+ *)
55
+ echo "usage: $0 [--apply] [--shared|--stack NAME] [--wait]" >&2
56
+ exit 2
57
+ ;;
58
+ esac
59
+ shift
60
+ done
61
+
62
+ # Naming one stack replaces the age floor rather than adding to it: the name is
63
+ # what makes the deletion safe, and the caller asking for it by name is the caller
64
+ # that created it.
65
+ if [ -n "$ONLY_STACK" ]; then
66
+ MAX_AGE_HOURS=0
67
+ else
68
+ MAX_AGE_HOURS="${HARNESS_SWEEP_MAX_AGE_HOURS:-6}"
69
+ fi
70
+ export HARNESS_STACK_PREFIX
71
+
72
+ # An age floor, not just a tag match: a concurrent run's stacks carry the same
73
+ # tag, and deleting one out from under a running test would look like an adapter
74
+ # bug. A harness deploy plus its test finishes well inside an hour.
75
+ #
76
+ # The floor is measured from a stack's last sign of use (`idle_hours`), not just
77
+ # CloudFormation's `CreationTime` / `LastUpdatedTime`: a shared stack is reused by
78
+ # name for hours and deployed into by hotswap, which never goes through
79
+ # CloudFormation, so neither timestamp moves. What does move is the server
80
+ # function's `LastModified` (or the ECS service's latest deployment), which every
81
+ # test file's hotswap rewrites.
82
+ #
83
+ # One stack by name is described by name. Listing every stack needs
84
+ # `cloudformation:ListStacks`, which a shard's own cleanup (`--shared`) should
85
+ # not depend on: before GitHubActionRole had it, that cleanup failed on
86
+ # AccessDenied and left its stack behind. A named stack that is already gone is
87
+ # simply not a candidate.
88
+ list_stacks() {
89
+ if [ -z "$ONLY_STACK" ]; then
90
+ aws cloudformation describe-stacks --output json
91
+ return
92
+ fi
93
+ local out
94
+ if out="$(aws cloudformation describe-stacks --stack-name "$ONLY_STACK" --output json 2>&1)"; then
95
+ printf '%s' "$out"
96
+ return
97
+ fi
98
+ case "$out" in
99
+ *"does not exist"*) printf '{"Stacks":[]}' ;;
100
+ *)
101
+ printf '%s\n' "$out" >&2
102
+ return 1
103
+ ;;
104
+ esac
105
+ }
106
+
107
+ # The tagged `hrns-*` stacks that are not already being deleted, one per line as
108
+ # `name status created updated probe`, tab-separated.
109
+ # DELETE_IN_PROGRESS is already going; DELETE_COMPLETE is gone. A DELETE_FAILED
110
+ # stack is exactly what needs another attempt.
111
+ LISTING="$(list_stacks)"
112
+ CANDIDATES="$(
113
+ printf '%s' "$LISTING" | node -e '
114
+ const { HARNESS_TAG_KEY, HARNESS_TAG_VALUE, HARNESS_STACK_PREFIX } = process.env;
115
+ let raw = "";
116
+ process.stdin.on("data", (chunk) => (raw += chunk));
117
+ process.stdin.on("end", () => {
118
+ for (const stack of JSON.parse(raw).Stacks || []) {
119
+ if (!stack.StackName.startsWith(HARNESS_STACK_PREFIX)) continue;
120
+ if (["DELETE_IN_PROGRESS", "DELETE_COMPLETE"].includes(stack.StackStatus)) continue;
121
+ const tagged = (stack.Tags || []).some(
122
+ (tag) => tag.Key === HARNESS_TAG_KEY && tag.Value === HARNESS_TAG_VALUE,
123
+ );
124
+ if (!tagged) continue;
125
+ // What a hotswap moves: the server function for the Functions types,
126
+ // the ECS service (as `ecs:<cluster>/<service>`) for the Containers types.
127
+ const output = (key) => ((stack.Outputs || []).find((o) => o.OutputKey === key) || {}).OutputValue;
128
+ const fn = output("ServerFunctionName");
129
+ const cluster = output("EcsClusterName");
130
+ const service = output("EcsServiceName");
131
+ const probe = fn || (cluster && service ? `ecs:${cluster}/${service}` : "-");
132
+ console.log(
133
+ [stack.StackName, stack.StackStatus, stack.CreationTime, stack.LastUpdatedTime || "-", probe].join("\t"),
134
+ );
135
+ }
136
+ });
137
+ '
138
+ )"
139
+
140
+ # Hours, rounded down to 0.1, since the most recent of a stack's CloudFormation
141
+ # timestamps and its probe's last hotswap. Read-only. A function or service that
142
+ # is gone (a DELETE_FAILED stack, say) contributes nothing - including a service
143
+ # ECS reports MISSING in a cluster that is still there, which comes back with no
144
+ # `services` at all, hence the `|| []`. Any other read failure
145
+ # (throttling, AccessDenied) returns non-zero instead, and the caller keeps the
146
+ # stack: without that timestamp, the floor can't tell an abandoned stack from one
147
+ # being hotswapped into right now.
148
+ idle_hours() {
149
+ local created="$1" updated="$2" probe="$3"
150
+ local modified=""
151
+ if [ "${probe#ecs:}" != "$probe" ]; then
152
+ local cluster service
153
+ cluster="${probe#ecs:}"
154
+ service="${cluster#*/}"
155
+ cluster="${cluster%%/*}"
156
+ if ! modified="$(aws ecs describe-services --cluster "$cluster" --services "$service" \
157
+ --query 'max_by(services[0].deployments || `[]`, &updatedAt).updatedAt' --output text 2>&1)"; then
158
+ case "$modified" in
159
+ *ClusterNotFoundException*) modified="" ;;
160
+ *)
161
+ echo "sweep: cannot read $probe's deployments: $modified" >&2
162
+ return 1
163
+ ;;
164
+ esac
165
+ fi
166
+ elif [ "$probe" != "-" ]; then
167
+ if ! modified="$(aws lambda get-function-configuration --function-name "$probe" \
168
+ --query LastModified --output text 2>&1)"; then
169
+ case "$modified" in
170
+ *ResourceNotFoundException*) modified="" ;;
171
+ *)
172
+ echo "sweep: cannot read $probe's LastModified: $modified" >&2
173
+ return 1
174
+ ;;
175
+ esac
176
+ fi
177
+ fi
178
+ node -e '
179
+ // ISO timestamps, and from ECS possibly epoch seconds, depending on the
180
+ // CLI'"'"'s `cli_timestamp_format`.
181
+ const ms = (t) => !t || t === "-" || t === "None" ? 0
182
+ : /^[\d.]+$/.test(t) ? Number(t) * 1000
183
+ : new Date(t).getTime() || 0;
184
+ const hours = (Date.now() - Math.max(...process.argv.slice(1).map(ms))) / 3_600_000;
185
+ process.stdout.write((Math.floor(hours * 10) / 10).toFixed(1));
186
+ ' "$created" "$updated" "$modified"
187
+ }
188
+
189
+ # A named stack mid-operation (a full update a timed-out deploy left running
190
+ # server-side, or its rollback) can't be deleted until it settles:
191
+ # `delete-stack` is rejected, and under `set -e` that would end the sweep with
192
+ # the stack still up. So poll until it does, bounded at 20 minutes, and print
193
+ # the status it settled on - or nothing, if it is gone. Diagnostics on stderr.
194
+ # (DELETE_IN_PROGRESS is fine as it is: `delete-stack` on it is a no-op.)
195
+ settled_status() {
196
+ local status
197
+ for _ in $(seq 1 80); do
198
+ status="$(harness_stack_status "$1")" || return 1
199
+ case "$status" in
200
+ REVIEW_IN_PROGRESS | DELETE_IN_PROGRESS) break ;;
201
+ *_IN_PROGRESS)
202
+ echo "sweep: $1 is $status; waiting for it to settle" >&2
203
+ sleep 15
204
+ ;;
205
+ *) break ;;
206
+ esac
207
+ done
208
+ printf '%s' "$status"
209
+ }
210
+
211
+ DESCRIPTION="harness stacks unused for ${MAX_AGE_HOURS}h"
212
+ if [ -n "$ONLY_STACK" ]; then
213
+ DESCRIPTION="harness stack ${ONLY_STACK}"
214
+ fi
215
+
216
+ FOUND=0
217
+ DELETED=()
218
+ # On fd 4 rather than stdin, so that nothing the loop body runs can read the list
219
+ # out from under it.
220
+ while IFS=$'\t' read -r name status created updated probe <&4; do
221
+ [ -n "$name" ] || continue
222
+ # An operation in flight is someone using the stack. An account-wide sweep
223
+ # leaves it to the next sweep; a named one waits for it below. (A change set
224
+ # that was never executed, REVIEW_IN_PROGRESS, never settles by itself.)
225
+ if [ -z "$ONLY_STACK" ] && [ "$status" != "REVIEW_IN_PROGRESS" ] \
226
+ && [ "${status%_IN_PROGRESS}" != "$status" ]; then
227
+ echo "sweep: keeping $name - $status"
228
+ continue
229
+ fi
230
+ # Skipped when one stack is named, since that drops the floor altogether.
231
+ used=""
232
+ if [ "$MAX_AGE_HOURS" != "0" ]; then
233
+ if ! idle="$(idle_hours "$created" "$updated" "$probe")"; then
234
+ echo "sweep: keeping $name - could not confirm it is unused"
235
+ continue
236
+ fi
237
+ if ! awk -v idle="$idle" -v max="$MAX_AGE_HOURS" 'BEGIN { exit !(idle >= max) }'; then
238
+ echo "sweep: keeping $name - in use ${idle}h ago"
239
+ continue
240
+ fi
241
+ used=", last used ${idle}h ago"
242
+ fi
243
+ FOUND=1
244
+ if [ "$APPLY" != "1" ]; then
245
+ echo "sweep: would delete $name ($status$used)"
246
+ continue
247
+ fi
248
+ if [ -n "$ONLY_STACK" ]; then
249
+ # A hotswap an orphaned `cdk deploy` is still running is invisible to
250
+ # CloudFormation, so `settled_status` alone would not wait for it.
251
+ harness_wait_for_cdk "$name"
252
+ if ! status="$(settled_status "$name")"; then
253
+ echo "sweep: keeping $name - could not re-read its status" >&2
254
+ exit 1
255
+ fi
256
+ case "$status" in
257
+ "")
258
+ echo "sweep: $name is already gone"
259
+ continue
260
+ ;;
261
+ REVIEW_IN_PROGRESS | DELETE_IN_PROGRESS) ;;
262
+ *_IN_PROGRESS)
263
+ echo "sweep: $name is still $status after 20 minutes; not deleting it" >&2
264
+ exit 1
265
+ ;;
266
+ esac
267
+ else
268
+ # The listing's status can be minutes old by now; a deploy that started
269
+ # since is someone using the stack.
270
+ if ! status="$(harness_stack_status "$name")"; then
271
+ echo "sweep: keeping $name - could not re-read its status"
272
+ continue
273
+ fi
274
+ case "$status" in
275
+ "")
276
+ echo "sweep: $name is already gone"
277
+ continue
278
+ ;;
279
+ REVIEW_IN_PROGRESS) ;;
280
+ *_IN_PROGRESS)
281
+ echo "sweep: keeping $name - $status"
282
+ continue
283
+ ;;
284
+ esac
285
+ fi
286
+ # Re-check the tag immediately before deleting rather than trusting the
287
+ # listing, and after the settle wait, which can outlast the stack it began on.
288
+ if ! harness_stack_is_ours "$name"; then
289
+ echo "sweep: skipping $name - no longer tagged ${HARNESS_TAG_KEY}=${HARNESS_TAG_VALUE}"
290
+ continue
291
+ fi
292
+ echo "sweep: deleting $name ($status$used)"
293
+ harness_stop_proxy "$name"
294
+ aws cloudformation delete-stack --stack-name "$name"
295
+ DELETED+=("$name")
296
+ done 4<<<"$CANDIDATES"
297
+
298
+ # Log groups outlive their stacks: the ECS task's is the stack's own but
299
+ # retained, and Lambda's and Container Insights' are created by AWS outside the
300
+ # stack, so no stack setting can expire them. Two days of runs left ~450. Only on
301
+ # an account-wide sweep, and only a group whose stack no longer exists (the name
302
+ # starts with `<stack>-`) and that is over an hour old - a stack being created
303
+ # right now is listed, so its groups are kept.
304
+ sweep_log_groups() {
305
+ local stacks
306
+ stacks="$(aws cloudformation describe-stacks \
307
+ --query "Stacks[?starts_with(StackName, '${HARNESS_STACK_PREFIX}')].StackName" \
308
+ --output text | tr '\t' '\n')" || return 1
309
+ local cutoff=$((($(date +%s) - 3600) * 1000))
310
+ local prefix group created base stack live count=0
311
+ for prefix in "${HARNESS_STACK_PREFIX}" "/aws/lambda/${HARNESS_STACK_PREFIX}" \
312
+ "/aws/ecs/containerinsights/${HARNESS_STACK_PREFIX}"; do
313
+ while IFS=$'\t' read -r group created; do
314
+ [ -n "$group" ] || continue
315
+ [ "$created" -lt "$cutoff" ] || continue
316
+ base="${group#/aws/lambda/}"
317
+ base="${base#/aws/ecs/containerinsights/}"
318
+ live=0
319
+ while IFS= read -r stack; do
320
+ if [ -n "$stack" ] && { [ "$base" = "$stack" ] || [ "${base#"$stack"-}" != "$base" ]; }; then
321
+ live=1
322
+ break
323
+ fi
324
+ done <<<"$stacks"
325
+ [ "$live" = "0" ] || continue
326
+ count=$((count + 1))
327
+ if [ "$APPLY" = "1" ]; then
328
+ aws logs delete-log-group --log-group-name "$group"
329
+ else
330
+ echo "sweep: would delete log group $group"
331
+ fi
332
+ done < <(aws logs describe-log-groups --log-group-name-prefix "$prefix" \
333
+ --query 'logGroups[].[logGroupName, creationTime]' --output text)
334
+ done
335
+ if [ "$APPLY" = "1" ]; then
336
+ echo "sweep: deleted $count log groups of stacks that are gone"
337
+ else
338
+ echo "sweep: $count log groups of stacks that are gone"
339
+ fi
340
+ }
341
+ if [ -z "$ONLY_STACK" ]; then
342
+ sweep_log_groups || echo "sweep: could not sweep log groups" >&2
343
+ fi
344
+
345
+ if [ "$FOUND" = "0" ]; then
346
+ echo "sweep: no ${DESCRIPTION}"
347
+ exit 0
348
+ fi
349
+ if [ "$APPLY" != "1" ]; then
350
+ echo "sweep: dry run; re-run with --apply to delete the above"
351
+ exit 0
352
+ fi
353
+
354
+ if [ "$WAIT" = "1" ]; then
355
+ FAILED=0
356
+ for name in "${DELETED[@]+"${DELETED[@]}"}"; do
357
+ echo "sweep: waiting for $name to finish deleting"
358
+ if ! aws cloudformation wait stack-delete-complete --stack-name "$name"; then
359
+ FAILED=1
360
+ echo "sweep: $name did not delete cleanly; its last failure events:" >&2
361
+ aws cloudformation describe-stack-events --stack-name "$name" \
362
+ --query "StackEvents[?ends_with(ResourceStatus, 'FAILED')] | [0:5].[LogicalResourceId, ResourceStatusReason]" \
363
+ --output text >&2 || true
364
+ fi
365
+ done
366
+ if [ "$FAILED" = "1" ]; then
367
+ echo "sweep: at least one delete failed; the next sweep retries DELETE_FAILED stacks" >&2
368
+ exit 1
369
+ fi
370
+ fi
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Creates the shared harness stack before `run-tests.js` starts, so that no test
4
+ # file pays for it.
5
+ #
6
+ # The first deploy creates a CloudFront distribution and everything behind it,
7
+ # measured at ~240s. The harness runs `createNext` inside jest's `beforeAll`, so
8
+ # that wait is charged against NEXT_E2E_TEST_TIMEOUT - 240000 in CI, i.e. the same
9
+ # order - and the first test file of every run fails wholesale, then passes on
10
+ # retry once the stack exists. Retries make that survivable, not correct: it
11
+ # burns a retry the next real failure needs, and a create slower than two retries
12
+ # goes red.
13
+ #
14
+ # So: deploy once here, from a throwaway app, and let every test file take the
15
+ # update path. Re-running against an existing stack is a fast no-op update, so
16
+ # this is safe to run unconditionally.
17
+ #
18
+ # @see scripts/e2e-harness/README.md
19
+ set -euo pipefail
20
+
21
+ : "${ADAPTER_DIR:?must be set to the cdk-nextjs checkout}"
22
+ HARNESS_DIR="$ADAPTER_DIR/scripts/e2e-harness"
23
+ # shellcheck source=scripts/e2e-harness/common.sh
24
+ source "$HARNESS_DIR/common.sh"
25
+
26
+ # The version the harness's own fixtures will build with. Read from this repo so
27
+ # the warm app cannot drift from it: the *installed* version, exact, not the
28
+ # `package.json` range, which would let the warm app resolve a newer release
29
+ # than the one `@next/routing` and the adapter fixtures are pinned to.
30
+ NEXT_VERSION="$(node -e '
31
+ const path = require.resolve("next/package.json", { paths: [process.argv[1]] });
32
+ process.stdout.write(require(path).version);
33
+ ' "$ADAPTER_DIR")"
34
+
35
+ # `e2e-deploy.sh` installs with `corepack pnpm`, and corepack takes the version
36
+ # from the app's own `packageManager` field - falling back to a latest it has
37
+ # never downloaded, which fails with MODULE_NOT_FOUND offline. The harness's real
38
+ # fixtures pin this, so the warm app has to as well.
39
+ PACKAGE_MANAGER="$(node -e '
40
+ const pkg = require(process.argv[1] + "/package.json");
41
+ if (!pkg.packageManager) throw new Error("no `packageManager` in " + process.argv[1]);
42
+ process.stdout.write(pkg.packageManager);
43
+ ' "$ADAPTER_DIR")"
44
+
45
+ WARM_DIR="$(mktemp -d "${TMPDIR:-/tmp}/hrns-warm-XXXXXX")"
46
+ # Deleted on every exit. Nothing is lost on a failure: `e2e-deploy.sh` tees its
47
+ # build and deploy output to stderr, so the job log already has what the files
48
+ # hold.
49
+ trap 'rm -rf "$WARM_DIR"' EXIT
50
+
51
+ echo "warm: building a throwaway app in $WARM_DIR (next@$NEXT_VERSION)"
52
+
53
+ # The smallest app that still produces every resource the real fixtures need:
54
+ # one static route, one dynamic route, and a `public/` file. Without a dynamic
55
+ # route there is no server function worth deploying; without `public/` the
56
+ # distribution would be missing the static behaviors the first real fixture then
57
+ # adds, which is a CloudFront propagation this is meant to have already paid.
58
+ mkdir -p "$WARM_DIR/app/dynamic" "$WARM_DIR/public"
59
+
60
+ cat >"$WARM_DIR/package.json" <<EOF
61
+ {
62
+ "name": "hrns-warm",
63
+ "private": true,
64
+ "packageManager": "$PACKAGE_MANAGER",
65
+ "scripts": { "build": "next build" },
66
+ "dependencies": {
67
+ "next": "$NEXT_VERSION",
68
+ "react": "^19.0.0",
69
+ "react-dom": "^19.0.0"
70
+ }
71
+ }
72
+ EOF
73
+
74
+ cat >"$WARM_DIR/next.config.js" <<'EOF'
75
+ module.exports = {};
76
+ EOF
77
+
78
+ cat >"$WARM_DIR/app/layout.js" <<'EOF'
79
+ export default function RootLayout({ children }) {
80
+ return (
81
+ <html lang="en">
82
+ <body>{children}</body>
83
+ </html>
84
+ );
85
+ }
86
+ EOF
87
+
88
+ cat >"$WARM_DIR/app/page.js" <<'EOF'
89
+ export default function Page() {
90
+ return <main>harness warm-up</main>;
91
+ }
92
+ EOF
93
+
94
+ cat >"$WARM_DIR/app/dynamic/page.js" <<'EOF'
95
+ export const dynamic = "force-dynamic";
96
+ export default function Page() {
97
+ return <main>harness warm-up {Date.now()}</main>;
98
+ }
99
+ EOF
100
+
101
+ printf 'harness warm-up\n' >"$WARM_DIR/public/warm.txt"
102
+
103
+ # Through `e2e-deploy.sh` rather than a second `cdk deploy` call of its own: the
104
+ # point is to exercise the same path the test files take, so a warm-up that
105
+ # succeeds means their deploys will too. It installs, builds through the adapter
106
+ # and deploys, and prints the URL on stdout.
107
+ # Named in the log because a sharded run has one of these per shard, and which
108
+ # stack a warm-up was for is otherwise only inferable from the job name.
109
+ STACK_NAME="$(harness_stack_name)"
110
+
111
+ echo "warm: deploying $STACK_NAME"
112
+ cd "$WARM_DIR"
113
+ URL="$("$ADAPTER_DIR/scripts/e2e-deploy.sh")"
114
+ echo "warm: shared stack ready at $URL"