fix(lib)!: preserve symlinks in TerraformAsset walkers - #321
Conversation
archiveSync, hashPath, and copySync all walked with fs.statSync, which follows symlinks: shared targets were duplicated N times into archives (15 MB -> 34 MB on a real pnpm Lambda bundle), symlink entries were never emitted, and circular links (legitimate in pnpm node_modules) crashed synth with ELOOP -- for every asset type, since hashPath runs in the TerraformAsset constructor. Walk with lstatSync instead and treat symlinks first-class, restoring the behavior cdktf gets from archiver (dropped in open-constructs#95 when archiver was replaced by yazl, kept broken through the fflate rewrite in open-constructs#148): - archiveSync emits real symlink entries (readlink target as STORED data, S_IFLNK unix mode in the external attrs, host os Unix), now also preserves regular-file modes (executable bits were stripped), and pins entry mtimes to a fixed date so archives are byte-reproducible (fflate defaulted to Date.now() per synth) - hashPath hashes a symlink by its target path, so retargeting still changes the asset hash without following (or double-counting) links - copySync recreates symlinks for AssetType.DIRECTORY assets Fixes open-constructs#320 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
flagged as breaking change as it changes the checksums / SHA created by the
Altho I'm not sure if we want this (or it should be possible to opt-in / default opt-out to this
|
sakul-learning
left a comment
There was a problem hiding this comment.
Review summary
The symlink fix itself is well targeted, and the new tests pass the behavior-oriented/value assessment: each core test exercises an observable regression rather than implementation text. In particular, the extraction tests prove real symlink fidelity; the shared-target size test catches archive materialization/bloat; the circular-link tests protect all three walkers from ELOOP; retargeting checks asset invalidation; and the directory-copy test proves links are recreated rather than dereferenced. The executable-mode test protects a real Lambda/CLI artifact property, while the byte-identical archive test protects deterministic output (subject to resolving the maintainer tradeoff already raised about forcing mtimes). These tests are included by the normal cdktn Nx/Jest target, and the configured full run passed: 39 suites, 454 tests, and 288 snapshots. The focused validations suite and nx build cdktn also passed.
I found one blocking correctness issue in the new hash representation: regular-file bytes and symlink-target bytes occupy the same unframed hash domain, so materially different artifacts can receive the same assetHash and synthesized path. See the inline comment for options and compatibility tradeoffs.
I also think this small PR should cover TerraformModuleAsset's separate source copier. packages/cdktn/src/terraform-module-asset.ts:136-144 sends every non-directory, including a directory symlink, to copyFileSync, which can fail with EISDIR before the corrected TerraformAsset walkers are reached. Reusing the corrected helper (or adding equivalent symlink handling) plus one module-asset regression would address the same class of synthesis failures without materially expanding the PR. GitHub cannot attach a review comment to those unchanged lines, so this item is recorded here and alongside the shared copier.
No workflow files changed, and I found no security concern in the touched code. Ponytail/artifact-value pass: the core regression tests earn their maintenance cost; no speculative abstraction or low-signal static test was added.
…ware copier Review follow-ups on open-constructs#321: - hashPath fed symlink targets and file contents into one unframed stream, so a file containing 'foo' and a symlink targeting 'foo' could produce the same assetHash for materially different artifacts. The default scheme now keeps the legacy content hash byte-identical for symlink-free trees and, only when symlinks exist, combines it with separately-framed symlink metadata under a tagged outer hash. - New canonicalAssetHashes feature flag (FUTURE_FLAGS, on for new projects): every entry is hashed as (type, relative path, payload size, payload) in sorted order -- the construction git trees and Nix NAR use -- which also makes renames, entry-boundary shifts, and file-vs-symlink swaps visible to the hash. Graduates to the default at the next major. - TerraformModuleAsset dropped its private copier, which routed directory symlinks into copyFileSync (EISDIR) before the fixed walkers could run; it now shares the symlink-aware copySync and the flag-aware hashPath. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keeping the pin, on prior art: Go module zips do exactly this — the Two things bound the blast radius here: If someone does turn up with a real need for source mtimes we can add a knob then, but I'd rather not grow the JSII surface for it preemptively. |
Requested downscope: keep option 3 here; move canonical hashing outThe compatibility-preserving option 3 now resolves the blocker from the earlier review: trees without symlinks keep their historical content hash, while symlink-bearing trees combine separately framed symlink metadata under a tagged outer digest. That is sufficient for #320 and should remain in this PR. Please remove the additional The reason to separate it is not only scope. The current canonical representation has a metadata hole: it frames file/symlink type, relative path, payload length, and payload, but regular-file records omit the mode that The Git/Nix precedent did not expose this problem because their canonical formats serialize more than payload framing:
So the framing pattern was borrowed without the complete metadata model that makes those formats canonical. #322 captures the needed contract and regressions for modes, empty directories, paths, entry boundaries, and symlink metadata. The |
…ware copier Review follow-ups on open-constructs#321, downscoped per review to the compatibility-preserving hash only (canonical hashing moved to open-constructs#322): - hashPath fed symlink targets and file contents into one unframed stream, so a file containing 'foo' and a symlink targeting 'foo' could produce the same assetHash for materially different artifacts. The scheme now keeps the legacy content hash byte-identical for symlink-free trees and, only when symlinks exist, combines it with separately-framed symlink metadata (relative path + target) under a tagged outer hash. - TerraformModuleAsset dropped its private copier, which routed directory symlinks into copyFileSync (EISDIR) before the fixed walkers could run; it now shares the symlink-aware copySync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0d8c055 to
ff817b2
Compare
|
Downscoped as requested — the branch was force-pushed so this PR is now exactly: the three symlink-aware walkers, the option-3 compatibility-preserving hash, and the shared The follow-up is a stack, review-ready:
Thanks for the git/Nix observation — you were right that the first draft borrowed the framing without the object model that makes those formats canonical. |
There was a problem hiding this comment.
Approved after the downscope and successful checks on ff817b2. The option-3 hash fix and TerraformModuleAsset copier regression are in scope; canonical hashing remains correctly split into the stacked follow-up.
Documentation follow-up for open-constructs/cdk-terrain-docs: please explain the deterministic ZIP timestamp policy and cite golang.org/x/mod/zip as precedent (its format says “File permissions and timestamps are also ignored”). Phrase the comparison precisely: TerraformAsset archives similarly avoid meaningful source timestamps by pinning ZIP mtimes, but unlike Go module ZIPs they preserve regular-file permission bits and support symlinks.
…ture flag (#323) ### Description Closes #322. ~~Stacked on #321~~ — #321 has merged and this branch is rebased onto current `main`; the diff is now the single canonical-hash commit. Adds the canonical entry-framed asset hash as a `canonicalAssetHashes` feature flag (`FUTURE_FLAGS`: enabled for new projects by `cdktn init`, opt-in via `cdktf.json` context for existing projects, becomes the default at the next major). The legacy scheme — including #321's compatibility-preserving symlink handling — stays untouched as the default. ### The canonical representation Modeled on git trees and Nix NAR, which serialize a full metadata model rather than payload framing alone. Every record contains what affects the emitted artifact: | entry | frame | | --- | --- | | file | `F <mode> <relPath>\0<size>\0` + content | | symlink | `L <mode> <relPath>\0<target byte length>\0` + target | | directory (incl. empty) | `D <relPath>\0` | - `<mode>` is the octal permission mask (`0o7777` bits) — exactly the bits `archiveSync` preserves in zip external attributes, closing the hole found in review where `0644` → `0755` changed archive bytes but not the hash. - Directories contribute explicit records (no mode — neither `archiveSync` nor `copySync` preserves directory permissions), so adding/removing an empty directory is visible. - Entries are framed in sorted directory order with `/`-separated relative paths; a symlink at the root is followed, matching how the asset source path is opened when the artifact is emitted. ### Acceptance criteria from #322 Each has a dedicated test in `packages/cdktn/test/canonical-asset-hash.test.ts`: - [x] File rename changes the canonical hash (and a paired assertion that legacy cannot see it) - [x] Entry-boundary shifts change the canonical hash - [x] File/symlink swaps and symlink retargeting change the canonical hash - [x] `0644` → `0755` changes the canonical hash - [x] Adding/removing an empty directory changes the canonical hash - [x] Identical logical trees hash deterministically (two locations + repeated runs) - [x] Legacy hashing remains available (flag off; `Testing.app({ enableFutureFlags: false })` covers the compat case per the `features.ts` test rule) ### Testing Full `cdktn` suite green (464 passing; the pre-existing environmental `matchers.test.ts` → `toPlanSuccessfully` failure shells out to a real `terraform plan` and is unrelated). jsii build green. Docs for the flag are being handled separately in the docs repository (the stacked docs PR #324 was closed in favor of that). ### Checklist - [x] I have updated the PR title to match [CDKTN's style guide](https://github.com/open-constructs/cdk-terrain/blob/main/CONTRIBUTING.md#pull-requests-1) - [x] I have run the linter on my code locally - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation if applicable (stacked docs PR follows) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works if applicable - [x] New and existing unit tests pass locally with my changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: vincent de smet <vincent.drl@gmail.com>
🤖 Release PR — merge to cut a new release. Kept open and rebased as commits land on `main`. --- <details><summary>0.24.0</summary> ## [0.24.0](v0.23.4...v0.24.0) (2026-08-06) ### ⚠ BREAKING CHANGES * **lib:** validate Terraform function versions by default ([#362](#362)) * **deps:** Require Node 22 minimum ([#345](#345)) * **lib:** preserve symlinks in TerraformAsset walkers ([#321](#321)) * **cli:** replace node-fetch with undici ([#306](#306)) ### Features * **lib:** canonical asset hashes behind the canonicalAssetHashes feature flag ([#323](#323)) ([76dd4ff](76dd4ff)) * **lib:** validate Terraform function versions by default ([#362](#362)) ([4ac0736](4ac0736)) * support newer provider plugin-protocol features via targetVersions (RFC-04) ([#296](#296)) ([90322f9](90322f9)) ### Bug Fixes * **cli:** don't downgrade prebuilt providers on a transient registry failure ([#298](#298)) ([a960c5c](a960c5c)) * **cli:** include dev dependencies in npm version lookup ([#280](#280)) ([955204a](955204a)) * **docs:** fix stale constructs pin breaking with()/IMixin docs ([#305](#305)) ([605cf63](605cf63)) * **gha:** Allow pnpm to update the lockfile after package updates ([#318](#318)) ([a899b7c](a899b7c)) * **gha:** Fix pnpm upgrade workflow ([#335](#335)) ([e1a69fc](e1a69fc)) * **gha:** flip merged release PR label to autorelease: tagged ([#302](#302)) ([8d64f6c](8d64f6c)) * **gha:** mint the Go-publish token from the CDKTN Maintainers app ([#369](#369)) ([71921ce](71921ce)) * **gha:** mint the Go-publish token from the open-constructs-cdktn App ([#368](#368)) ([179f10c](179f10c)) * **gha:** pass the Go-publish App token as x-access-token userinfo ([#370](#370)) ([4e3ff19](4e3ff19)) * **lib:** Disallow constructs 10.8 until support can be added ([#363](#363)) ([8bdae0d](8bdae0d)) * **lib:** preserve symlinks in TerraformAsset walkers ([#321](#321)) ([6360e20](6360e20)) * typo in `moveFromId` JSDoc ([#355](#355)) ([e1cf8ce](e1cf8ce)) ### Miscellaneous Chores * **cli:** replace Ink + React with smaller-tree CLI libraries ([#264](#264)) ([a6aff7e](a6aff7e)) * **cli:** replace node-fetch with undici ([#306](#306)) ([1317141](1317141)) * **deps:** bump glob to 13.0.6 ([#307](#307)) ([47ee2bb](47ee2bb)) * **deps:** bump the github-actions-backward-compatible group with 2 updates ([#295](#295)) ([eab2a01](eab2a01)) * **deps:** replace lerna with nx ([#315](#315)) ([94999fc](94999fc)) * **deps:** Require Node 22 minimum ([#345](#345)) ([2bf315d](2bf315d)) * **deps:** update ci-info to 4.4.0 across all packages ([#329](#329)) ([557a163](557a163)) * **deps:** update fs-extra to 11.3.6 across all packages ([#328](#328)) ([95753a9](95753a9)) * **deps:** update minimatch to 10.2.5 ([#330](#330)) ([d1c1c53](d1c1c53)) * **deps:** Update sscaff to v2.0.388 ([#331](#331)) ([38ea0ba](38ea0ba)) * **deps:** Update zod to v4.4.3 ([#332](#332)) ([c10ee35](c10ee35)) * **deps:** Upgrade dependencies ([#347](#347)) ([45d3a66](45d3a66)) * remove cdktf from tests ([#277](#277)) ([dc9a8e9](dc9a8e9)) * ship Terraform 1.15.8 in the jsii-terraform image ([#367](#367)) ([deaa9b0](deaa9b0)) * Upgrade dependencies for lib ([#348](#348)) ([d616f17](d616f17)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: open-constructs-cdktn[bot] <291052431+open-constructs-cdktn[bot]@users.noreply.github.com>
…ture flag (open-constructs#323) ### Description Closes open-constructs#322. ~~Stacked on open-constructs#321~~ — open-constructs#321 has merged and this branch is rebased onto current `main`; the diff is now the single canonical-hash commit. Adds the canonical entry-framed asset hash as a `canonicalAssetHashes` feature flag (`FUTURE_FLAGS`: enabled for new projects by `cdktn init`, opt-in via `cdktf.json` context for existing projects, becomes the default at the next major). The legacy scheme — including open-constructs#321's compatibility-preserving symlink handling — stays untouched as the default. ### The canonical representation Modeled on git trees and Nix NAR, which serialize a full metadata model rather than payload framing alone. Every record contains what affects the emitted artifact: | entry | frame | | --- | --- | | file | `F <mode> <relPath>\0<size>\0` + content | | symlink | `L <mode> <relPath>\0<target byte length>\0` + target | | directory (incl. empty) | `D <relPath>\0` | - `<mode>` is the octal permission mask (`0o7777` bits) — exactly the bits `archiveSync` preserves in zip external attributes, closing the hole found in review where `0644` → `0755` changed archive bytes but not the hash. - Directories contribute explicit records (no mode — neither `archiveSync` nor `copySync` preserves directory permissions), so adding/removing an empty directory is visible. - Entries are framed in sorted directory order with `/`-separated relative paths; a symlink at the root is followed, matching how the asset source path is opened when the artifact is emitted. ### Acceptance criteria from open-constructs#322 Each has a dedicated test in `packages/cdktn/test/canonical-asset-hash.test.ts`: - [x] File rename changes the canonical hash (and a paired assertion that legacy cannot see it) - [x] Entry-boundary shifts change the canonical hash - [x] File/symlink swaps and symlink retargeting change the canonical hash - [x] `0644` → `0755` changes the canonical hash - [x] Adding/removing an empty directory changes the canonical hash - [x] Identical logical trees hash deterministically (two locations + repeated runs) - [x] Legacy hashing remains available (flag off; `Testing.app({ enableFutureFlags: false })` covers the compat case per the `features.ts` test rule) ### Testing Full `cdktn` suite green (464 passing; the pre-existing environmental `matchers.test.ts` → `toPlanSuccessfully` failure shells out to a real `terraform plan` and is unrelated). jsii build green. Docs for the flag are being handled separately in the docs repository (the stacked docs PR open-constructs#324 was closed in favor of that). ### Checklist - [x] I have updated the PR title to match [CDKTN's style guide](https://github.com/open-constructs/cdk-terrain/blob/main/CONTRIBUTING.md#pull-requests-1) - [x] I have run the linter on my code locally - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation if applicable (stacked docs PR follows) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works if applicable - [x] New and existing unit tests pass locally with my changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: vincent de smet <vincent.drl@gmail.com>
🤖 Release PR — merge to cut a new release. Kept open and rebased as commits land on `main`. --- <details><summary>0.24.0</summary> ## [0.24.0](open-constructs/cdk-terrain@v0.23.4...v0.24.0) (2026-08-06) ### ⚠ BREAKING CHANGES * **lib:** validate Terraform function versions by default ([open-constructs#362](open-constructs#362)) * **deps:** Require Node 22 minimum ([open-constructs#345](open-constructs#345)) * **lib:** preserve symlinks in TerraformAsset walkers ([open-constructs#321](open-constructs#321)) * **cli:** replace node-fetch with undici ([open-constructs#306](open-constructs#306)) ### Features * **lib:** canonical asset hashes behind the canonicalAssetHashes feature flag ([open-constructs#323](open-constructs#323)) ([76dd4ff](open-constructs@76dd4ff)) * **lib:** validate Terraform function versions by default ([open-constructs#362](open-constructs#362)) ([4ac0736](open-constructs@4ac0736)) * support newer provider plugin-protocol features via targetVersions (RFC-04) ([open-constructs#296](open-constructs#296)) ([90322f9](open-constructs@90322f9)) ### Bug Fixes * **cli:** don't downgrade prebuilt providers on a transient registry failure ([open-constructs#298](open-constructs#298)) ([a960c5c](open-constructs@a960c5c)) * **cli:** include dev dependencies in npm version lookup ([open-constructs#280](open-constructs#280)) ([955204a](open-constructs@955204a)) * **docs:** fix stale constructs pin breaking with()/IMixin docs ([open-constructs#305](open-constructs#305)) ([605cf63](open-constructs@605cf63)) * **gha:** Allow pnpm to update the lockfile after package updates ([open-constructs#318](open-constructs#318)) ([a899b7c](open-constructs@a899b7c)) * **gha:** Fix pnpm upgrade workflow ([open-constructs#335](open-constructs#335)) ([e1a69fc](open-constructs@e1a69fc)) * **gha:** flip merged release PR label to autorelease: tagged ([open-constructs#302](open-constructs#302)) ([8d64f6c](open-constructs@8d64f6c)) * **gha:** mint the Go-publish token from the CDKTN Maintainers app ([open-constructs#369](open-constructs#369)) ([71921ce](open-constructs@71921ce)) * **gha:** mint the Go-publish token from the open-constructs-cdktn App ([open-constructs#368](open-constructs#368)) ([179f10c](open-constructs@179f10c)) * **gha:** pass the Go-publish App token as x-access-token userinfo ([open-constructs#370](open-constructs#370)) ([4e3ff19](open-constructs@4e3ff19)) * **lib:** Disallow constructs 10.8 until support can be added ([open-constructs#363](open-constructs#363)) ([8bdae0d](open-constructs@8bdae0d)) * **lib:** preserve symlinks in TerraformAsset walkers ([open-constructs#321](open-constructs#321)) ([6360e20](open-constructs@6360e20)) * typo in `moveFromId` JSDoc ([open-constructs#355](open-constructs#355)) ([e1cf8ce](open-constructs@e1cf8ce)) ### Miscellaneous Chores * **cli:** replace Ink + React with smaller-tree CLI libraries ([open-constructs#264](open-constructs#264)) ([a6aff7e](open-constructs@a6aff7e)) * **cli:** replace node-fetch with undici ([open-constructs#306](open-constructs#306)) ([1317141](open-constructs@1317141)) * **deps:** bump glob to 13.0.6 ([open-constructs#307](open-constructs#307)) ([47ee2bb](open-constructs@47ee2bb)) * **deps:** bump the github-actions-backward-compatible group with 2 updates ([open-constructs#295](open-constructs#295)) ([eab2a01](open-constructs@eab2a01)) * **deps:** replace lerna with nx ([open-constructs#315](open-constructs#315)) ([94999fc](open-constructs@94999fc)) * **deps:** Require Node 22 minimum ([open-constructs#345](open-constructs#345)) ([2bf315d](open-constructs@2bf315d)) * **deps:** update ci-info to 4.4.0 across all packages ([open-constructs#329](open-constructs#329)) ([557a163](open-constructs@557a163)) * **deps:** update fs-extra to 11.3.6 across all packages ([open-constructs#328](open-constructs#328)) ([95753a9](open-constructs@95753a9)) * **deps:** update minimatch to 10.2.5 ([open-constructs#330](open-constructs#330)) ([d1c1c53](open-constructs@d1c1c53)) * **deps:** Update sscaff to v2.0.388 ([open-constructs#331](open-constructs#331)) ([38ea0ba](open-constructs@38ea0ba)) * **deps:** Update zod to v4.4.3 ([open-constructs#332](open-constructs#332)) ([c10ee35](open-constructs@c10ee35)) * **deps:** Upgrade dependencies ([open-constructs#347](open-constructs#347)) ([45d3a66](open-constructs@45d3a66)) * remove cdktf from tests ([open-constructs#277](open-constructs#277)) ([dc9a8e9](open-constructs@dc9a8e9)) * ship Terraform 1.15.8 in the jsii-terraform image ([open-constructs#367](open-constructs#367)) ([deaa9b0](open-constructs@deaa9b0)) * Upgrade dependencies for lib ([open-constructs#348](open-constructs#348)) ([d616f17](open-constructs@d616f17)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: open-constructs-cdktn[bot] <291052431+open-constructs-cdktn[bot]@users.noreply.github.com>
|
I'm going to lock this pull request because it has been closed for 30 days. This helps our maintainers find and focus on the active issues. If you've found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. |
Description
Closes #320.
TerraformAssetwalked source trees withfs.statSync, which follows symlinks. On symlinked trees (e.g. pnpm'snode_modules) this meant:ELOOP. Notably this crashed for every asset type, not justARCHIVE, becausehashPathruns in theTerraformAssetconstructor.This is a fork regression, not inherited from cdktf: upstream archives with
archiver, which walks withlstatand emits real symlink entries (cycles are structurally impossible there). The regression entered whenarchiverwas replaced by yazl in #95 — yazl has no symlink awareness at all (addFilestats-and-follows) — and survived the yazl → fflate rewrite in #148, which kept the hand-rolledstatSyncwalk.The fix
All three walkers in
packages/cdktn/src/private/fs.tsnow uselstatSyncand treat symlinks first-class, restoringarchiver/cdktf parity:archiveSyncemits real symlink entries: thereadlinktarget as STORED entry data withS_IFLNK | permsin the unix external attributes and version-made-by host = Unix — exactly whatarchiverproduces and what Info-ZIPunzip, Goarchive/zip, and the AWS Lambda runtime (verified live onnodejs22.xin TerraformAsset: AssetType.ARCHIVE follows symlinks (statSync), duplicating content and crashing on cyclic links #320) recreate as symlinks. Never recursing through links makes cycles unreachable by construction. fflate 0.8.2 supports this natively via per-file[data, { os, attrs, level }]tuples (attrs must be caller-pre-shifted,(mode << 16) >>> 0).hashPathhashes a symlink by its target path instead of following it — retargeting a link still changes the asset hash, but shared targets are no longer double-counted and cycles no longer crash the constructor.copySync(AssetType.DIRECTORY) recreates symlinks withfs.symlinkSync.Two latent zip-metadata gaps fixed along the way (both also
archiver/cdktf parity):.binscripts).mtimetoDate.now(), so every synth previously produced different zip bytes — perpetual drift for anyone hashing the archive, e.g.filebase64sha256). The pin uses the local-timeDateconstructor deliberately: fflate encodes DOS dates from local getters and rejects years < 1980, so a UTC midnight date would underflow to 1979 in timezones west of UTC.Prior art considered and rejected:
terraform-provider-archivealways dereferences and has no cycle detection — it accumulated the opt-inexclude_symlink_directoriesband-aid (v2.4.0, hashicorp/terraform-provider-archive#183; follow-up defect fixed in v2.4.2, #298) and stillELOOPs on cycles. Reverting toarchiverwould reintroduce theexecSyncchild-process bridge that #148 removed (its API is async-only).Hash compatibility (updated after review — downscoped)
Review surfaced a domain collision in the first cut of the
hashPathchange (file bytes and symlink-target bytes shared one unframed stream). Per the requested downscope this PR now carries only the compatibility-preserving resolution (reviewer's option 3):The canonical entry-framed scheme and its
canonicalAssetHashesfeature flag moved to the stack: design issue #322 → implementation #323 → docs #324.TerraformModuleAssetstays in scope per review: it shares the corrected copier (its private copier sent directory symlinks intocopyFileSync→EISDIR) with a regression test.Testing
packages/cdktn/test/archive-symlink.test.ts: 10 tests — the issue's repros (file/dir symlink preservation via system-unzipround-trip +lstat, dedup of shared targets,ELOOPon cycles for botharchiveSyncandhashPath, hash-changes-on-retarget),copySyncsymlink recreation, executable-bit preservation, and byte-identical archives across runs. 5 of the 6 core repros fail onmain.cdktnpackage suite green (453 passed; the one pre-existingmatchers.test.ts→toPlanSuccessfullyfailure shells out to a realterraform planand fails identically without this change).zipinfoshowslrwxr-xr-x ... storentries forlink-a/link-b, one payload copy (538-byte zip vs 3× 200 KiB before), and the cyclic tree archives to a 120-byte zip instead of aborting synth.Checklist
🤖 Generated with Claude Code