Skip to content

Commit 18d84ae

Browse files
authored
test(integ): add s3-asset-deploy integ (S3 zip asset upload + Lambda from local dir) (#832)
1 parent 759422a commit 18d84ae

24 files changed

Lines changed: 621 additions & 4 deletions

docs/_generated/integ-coverage.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2301,6 +2301,7 @@
23012301
"recreate-mixed-direction",
23022302
"recreate-via-cc-api",
23032303
"recreate-via-sdk-provider",
2304+
"s3-asset-deploy",
23042305
"scheduled-task",
23052306
"serverless-api",
23062307
"sns-sqs-event",
@@ -2458,6 +2459,9 @@
24582459
"recreate-via-sdk-provider": [
24592460
"l1"
24602461
],
2462+
"s3-asset-deploy": [
2463+
"l2"
2464+
],
24612465
"scheduled-task": [
24622466
"l2"
24632467
],

docs/_generated/integ-last-run.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,4 @@ cc-api-fallback 2026-06-13T09:16:21Z PASS 57 verify.sh regression sweep; 0 err 0
126126
custom-resource-provider 2026-06-13T09:16:21Z PASS 290 standard regression sweep; 0 err 0 orphan
127127
deployment-events 2026-06-13T09:16:21Z PASS 34 verify.sh #808 events sidecar + cdkd events + no-secrets + survives-destroy; 2 deleted 0 err
128128
update-replace 2026-06-13T10:43:46Z PASS 65 verify.sh in-place(S3/Lambda/IAM/SG)+replacement(S3 rename); 0 err 0 orphan
129+
s3-asset-deploy 2026-06-13T10:51:36Z PASS 44 verify.sh S3 zip asset upload + Lambda runs from it + generic Asset read-back; 0 err

docs/_generated/scenario-coverage.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@
144144
"tag": "remove-protection-bypass",
145145
"description": "`--remove-protection` flag bypassing AWS-side deletion-protection on supported types."
146146
},
147+
{
148+
"tag": "s3-asset-deploy",
149+
"description": "File/ZIP asset publishing during `cdkd deploy`: a multi-file local directory is zipped + uploaded to the CDK bootstrap asset bucket by `FileAssetPublisher` (content-addressed, skip-if-exists), the Lambda `Code.S3Bucket`/`Code.S3Key` ref is wired to the uploaded object (CodeSize proves it is NOT inline), AND a generic `s3_assets.Asset` upload is read back at runtime via cdkd-resolved bucket/key env vars. Bootstrap-bucket asset objects persist by design across destroy."
150+
},
147151
{
148152
"tag": "state-bucket-region-resolve",
149153
"description": "State-bucket S3 clients (state backend + lock manager) auto-detect bucket region via `GetBucketLocation` regardless of caller-profile region."
@@ -825,6 +829,13 @@
825829
"annotated": true,
826830
"scenarios": []
827831
},
832+
{
833+
"name": "s3-asset-deploy",
834+
"annotated": true,
835+
"scenarios": [
836+
"s3-asset-deploy"
837+
]
838+
},
828839
{
829840
"name": "s3-cloudfront",
830841
"annotated": true,
@@ -1227,6 +1238,13 @@
12271238
"remove-protection"
12281239
]
12291240
},
1241+
{
1242+
"scenario": "s3-asset-deploy",
1243+
"description": "File/ZIP asset publishing during `cdkd deploy`: a multi-file local directory is zipped + uploaded to the CDK bootstrap asset bucket by `FileAssetPublisher` (content-addressed, skip-if-exists), the Lambda `Code.S3Bucket`/`Code.S3Key` ref is wired to the uploaded object (CodeSize proves it is NOT inline), AND a generic `s3_assets.Asset` upload is read back at runtime via cdkd-resolved bucket/key env vars. Bootstrap-bucket asset objects persist by design across destroy.",
1244+
"fixtures": [
1245+
"s3-asset-deploy"
1246+
]
1247+
},
12301248
{
12311249
"scenario": "state-bucket-region-resolve",
12321250
"description": "State-bucket S3 clients (state backend + lock manager) auto-detect bucket region via `GetBucketLocation` regardless of caller-profile region.",

docs/changelog-cdkd.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The CLAUDE.md `## Known Limitations` section retains the load-bearing summary
1818

1919
**Recently Implemented** (2026-06-13):
2020

21-
- ✅ **New `update-replace` integ broadens real-AWS UPDATE / replacement coverage (refs [#807](https://github.com/go-to-k/cdkd/issues/807) / [#809](https://github.com/go-to-k/cdkd/issues/809))** — `tests/integration/update-replace/**`. Before this fixture only `basic`, `dynamodb-globaltable`, and `ecs-fargate` exercised `CDKD_TEST_UPDATE`; none covered the in-place provider `update()` paths for S3 versioning / Lambda env+memory / IAM inline-policy / SecurityGroup ingress, and only S3 BucketName / ECS TaskDefinition replacement was implicitly exercised. The new stack (`CdkdUpdateReplaceExample`) reads `CDKD_TEST_UPDATE` at synth time and on the second deploy mutates properties to cover BOTH paths in one cheap deploy (no VPC / NAT / RDS — the SecurityGroup attaches to the account's default VPC via `ec2.Vpc.fromLookup`): in-place `update()` (physical id unchanged) for `AWS::S3::Bucket` (VersioningConfiguration off -> Enabled), `AWS::Lambda::Function` (Environment `STAGE` dev -> prod + MemorySize 128 -> 256), `AWS::IAM::Role` (inline `Policies` document gains `s3:PutObject`), and `AWS::EC2::SecurityGroup` (ingress gains tcp/443 from `0.0.0.0/0`); and replacement (new physical id) for a second `AWS::S3::Bucket` whose `BucketName` suffix changes `-v1` -> `-v2` (BucketName is in the S3 entry of `ReplacementRulesRegistry`). `verify.sh` deploys flag-off (capturing each physical id + baseline property value from AWS via `get-bucket-versioning` / `get-function-configuration` / `get-role-policy` / `describe-security-groups` / `head-bucket`), redeploys flag-on, and asserts per resource that the in-place resources keep the SAME physical id with the NEW value on AWS while the replaced bucket has a CHANGED physical id (old gone, new present), then destroys and asserts the state file + both buckets are gone. BSD/macOS-portable (real rc captured, jq `has()` for boolean fields, no `grep -P` / `date -d`); ends with `[verify] PASS`. New scenario tag `update-replace-breadth`. This is the broad regression net for the provider `update()` paths plus the #807 replacement-propagation and #809 Cloud-Control write-only-property UPDATE work on non-ECS types.
21+
- ✅ **New `s3-asset-deploy` integ exercises the S3 file/ZIP asset-publishing path during a real `cdkd deploy`** — `tests/integration/s3-asset-deploy/**`. Most Lambda fixtures use inline code (`Code.fromInline`) or never assert the asset upload itself, leaving cdkd's `FileAssetPublisher` (S3 zip + upload, content-addressed skip-if-exists) without a dedicated end-to-end regression. The new `CdkdS3AssetDeployExample` stack closes that gap: a Lambda whose code comes from a **local multi-file directory** (`lambda/` — handler + `helpers/` + `vendored/` Python sub-packages, so the asset is a genuine multi-file ZIP, not a trivial single file) forces cdkd to zip the directory + upload it to the CDK bootstrap asset bucket and wire the function's `Code.S3Bucket`/`Code.S3Key` to the uploaded object; AND a generic `s3_assets.Asset` (`asset-data/`) is uploaded to the same bucket with its resolved `s3BucketName`/`s3ObjectKey` threaded into the Lambda as `CONFIG_BUCKET`/`CONFIG_KEY` env vars (synth confirms both are emitted as `Fn::Sub`-backed bucket + literal key refs, exercising cdkd's intrinsic resolver). `verify.sh` (BSD/macOS-portable, real-rc + explicit `[verify] PASS`) deploys, asserts the function's `CodeSize > 500` bytes (proves it ran from the uploaded ZIP, not inline), invokes it and asserts the handler marker (`cdkd-s3-asset-deploy-marker-v1`) plus a non-zero `configBytes` from the generic-asset S3 read-back (proving that upload reached AWS and the bucket/key env wiring resolved), then destroys and asserts the Lambda + state file are gone with 0 errors. The bootstrap-bucket asset OBJECTS persist by design (cdkd does not own / delete the CDK bootstrap bucket) — the script deliberately does NOT assert their absence and notes this. New scenario tag `s3-asset-deploy` in the canonical taxonomy (`scripts/build-scenario-coverage-matrix.ts`); coverage matrices regenerated. (NOTE: not yet run against real AWS — needs `/run-integ s3-asset-deploy` before merge.)
2222
- ✅ **Extracted the triplicated `ensureClientForBucket()` state-bucket-region rebuild into one shared helper (issue [#827](https://github.com/go-to-k/cdkd/issues/827))** — `src/utils/bucket-region-client.ts` (new) + `src/state/s3-state-backend.ts` + `src/state/lock-manager.ts` + `src/state/export-index-store.ts`. Pure refactor, no behavior change. The "resolve the state bucket's region via the cached `GetBucketLocation` probe, short-circuit when it already matches the client's region, else rebuild a region-corrected `S3Client` reusing the caller's credentials" pattern had drifted into three near-identical private `ensureClientForBucket()` copies (the state backend PR #60, the lock manager #803, the exports index store #819). They are now one exported helper `rebuildClientForBucketRegion(client, bucket, opts): Promise<S3Client | null>` — it returns `null` to mean "no rebuild needed, keep the original client" and a fresh client otherwise. Every load-bearing per-store difference is preserved via options rather than collapsed: `destroyOldClient` (the state backend OWNS its client and `.destroy()`s the replaced one; the lock manager + exports store share `AwsClients.s3` and must NOT), `profile` + static `credentials` (the state backend threads its constructor `clientOpts` into both the probe and the rebuild) vs `reuseClientCredentials` (the other two authenticate the probe via `client.config.credentials()` best-effort and reuse the original client's `config.credentials` PROVIDER REFERENCE — not a resolved snapshot — for the rebuild), and `tolerateNonStandardClient` (only the exports store gracefully degrades a test double whose `config.region` is not a function by returning `null`; the other two read `config.region()` directly as before). Each store keeps its own per-instance memoization (`clientResolved` flag + single-flight `resolveInFlight` promise) and its own debug log wording via an `onRebuild` callback. The helper deliberately lives in its OWN module rather than alongside `resolveBucketRegion` in `aws-region-resolver.ts`: the three stores' unit tests mock `resolveBucketRegion` via `vi.mock('aws-region-resolver.js', ...)`, and a helper co-located in that module would call its sibling through an in-module binding vitest cannot intercept (a module cannot mock itself) — a separate module imports the mocked binding cross-module so the mock still applies. Tests: all 75 existing unit tests across `tests/unit/state/{s3-state-backend,lock-manager,export-index-store}.test.ts` (incl. each store's 301-rebuild / same-region-no-rebuild / resolve-once-cached suites) pass UNCHANGED, plus 7 new focused tests in `tests/unit/utils/bucket-region-client.test.ts` (region-mismatch → rebuilt client with correct region; same-region → `null`; static-credential carry-over to probe + rebuild; provider-reference reuse; `destroyOldClient` gating; non-standard-client tolerance → `null`; no-credentials → omit `credentials` from the rebuilt client). No runtime behavior change for any of the three cross-region-state-bucket paths (lock acquisition, state read/write, exports index).
2323
- ✅ **`--region` deprecation warning no longer contradicts the actual behavior (issue [#818](https://github.com/go-to-k/cdkd/issues/818))** — `src/cli/options.ts`. `warnIfDeprecatedRegion` and the hidden `deprecatedRegionOption` help text both claimed `--region` "has no effect" on non-bootstrap commands, but every non-bootstrap command (`deploy`, `destroy`, `diff`, `synth`, `list`, `state`, `force-unlock`, `publish-assets`, `import`, `export`, `orphan`, `drift`, `events`, `local *`, …) actually consumes `options.region` as the highest-precedence region source: `const region = options.region || process.env['AWS_REGION'] || 'us-east-1'` feeds the provisioning / state-bucket SDK clients and the `applyRoleArnIfSet` STS hop, and `deploy` / `destroy` / `import` / `export` / `orphan` additionally inject it into `process.env.AWS_REGION` so the CDK synth subprocess inherits it (e.g. `deploy.ts` ~L167/L175/L341). The warning and the code therefore disagreed — a user passing `--region` was told it did nothing while it silently took effect. **Investigation determined `--region` IS legitimately honored everywhere (option B in the issue), so the fix is purely in the warning + help text — no command implementation (`deploy.ts` etc.) was touched**, keeping the change out of the `integ-broad` merge-gate scope and carrying zero behavior-change risk. The warning now reads "`--region is deprecated and will be removed in a future release. It is still honored for now (it overrides AWS_REGION / your AWS profile), but prefer the AWS_REGION environment variable or your AWS profile…`" and the option description drops the false "No effect" claim. The recommended mechanism is still `AWS_REGION` / the AWS profile; the flag stays hidden + deprecated, just honestly described. Docs corrected: two "deprecated and ignored" lines in [docs/cli-reference.md](cli-reference.md) and the `--region` bullet in [.claude/rules/cli-internals.md](../.claude/rules/cli-internals.md). Tests: `tests/unit/cli/options.test.ts` — the existing message assertion updated, plus new assertions that neither the warning nor the option description contains "no effect" and that both mention the flag is "still honored" (issue #818).
2424
- ✅ **`destroy` waits for NAT Gateway deletion before detaching / deleting the IGW + VPCGatewayAttachment (issue [#817](https://github.com/go-to-k/cdkd/issues/817))** — `src/analyzer/implicit-delete-deps.ts`. Destroying a VPC + NAT Gateway + IGW stack attempted the `VPCGatewayAttachment` detach while the NAT Gateway's Elastic IP was still mapped to the VPC's public address space, failing with `Network vpc-xxx has some mapped public address(es)`, after which the IGW delete hung (~19 min observed). This was the first-run failure split out of the #804 incident as a separate issue. The fix adds two type-based implicit delete-dependency edges so the shared deploy DELETE phase + standalone destroy command order the teardown like CloudFormation does: `AWS::EC2::InternetGateway` gains `AWS::EC2::NatGateway` (alongside its existing `AWS::EC2::VPCGatewayAttachment` dependee) and a new `AWS::EC2::VPCGatewayAttachment` key lists `AWS::EC2::NatGateway` — both are deleted AFTER the NAT Gateway is gone (NAT deletion releases / decouples the EIP). No type-based rule is needed for the EIP itself: the NAT Ref's its EIP via `AllocationId`, so the reversed delete traversal already deletes the NAT before the EIP is released. The injection logic (`destroy-runner.ts` / `deploy-engine.ts`) naturally produces no edge when no NatGateway is in state. Tests: 4 unit assertions in `tests/unit/analyzer/implicit-delete-deps.test.ts` (IGW-after-NAT edge, VPCGatewayAttachment-after-NAT edge, no NatGateway / EIP key registered; the existing no-self-cycle guard covers the new entries). Integ: the existing `vpc-nat-gateway` fixture (VPC + public/private subnets + IGW + NatGateway + EIP) exercises exactly this teardown end-to-end.

0 commit comments

Comments
 (0)