cdk-nextjs 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsii +2467 -2714
- package/.jsii.tabl.json +1 -1
- package/API.md +1725 -1909
- package/CLAUDE.md +2 -1
- package/README.md +429 -130
- package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +152 -170
- package/assets/lambdas/redeploy-stage/redeploy-stage.lambda/index.js +875 -0
- package/docs/adapter-runtime.md +185 -0
- package/docs/breaking-changes.md +687 -4
- package/docs/caching-guide.md +156 -112
- package/docs/cdk-nextjs-cfn-exec-policy.json +2 -1
- package/docs/development-guide.md +1 -1
- package/docs/harness-coverage.md +598 -0
- package/docs/init-cache-deployment.md +32 -1
- package/docs/next-build-output-guide.md +30 -20
- package/examples/README.md +5 -1
- package/lib/adapter/__fixtures__/app-playground-base-path.json +4926 -0
- package/lib/adapter/__fixtures__/app-playground.json +4917 -0
- package/lib/adapter/__fixtures__/pages-i18n.json +1728 -0
- package/lib/adapter/adapter.mjs +1548 -185
- package/lib/adapter/adapter.test.js +106 -0
- package/lib/adapter/aws-cache-store.d.ts +489 -0
- package/lib/adapter/aws-cache-store.js +1004 -0
- package/lib/adapter/aws-cache-store.test.d.ts +1 -0
- package/lib/adapter/aws-cache-store.test.js +361 -0
- package/lib/adapter/build-outputs.d.ts +80 -0
- package/lib/adapter/build-outputs.js +852 -0
- package/lib/adapter/build-outputs.test.d.ts +1 -0
- package/lib/adapter/build-outputs.test.js +861 -0
- package/lib/adapter/cache-handler.d.ts +9 -2
- package/lib/adapter/cache-handler.js +72 -96
- package/lib/adapter/cache-handler.mjs +18956 -17774
- package/lib/adapter/cache-handler.test.js +263 -31
- package/lib/adapter/cache-kinds.d.ts +32 -0
- package/lib/adapter/cache-kinds.js +35 -0
- package/lib/adapter/cache-kinds.test.d.ts +1 -0
- package/lib/adapter/cache-kinds.test.js +69 -0
- package/lib/adapter/cache-utils.d.ts +90 -0
- package/lib/adapter/cache-utils.js +199 -4
- package/lib/adapter/cache-utils.test.d.ts +1 -0
- package/lib/adapter/cache-utils.test.js +179 -0
- package/lib/adapter/function-groups.d.ts +215 -0
- package/lib/adapter/function-groups.js +1099 -0
- package/lib/adapter/function-groups.test.d.ts +1 -0
- package/lib/adapter/function-groups.test.js +743 -0
- package/lib/adapter/init-cache.d.ts +6 -0
- package/lib/adapter/init-cache.js +245 -0
- package/lib/adapter/init-cache.test.d.ts +1 -0
- package/lib/adapter/init-cache.test.js +137 -0
- package/lib/adapter/local-file-cache-handler.d.ts +23 -0
- package/lib/adapter/local-file-cache-handler.js +52 -1
- package/lib/adapter/memory-cache-handler.d.ts +26 -17
- package/lib/adapter/memory-cache-handler.js +32 -50
- package/lib/adapter/memory-cache-handler.test.js +105 -85
- package/lib/adapter/s3-cache-handler.d.ts +179 -40
- package/lib/adapter/s3-cache-handler.js +807 -277
- package/lib/adapter/s3-cache-handler.test.js +1186 -31
- package/lib/adapter/use-cache-common.d.ts +90 -0
- package/lib/adapter/use-cache-common.js +250 -0
- package/lib/adapter/use-cache-common.test.d.ts +1 -0
- package/lib/adapter/use-cache-common.test.js +646 -0
- package/lib/adapter/use-cache-default-handler.d.ts +24 -0
- package/lib/adapter/use-cache-default-handler.js +90 -0
- package/lib/adapter/use-cache-default-handler.mjs +38645 -0
- package/lib/adapter/use-cache-handlers.test.d.ts +1 -0
- package/lib/adapter/use-cache-handlers.test.js +349 -0
- package/lib/adapter/use-cache-remote-handler.d.ts +26 -0
- package/lib/adapter/use-cache-remote-handler.js +186 -0
- package/lib/{image-optimization/handler.mjs → adapter/use-cache-remote-handler.mjs} +33572 -38798
- package/lib/generated-structs/OptionalNextjsApiProps.d.ts +59 -0
- package/lib/generated-structs/OptionalNextjsApiProps.js +3 -0
- package/lib/generated-structs/OptionalNextjsCacheProps.d.ts +15 -6
- package/lib/generated-structs/OptionalNextjsCacheProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsContainersProps.d.ts +25 -11
- package/lib/generated-structs/OptionalNextjsContainersProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +31 -9
- package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
- package/lib/generated-structs/OptionalNextjsPostDeployProps.d.ts +13 -0
- package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
- package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +11 -2
- package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.js +1 -1
- package/lib/index.d.ts +6 -7
- package/lib/index.js +2 -4
- package/lib/lambdas/post-deploy/post-deploy.lambda.js +3 -2
- package/lib/lambdas/post-deploy/prune-cache-bucket.d.ts +7 -3
- package/lib/lambdas/post-deploy/prune-cache-bucket.js +57 -71
- package/lib/lambdas/post-deploy/prune-cache-bucket.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-cache-bucket.test.js +114 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.js +23 -31
- package/lib/lambdas/post-deploy/prune-revalidation-table.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +71 -0
- package/lib/lambdas/post-deploy/prune-s3.d.ts +19 -3
- package/lib/lambdas/post-deploy/prune-s3.js +54 -15
- package/lib/lambdas/post-deploy/prune-s3.test.d.ts +1 -0
- package/lib/lambdas/post-deploy/prune-s3.test.js +255 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage-function.d.ts +13 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage-function.js +55 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.d.ts +24 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.js +68 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.d.ts +20 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.js +24 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.test.d.ts +1 -0
- package/lib/lambdas/redeploy-stage/redeploy-stage.test.js +93 -0
- package/lib/nextjs-api.d.ts +107 -18
- package/lib/nextjs-api.js +349 -39
- package/lib/nextjs-api.test.js +582 -1
- package/lib/nextjs-build/global-containers.Dockerfile +15 -10
- package/lib/nextjs-build/nextjs-build.d.ts +339 -63
- package/lib/nextjs-build/nextjs-build.js +684 -327
- package/lib/nextjs-build/nextjs-build.test.d.ts +1 -0
- package/lib/nextjs-build/nextjs-build.test.js +412 -0
- package/lib/nextjs-build/patch-fetch.js +1 -1
- package/lib/nextjs-build/patch-fetch.test.js +166 -11
- package/lib/nextjs-build/regional-containers.Dockerfile +17 -10
- package/lib/nextjs-cache.d.ts +82 -4
- package/lib/nextjs-cache.js +186 -10
- package/lib/nextjs-cache.test.js +176 -1
- package/lib/nextjs-compute/nextjs-compute-base-props.d.ts +21 -8
- package/lib/nextjs-compute/nextjs-compute-base-props.js +41 -1
- package/lib/nextjs-compute/nextjs-compute-base-props.test.d.ts +1 -0
- package/lib/nextjs-compute/nextjs-compute-base-props.test.js +44 -0
- package/lib/nextjs-compute/nextjs-containers.d.ts +19 -0
- package/lib/nextjs-compute/nextjs-containers.js +57 -20
- package/lib/nextjs-compute/nextjs-functions.d.ts +122 -7
- package/lib/nextjs-compute/nextjs-functions.js +113 -67
- package/lib/nextjs-compute/nextjs-functions.test.d.ts +1 -0
- package/lib/nextjs-compute/nextjs-functions.test.js +99 -0
- package/lib/nextjs-distribution.d.ts +194 -23
- package/lib/nextjs-distribution.js +535 -62
- package/lib/nextjs-distribution.test.d.ts +1 -0
- package/lib/nextjs-distribution.test.js +697 -0
- package/lib/nextjs-post-deploy.d.ts +23 -3
- package/lib/nextjs-post-deploy.js +14 -10
- package/lib/nextjs-post-deploy.test.d.ts +1 -0
- package/lib/nextjs-post-deploy.test.js +34 -0
- package/lib/nextjs-static-assets.d.ts +23 -4
- package/lib/nextjs-static-assets.js +29 -6
- package/lib/nextjs-static-assets.test.js +20 -1
- package/lib/root-constructs/nextjs-base-construct.d.ts +105 -45
- package/lib/root-constructs/nextjs-base-construct.js +144 -34
- package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
- package/lib/root-constructs/nextjs-global-containers.js +20 -70
- package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
- package/lib/root-constructs/nextjs-global-functions.js +52 -69
- package/lib/root-constructs/nextjs-regional-containers.d.ts +25 -5
- package/lib/root-constructs/nextjs-regional-containers.js +17 -15
- package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
- package/lib/root-constructs/nextjs-regional-functions.js +8 -25
- package/lib/runtime/api-gateway-path.d.ts +34 -0
- package/lib/runtime/api-gateway-path.js +71 -0
- package/lib/runtime/api-gateway-path.test.d.ts +1 -0
- package/lib/runtime/api-gateway-path.test.js +65 -0
- package/lib/runtime/case-redirect.d.ts +22 -0
- package/lib/runtime/case-redirect.js +63 -0
- package/lib/runtime/case-redirect.test.d.ts +1 -0
- package/lib/runtime/case-redirect.test.js +32 -0
- package/lib/runtime/core.d.ts +234 -0
- package/lib/runtime/core.js +1213 -0
- package/lib/runtime/core.test.d.ts +1 -0
- package/lib/runtime/core.test.js +1678 -0
- package/lib/runtime/deployment-root.d.ts +13 -0
- package/lib/runtime/deployment-root.js +29 -0
- package/lib/runtime/dispatch.d.ts +331 -0
- package/lib/runtime/dispatch.js +751 -0
- package/lib/runtime/dispatch.test.d.ts +1 -0
- package/lib/runtime/dispatch.test.js +853 -0
- package/lib/runtime/entrypoints.d.ts +57 -0
- package/lib/runtime/entrypoints.js +115 -0
- package/lib/runtime/http/abort-signal.d.ts +6 -0
- package/lib/runtime/http/abort-signal.js +19 -0
- package/lib/runtime/http/abort-signal.test.d.ts +1 -0
- package/lib/runtime/http/abort-signal.test.js +49 -0
- package/lib/runtime/http/node-server.d.ts +38 -0
- package/lib/runtime/http/node-server.js +99 -0
- package/lib/runtime/http/node-server.test.d.ts +1 -0
- package/lib/runtime/http/node-server.test.js +96 -0
- package/lib/runtime/http/node-sink.d.ts +16 -0
- package/lib/runtime/http/node-sink.js +48 -0
- package/lib/runtime/http/node-sink.test.d.ts +1 -0
- package/lib/runtime/http/node-sink.test.js +66 -0
- package/lib/runtime/http/request.d.ts +62 -0
- package/lib/runtime/http/request.js +134 -0
- package/lib/runtime/http/request.test.d.ts +1 -0
- package/lib/runtime/http/request.test.js +103 -0
- package/lib/runtime/http/response.d.ts +77 -0
- package/lib/runtime/http/response.js +229 -0
- package/lib/runtime/http/response.test.d.ts +1 -0
- package/lib/runtime/http/response.test.js +178 -0
- package/lib/runtime/http/sink.d.ts +73 -0
- package/lib/runtime/http/sink.js +188 -0
- package/lib/runtime/http/sink.test.d.ts +1 -0
- package/lib/runtime/http/sink.test.js +257 -0
- package/lib/runtime/image-utils.d.ts +78 -0
- package/lib/runtime/image-utils.js +129 -0
- package/lib/runtime/image-utils.test.d.ts +1 -0
- package/lib/runtime/image-utils.test.js +237 -0
- package/lib/runtime/image.d.ts +76 -0
- package/lib/runtime/image.js +296 -0
- package/lib/runtime/image.test.d.ts +1 -0
- package/lib/runtime/image.test.js +405 -0
- package/lib/runtime/lambda-event.d.ts +4 -0
- package/lib/runtime/lambda-event.js +113 -0
- package/lib/runtime/lambda-event.test.d.ts +1 -0
- package/lib/runtime/lambda-event.test.js +119 -0
- package/lib/runtime/lambda.mjs +41595 -0
- package/lib/runtime/load-module.d.ts +8 -0
- package/lib/runtime/load-module.js +35 -0
- package/lib/runtime/manifest.d.ts +215 -0
- package/lib/runtime/manifest.js +118 -0
- package/lib/runtime/middleware.d.ts +43 -0
- package/lib/runtime/middleware.js +113 -0
- package/lib/runtime/middleware.test.d.ts +1 -0
- package/lib/runtime/middleware.test.js +259 -0
- package/lib/runtime/next-modules.d.ts +55 -0
- package/lib/runtime/next-modules.js +124 -0
- package/lib/runtime/next-modules.test.d.ts +1 -0
- package/lib/runtime/next-modules.test.js +34 -0
- package/lib/runtime/public-files.d.ts +32 -0
- package/lib/runtime/public-files.js +117 -0
- package/lib/runtime/public-files.test.d.ts +1 -0
- package/lib/runtime/public-files.test.js +97 -0
- package/lib/runtime/server.mjs +41588 -0
- package/lib/runtime/static-files.d.ts +50 -0
- package/lib/runtime/static-files.js +266 -0
- package/lib/runtime/static-files.test.d.ts +1 -0
- package/lib/runtime/static-files.test.js +118 -0
- package/lib/runtime/util.d.ts +28 -0
- package/lib/runtime/util.js +79 -0
- package/lib/utils/base-path.d.ts +123 -11
- package/lib/utils/base-path.js +276 -39
- package/lib/utils/base-path.test.js +275 -50
- package/lib/utils/get-architecture.d.ts +7 -8
- package/lib/utils/get-architecture.js +12 -13
- package/lib/utils/zip-directory.d.ts +14 -0
- package/lib/utils/zip-directory.js +121 -0
- package/lib/utils/zip-directory.test.d.ts +1 -0
- package/lib/utils/zip-directory.test.js +99 -0
- package/package.json +22 -3
- package/scripts/capture-adapter-fixture.mjs +325 -0
- package/scripts/e2e-deploy.sh +398 -0
- package/scripts/e2e-harness/README.md +707 -0
- package/scripts/e2e-harness/app.js +370 -0
- package/scripts/e2e-harness/common.sh +249 -0
- package/scripts/e2e-harness/screen.mjs +302 -0
- package/scripts/e2e-harness/stage-proxy.mjs +145 -0
- package/scripts/e2e-harness/type-filters.mjs +47 -0
- package/scripts/e2e-logs.sh +75 -0
- package/scripts/e2e-offline.sh +91 -0
- package/scripts/e2e-sweep.sh +370 -0
- package/scripts/e2e-warm.sh +114 -0
- package/scripts/zero-config-build.mjs +418 -0
- package/docs/1k-concurrent-users-executions.png +0 -0
- package/docs/plans/adapter-runtime-release.md +0 -362
- package/docs/plans/phase-0-image-function-opt-in.md +0 -295
- package/lib/generated-structs/OptionalDockerImageFunctionProps.d.ts +0 -425
- package/lib/generated-structs/OptionalDockerImageFunctionProps.js +0 -3
- package/lib/image-optimization/handler-utils.d.ts +0 -34
- package/lib/image-optimization/handler-utils.js +0 -83
- package/lib/image-optimization/handler-utils.test.js +0 -178
- package/lib/nextjs-build/functions.Dockerfile +0 -31
- package/lib/nextjs-compute/nextjs-image-function.d.ts +0 -53
- package/lib/nextjs-compute/nextjs-image-function.js +0 -59
- package/lib/utils/experimental-flags.d.ts +0 -24
- package/lib/utils/experimental-flags.js +0 -30
- /package/lib/{image-optimization/handler-utils.test.d.ts → adapter/adapter.test.d.ts} +0 -0
|
@@ -0,0 +1,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"
|