Repository navigation
fix(cli): resolve upgraded providers from the CLI's own registry - #489
Conversation
`cdktn provider add random@=3.9.0` in an OpenTofu-only project reported
Pre-built provider does not exist for the given constraints.
and fell back to local generation, even though @cdktn/provider-random exists. The
same command in a Terraform-targeting project finds it.
`getNpmPackageName` compared the full normalized source of each entry in the
published prebuilt map against the incoming constraint. The map keys providers
without a hostname, so its entries normalize under the default Terraform
registry, while the constraint normalizes against whatever the project declares.
In an OpenTofu-only project those never matched. A prebuilt package identifies a
provider rather than a registry, so the comparison is now on namespace and name.
The existing `getNpmPackageName` block is `describe.skip` with a TODO about an
unknown URL - it mocks www.cdk.tf while the code now reads provider.json from
raw.githubusercontent.com - which is part of why this went unnoticed. The three
new cases mock the real URL and are not skipped; only the OpenTofu one fails
against the previous implementation, so Terraform behaviour and the
no-such-package path are unchanged.
Found while auditing what remained after open-constructs#479, open-constructs#489, open-constructs#484 and open-constructs#490: no
integration fixture declares `targetVersions`, so the OpenTofu matrix rows cover
the CLI being OpenTofu rather than the project targeting it. Confirmed by
pointing the fixture at `targetVersions.opentofu` and running under tofu 1.12.6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ry (#493) Independent of #479 / #489 / #484 / #490 — branched off `main`, touches only `getNpmPackageName`. ## Symptom `cdktn provider add random@=3.9.0` in an **OpenTofu-only** project (`targetVersions: { opentofu: ... }`): ``` Checking whether pre-built provider exists for the following constraints: provider: random version : =3.9.0 Pre-built provider does not exist for the given constraints. Adding local provider registry.opentofu.org/hashicorp/random with version constraint =3.9.0 to cdktf.json ``` `@cdktn/provider-random@14.1.0` exists, and the identical command in a Terraform-targeting project finds it. So OpenTofu-only projects silently never get prebuilt providers — they always fall back to local generation, which is slower and produces different output. ## Cause `getNpmPackageName` compared full normalized sources: ```ts ProviderConstraint.fromConfigEntry(p).source.toLowerCase() === constraint.source.toLowerCase() ``` The published map keys providers *without* a hostname (`{ "random": "hashicorp/random" }`), so its entries normalize under the default Terraform registry, while `constraint` normalizes against whatever the project declares. In an OpenTofu-only project that is `registry.opentofu.org/...` versus `registry.terraform.io/...` — never equal. A prebuilt package identifies a **provider**, not a registry, so the comparison is now on namespace and name. ## Why no test caught it The existing `getNpmPackageName` block is `describe.skip`, with `// TODO rebuild these tests when final url is known` — it mocks `www.cdk.tf` while the code now reads `provider.json` from `raw.githubusercontent.com`. So this function had no live unit coverage at all. The three new cases mock the real URL and are **not** skipped. Only the OpenTofu one fails against the previous implementation; the Terraform case and the no-such-package case pass either way, so behaviour there is provably unchanged. I left the stale skipped block alone rather than widen scope. `@cdktn/cli-core`: 18 suites, 201 passed, 6 pre-existing skips. ## The gap this came from Found while auditing what remained after #479, #489, #484 and #490. **No integration fixture declares `targetVersions`**, so the OpenTofu matrix rows exercise *the CLI being OpenTofu*, not *the project targeting OpenTofu* — and every defect in this family only appears when `registryForTargetVersions` returns OpenTofu. Confirmed by pointing the fixture at `targetVersions.opentofu` and running under tofu 1.12.6; that is also how the `provider upgrade` failure fixed in #489 was found. Worth closing that gap properly with a fixture that declares an OpenTofu target. Filing separately rather than bundling it here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
94db4c7 to
13e03aa
Compare
|
| Scenario | Lock before | cdktn's init with 72da894 (any public registry) | cdktn's init with a79880b (running CLI only) |
|---|---|---|---|
| Terraform → OpenTofu, no bump | registry.terraform.io 3.6.0 |
tofu init ✅ OpenTofu rewrites the lock entry to registry.opentofu.org itself ("Dependency lock file entries automatically updated… version selections were preserved"), apply is a no-op |
tofu init + providers lock ✅ |
| Terraform → OpenTofu, bump to 3.6.1 | registry.terraform.io 3.6.0 |
tofu init -upgrade ✅ |
plain tofu init ❌ Could not resolve provider hashicorp/random: locked provider … must use tofu init -upgrade |
| OpenTofu → Terraform, no bump | registry.opentofu.org 3.6.0 |
terraform init ✅ keeps both entries (OpenTofu state references its own provider), apply is a no-op |
not run |
| OpenTofu → Terraform, bump | registry.opentofu.org 3.6.0 |
terraform init -upgrade ✅ |
not run |
- Case 1 in the commit message ("counted as locked,
initwas skipped, and OpenTofu then failed on the lock file"):initalways runs. The lock checks only add-upgradeand the follow-upproviders lock -platform=linux_amd64. OpenTofu migrates the Terraform entry on its own, so nothing fails. - The bump after a switch fails because of the running-CLI filter. a79880b ignores the other registry's entry, so no
-upgradeis added and OpenTofu rejects the plaininit. 72da894 handles it. - Case 2 (a stale entry for the inactive registry forcing an unneeded
-upgrade) is real, but only after a switch, and it costs one extra-upgrade.
Switching binaries over the same .terraform and lock file is awkward in plain HCL too, so I'd rather not add per-init CLI detection to work around it.
Suggested fix
One commit on this branch: revert the running-CLI part of a79880b and keep its tests.
- Remove
runningCliRegistry()and thecliRegistryparameter. A bare source matches its lock entry under any public registry, as in 72da894. A source that names a host still matches exactly. - Keep
needsLockfileUpdate/needsUpgradeas the exported functions production calls, with their tests minus thecliRegistrycases, including the lock that holds both registries. - Make
lockAddressesFormodule-private and drop itsdescribeblock, since the two decision functions already cover it.
This removes the log output and the extra spawn. It also removes the forced providers lock on every init when the CLI can't be identified (e.g. a wrapper set as TERRAFORM_BINARY_NAME).
Follow-up, not this PR: warning when the declared target and the running binary disagree belongs in the opt-in validateInstalledBinary check, ideally once per command from the cached commons terraformCli() probe rather than per stack.
Happy to push that commit here if that works for you.
|
Thanks for running the switch cases. You're right on every point, and I reproduced the decisive one with Terraform 1.14.3 and OpenTofu 1.12.6: after a Terraform deploy, bumping 3.6.0 → 3.6.1 under OpenTofu gets no I've pushed the change you outlined as b668906. It reverts the running-CLI detection, so there's no extra |
|
Empirical repro ( Two follow-ups:
Repro setup and scenario runner: https://github.com/sakul-learning/cdktn-provider-registry-scenarios |
…ness Three assumptions in the integration layer only hold under Terraform, so the fixtures could not run against OpenTofu. versions.json is keyed by fully qualified provider name, whose host is whichever registry the fetching CLI used. Five assertions indexed those keys with a literal `registry.terraform.io/...`, which reads `undefined` under OpenTofu. Adds `providerVersion()` to the test helper, matching on the provider part the way the provider-generator unit test already does, and uses it at every site. Two of the five are in `onWindows` branches that no job runs today, since `windows_integration` is `if: false`; they are corrected so re-enabling that job does not reintroduce the problem. The helper still returns `undefined` for an absent provider, so the assertions stay real guards. `TestDriver.validate()` shelled out to a bare `terraform`. The CI image points that symlink at DEFAULT_TERRAFORM_VERSION, so the helper validated with 1.16.1 even on the 1.5.7 matrix rows, and would have run Terraform on an OpenTofu row. It now honours TERRAFORM_BINARY_NAME, which the workflow already exports. Pins the two provider-command targets to OpenTofu as well. They drive provider resolution through the CLI's own registry, so they are what would catch a regression in the registry selection added by open-constructs#445. Their Terraform entries restate the `tested` default so the pin does not silently drop 1.5.7; the matrix grows by two jobs. Also normalises path separators in the matrix builder. `jest --listTests` emits Windows separators, so no target matched the `test/` prefix and every configured pin tripped the "not a discovered test target" guard, making the builder impossible to run outside CI. Verified every provider, module and exact version the fixtures pin is present on registry.opentofu.org, checking a known-absent provider and a nonsense path first to confirm that endpoint 404s rather than soft-404ing like search.opentofu.org. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…istry `CdktfConfigManager` stripped the literal `registry.terraform.io/` prefix and compared existing entries via `ProviderConstraint.fromConfigEntry()` with no registry, which defaults to Terraform. Both only hold for a Terraform project, so open-constructs#445 left this file behind when it made provider resolution registry-aware. In an OpenTofu project `cdktn provider upgrade random@=3.2.0` produced: ["hashicorp/random@=3.1.3", "registry.opentofu.org/hashicorp/random@=3.2.0"] The prefix did not match so the source stayed qualified, and the comparison normalised the existing entry under the wrong registry, so it was not recognised as the same provider and was left in place next to the new one. The project was left with two conflicting constraints for one provider. The manager now derives its registry from the `targetVersions` of the very file it manages, which is the same source of truth open-constructs#445 used, and needs no plumbing from callers. The host is dropped only when it matches that registry, so a source explicitly qualified with another registry stays qualified - OpenTofu users have been told to fully qualify providers and that must keep working. Terraform behaviour is unchanged: of the five new test cases, the four covering OpenTofu fail against the previous implementation and the Terraform one passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…fect Pinning typescript/provider-upgrade-command to OpenTofu exposed a real defect, not a test problem: `cdktn provider upgrade` leaves `.gen/versions.json` with two entries for one provider, keyed by different registry hosts, and the stale one is read back. Filed as open-constructs#483 with the reproduction. The target goes back to the `tested` Terraform cross-product until that is fixed; provider-add-command and provider-features keep their OpenTofu pins and pass. Also makes `providerVersion()` throw when a versions.json holds more than one entry for the same provider. It previously returned the first match, which was only accidentally the stale one - had insertion order gone the other way the duplicate would have passed silently. A loud failure naming both entries is what this helper owes its callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes open-constructs#483. `cdktn provider upgrade` in an OpenTofu project fetched the provider from registry.terraform.io, and left `.gen/versions.json` holding two entries for one provider with the stale version winning any lookup by name. `readProviderSchema` writes a throwaway `main.tf.json` whose `required_providers` source comes straight from the target, and a source that names a registry pins the CLI to it. The upgrade handler built `constraintsToGenerate` from `ProviderConstraint.fromConfigEntry(provider)` with no registry, which normalizes to `registry.terraform.io/...`, so that host reached the schema fetch. The add path passes no `constraintsToGenerate`, so `get` derived bare constraints from cdktf.json and OpenTofu resolved them from its own registry - which is why the two commands disagreed and the file ended up with one key per host. It now passes `namespace/name`, leaving resolution to the CLI. Of the ten `new TerraformProviderConstraint(...)` sites, this was the only one handing on a normalized FQPN. `updateVersionsFile` no longer carries a previous entry forward for a provider regenerated this run. It matched the old entry by provider part but re-inserted it under its old fully qualified key, so a host change left both - defence in depth now that the host above is stable. `LocalProviderVersions` stripped the literal `registry.terraform.io/` prefix, so under OpenTofu `versionForProvider("random")` found nothing and version detection and `provider list` came up empty. It now strips any public registry host. Restores the OpenTofu pin on typescript/provider-upgrade-command that open-constructs#479 had to drop, bringing the matrix back to three tofu rows. Verified against both CLIs locally, each leaving a single correctly-keyed entry: terraform 1.14.3 gives registry.terraform.io/hashicorp/random 3.2.0, opentofu 1.12.6 gives registry.opentofu.org/hashicorp/random 3.2.0. Of the four new LocalProviderVersions tests, the two OpenTofu cases fail against the previous implementation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cdktn provider upgrade` failed outright in an OpenTofu-only project:
Usage Error: Trying to upgrade random but it is not installed,
please use "cdktn provider add random" to add it.
even with `hashicorp/random@=3.1.3` declared in cdktf.json.
The handler built its constraint with `ProviderConstraint.fromConfigEntry()` and
no registry, which defaults to Terraform, so it normalized to
`registry.terraform.io/hashicorp/random`. `CdktfConfigManager` resolves the
project's entries against the registry the project declares, giving
`registry.opentofu.org/hashicorp/random`, so `hasProvider` compared two
different sources and reported an installed provider as missing.
It now resolves the argument the way the project's own entries resolve. Without
this the earlier commit on this branch only fixed the schema fetch, so the
command still aborted before reaching it in exactly the projects it was meant
to help.
No integration fixture declares `targetVersions`, which is why this was not
caught: the OpenTofu matrix rows cover the CLI being OpenTofu, not the project
targeting it. Verified by pointing the fixture at `targetVersions.opentofu` and
running under tofu 1.12.6 - failing before, and afterwards leaving cdktf.json at
=3.2.0 with a single `registry.opentofu.org/hashicorp/random` entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ked it
`cdktn deploy` failed for an OpenTofu project after a provider version bump:
Could not resolve provider hashicorp/random: locked provider
registry.opentofu.org/hashicorp/random 3.6.0 does not match configured
version constraint 3.6.1; must use tofu init -upgrade
Before diff and deploy, CdktfStack compares the synthesized `required_providers`
against `.terraform.lock.hcl` to decide whether to run `init` and whether an
`init -upgrade` is needed. Both lookups were exact on the fully qualified
address, and `requiredProviders()` normalized each source with
`new ProviderConstraint(source, version)` - no registry, so always
`registry.terraform.io/...`.
Synthesized sources are bare in practice: generated bindings copy the source as
written in cdktf.json, and published prebuilt packages ship bare sources too
(checked @cdktn/provider-aws 25.7.2 and @cdktn/provider-random 15.0.1). OpenTofu
locks a bare source under its own registry, so the comparison missed for every
OpenTofu project in both directions:
- `checkNeedsLockfileUpdate` never found the provider, so every diff and deploy
ran `init`
- `checkNeedsUpgrade` looked the lock entry up by its own address, missed, and
treated the provider as no longer used, so a version bump never triggered
`init -upgrade` - and the plain `init` that did run rejects the bump, as above
`lockAddressesFor` now returns the addresses that can satisfy a source: exactly
its own for a source that names a host, and one per public registry for a bare
source, since which one the CLI locked depends on whether it is Terraform or
OpenTofu. Both checks go through it. An explicit host still has to match
exactly; treating every public registry as equivalent would report an explicit
`registry.terraform.io` source as locked when only the OpenTofu entry exists,
skip `init`, and leave the CLI to fail. Projects that fully qualify their
providers were never affected, since their sources match exactly.
Verified end to end with the OpenTofu-target integration fixture: with this
change reverted, its provider-bump test fails with the error above; with it, the
bump deploys. Terraform projects are unchanged - Terraform locks a bare source
under registry.terraform.io, which was the only address checked before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes open-constructs#488. `run-against-dist.mjs` published dist/js to Verdaccio and then ran `pnpm add cdktn-cli`. Every local build carries the same version, and nothing invalidates pnpm's view of it - only Verdaccio's storage is cleaned - so pnpm could install an earlier build it already held. Observed directly: a freshly built tarball containing a fix, and an installed `bundle/bin/cmds/handlers.js` from that morning whose shipped source map did not contain it. The run reported normally, so a stale CLI looked like real evidence. The install now names the tarball path. The glob is `cdktn-cli-[0-9]*.tgz` rather than a fixed `0.0.0` because release builds stamp real versions through align-version, and the digit keeps `cdktn-cli-core-*.tgz` - which a plain `cdktn-cli-*` would also match - out of it. Anything other than exactly one match fails the run with the files found. `--registry` stays so the CLI's @cdktn/* dependencies still resolve from Verdaccio. CI was not affected, since jobs start from a clean container; this only bit local verification, which is where a fix is iterated and a stale result is most likely to be misread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes open-constructs#494. Every fixture left `targetVersions` unset, and `cdktn init` writes none, so `registryForTargetVersions()` returned the Terraform registry in every integration run. The matrix's OpenTofu rows only changed which CLI ran, and nothing exercised a project whose registry resolution follows its declared target - which is where the recent run of OpenTofu defects lived. `typescript/opentofu-target` declares `targetVersions: { opentofu: ">=1.6.0" }` and is pinned to the OpenTofu runtime alone, since driving an OpenTofu-targeting project with the Terraform CLI is not a pairing worth paying a job for. One shared project walks the lifecycle a user does: - bindings come from the OpenTofu registry: one `registry.opentofu.org` entry in versions.json, and docs links to search.opentofu.org - synth keeps the provider source bare, since a host would pin the CLI - deploy installs from registry.opentofu.org - `provider list` reports the generated version, through the LocalProviderVersions reader - a provider bump with `provider upgrade` redeploys at the new version Each assertion was checked to fail for the defect it guards rather than pass incidentally. Reverting the lock-file matching fix makes the bump fail with "locked provider registry.opentofu.org/hashicorp/random 3.6.0 does not match configured version constraint 3.6.1". The fixture also ran against a CLI shown by extracting its tarball to contain the fix, after three separate stale-build layers had produced misleading results. Adds one matrix job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`package` depends on `build`, but `package:js`, `package:python`, `package:java`, `package:dotnet` and `package:go` had no target defaults, so they repacked whatever compiled output was already in `lib/` and `bundle/`. The tarball gets a fresh timestamp while its contents are stale, which is the hardest kind of stale artifact to notice: run-against-dist.mjs's own error message points people at `pnpm run package:js`, and a fix made since the last full build silently goes untested. Observed directly: after source edits, `package:js` produced a 07:06 tarball whose `lib/features.js` was from 06:21 and lacked the change. With `dependsOn: ["build"]` the same command runs the dependency build chain first. Only `dependsOn` is added, not caching: a cached `package` target is what served a weeks-old CLI tarball in the first place. `build` is cached, so the added dependency costs a cache hit when nothing changed. No CI workflow invokes these targets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test/Readme.md still said "OpenTofu is currently not part of the CI image; OpenTofu runs are local only for now". The image has shipped OpenTofu as `tofu<version>` since the jsii-terraform image update, unit tests tagged `unit-test:terraform` run once under it, and four integration targets are now pinned to it. Documents the `opentofu` block in .terraform.versions.json, how a test opts into OpenTofu through `pinnedRuntimes` and is kept out of it with `terraformOnly`, and the distinction that open-constructs#494 turned on: running under the OpenTofu CLI does not exercise a project that targets OpenTofu, because registry resolution follows the project's declared `targetVersions`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found that `lockAddressesFor` accepted a bare source's lock entry from either public registry. A bare source resolves according to the CLI that runs, so Terraform and OpenTofu entries are not interchangeable: - a project deployed with Terraform and then run with OpenTofu has only a registry.terraform.io entry; that counted as locked, `init` was skipped, and OpenTofu then failed on the lock file - with both entries present, a stale entry for the inactive registry could force an unnecessary `init -upgrade` CdktfStack now identifies the running CLI from `version` output and resolves a bare source against that CLI's registry only. When it cannot be identified, both public registries are candidates and the decisions stay conservative: refresh unless every candidate is locked, and upgrade if any conflicts. A source that names a host still matches exactly that host. The two decisions move out of CdktfStack's private methods into `needsLockfileUpdate` and `needsUpgrade`, so the tests exercise the code production calls. They cover a lock holding both public-registry entries with conflicting constraints, a switch from Terraform to OpenTofu, an unidentified CLI, and an explicit host against another registry's entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found that `provider upgrade` rebuilt every updated constraint for schema generation as `namespace/name`, dropping any host. In an OpenTofu-targeted project, upgrading `registry.terraform.io/hashicorp/random` kept the Terraform source in cdktf.json while generation fetched `hashicorp/random`, which OpenTofu resolves from its own registry - bindings from a different registry than the configured provider. Private hosts were dropped the same way. ProviderConstraint now records whether its source named a host as given, and `requiredProvidersSource` keeps such a host while still dropping one added by normalization. The handler uses it, and the rule is unit tested where the handler cannot be. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of open-constructs#479 noted that the comment on the provider-add/upgrade OpenTofu pins claimed they catch regressions in registry selection. Their fixtures declare no targetVersions, so they exercise the OpenTofu CLI, not a project targeting OpenTofu; typescript/opentofu-target covers that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry again Reverts the running-CLI part of a79880b, from review. That commit made the lock checks match a bare provider source only against the registry of the CLI that was running, identified by calling `terraform version` before every init. It was justified by two switching cases, and neither held up: - "counted as locked, `init` was skipped, and OpenTofu then failed on the lock file" - `init` always runs; these checks only add `-upgrade` and a follow-up `providers lock`. OpenTofu rewrites a registry.terraform.io entry to its own registry on init and keeps the selected version. - a stale entry for the other registry forcing an unneeded `-upgrade` - real, but only after a switch, and it costs one extra `-upgrade`. And the filter broke the realistic case. Reproduced with Terraform 1.14.3 and OpenTofu 1.12.6 on a stack deployed by Terraform and then bumped from 3.6.0 to 3.6.1 under OpenTofu: the filter ignored the Terraform entry, added no `-upgrade`, and `tofu init` failed with "locked provider ... must use tofu init -upgrade". Matching either public registry adds `-upgrade` and the bump applies. It also put `terraform version` output - including "Your version of Terraform is out of date!" - into every diff, deploy and destroy log, once per stack, and spawned that process for every project; on main it only ran behind the opt-in validateInstalledBinary. A bare source again matches its lock entry under any public registry; a source that names a host still matches only that host. `needsLockfileUpdate` and `needsUpgrade` stay the exported functions CdktfStack calls, and their tests now cover the Terraform-to-OpenTofu bump. `lockAddressesFor` is module-private. Filtering bare sources to one registry fails three of the cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sakul-learning's scenario runner showed `provider upgrade random@=3.6.1` turning `registry.opentofu.org/hashicorp/random@=3.6.0` in an OpenTofu-targeted project's cdktf.json into `hashicorp/random@=3.6.1`. Matching the entry was the fix in this PR, but the update was then written back through the step that shortens the project registry's own host, so the host the user wrote was dropped. The fully qualified upgrade argument dropped it the same way. An update now keeps the existing entry's spelling of its source and changes only the version. New entries are still written in the short form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
368efb3 to
a566030
Compare
|
Independent rerun of the scenario runner against
Rebased onto main (no changes to the PR commits) and set to auto-merge. |
… changes (#490) Fixes [#462](#462). Independent of #479 / #489 / #484 — branched off `main` and touches only `filterAlreadyGenerated`. ## The problem #443 made generated docs links follow the project's declared `targetVersions`. `filterAlreadyGenerated` decided freshness from the provider version map and the CDKTN version alone, so this left stale output: 1. `cdktn get` with no `targetVersions` → bindings carry `registry.terraform.io` links 2. set `targetVersions: { opentofu: ... }`, provider versions untouched 3. `cdktn get` → provider considered already generated, skipped `constraints.json` already stamped `targetVersions`, but as a diagnostic that was explicitly *never read back*, so the information was there and unused. ## The fix Compare the registry class, derived through `registryForTargetVersions`, and regenerate when it differs. **The class, not the raw ranges, is the correct key — and that isn't a judgement call.** I traced every use of `targetVersions` inside the generator: it is consumed *only* through `registryForTargetVersions`, for `ResourceModel.docsRegistryName` and `linkToDocs`. Nothing else. `ResourceParser` takes it but only forwards it to `ResourceModel`; it does not narrow the generated surface. So a version range moving within one class provably cannot change any generated output, and forcing a regeneration for it would be pure waste. That also means the docblock calling the field "purely informational — never read back" is now wrong on both counts, and its stated rationale ("doesn't affect the generated surface") was already misleading. Corrected. ## Tests Five cases; **the two transitions fail against the previous implementation**, the three that should keep skipping already passed: | case | expected | |---|---| | nothing changed | skip | | → OpenTofu-only | **regenerate** | | ← away from OpenTofu-only | **regenerate** | | range moves within a class | skip | | both products declared (same class as declaring nothing) | skip | They drive `filterAlreadyGenerated` directly against a hand-written `constraints.json`, so they need no network and no terraform binary. `constructs-maker` and `module-generator` suites unchanged: 27 tests, 10 snapshots. ## Scope note This also covers generated **module** links once #484 lands, since those derive from the same class. Not a dependency — the two are independent. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
🤖 Release PR — merge to cut a new release. Kept open and rebased as commits land on `main`. --- <details><summary>0.25.0</summary> ## [0.25.0](v0.24.0...v0.25.0) (2026-10-27) ### ⚠ BREAKING CHANGES * **lib:** `IAssetPackaging` gains `acceptsDirectorySource` as a required member. Custom implementations of the interface must add it. * **lib:** ValidateBinaryVersion and ValidateTerraformVersion are removed (they were only reachable through deep imports, not the cdktn package root). Synth no longer executes the terraform/tofu binary for feature validation; set validateInstalledBinary: true in cdktf.json to verify the installed binary before commands that run it. * **lib:** resource moves (moved blocks) are now validated at synth against the project's declared targetVersions (default terraform >=1.5.7, opentofu >=1.6.0) instead of the locally installed binary. Projects that declare terraform targets below 1.5.0 now fail synth; projects whose installed binary is older than their declared targets should set validateInstalledBinary: true in cdktf.json. * **lib:** import blocks (importFrom / ImportableResource) are now validated at synth against the project's declared targetVersions (default terraform >=1.5.7, opentofu >=1.6.0) instead of the locally installed binary. Projects that declare terraform targets below 1.5.0 now fail synth; projects whose installed binary is older than their declared targets should set validateInstalledBinary: true in cdktf.json. * **lib:** terraform_data resources are now validated at synth against the project's declared targetVersions (default terraform >=1.5.7, opentofu >=1.6.0) instead of the locally installed binary. Projects that declare terraform targets below 1.4.0 now fail synth; projects whose installed binary is older than their declared targets should set * **lib:** cloud backends (the cloud block) are now validated at synth against the project's declared targetVersions (default terraform >=1.5.7, opentofu >=1.6.0) instead of the locally installed binary. Projects that declare terraform targets below 1.1.0 now fail synth; projects whose installed binary is older than their declared targets should set validateInstalledBinary: true in cdktf.json. * **lib:** IAssetPackaging gains a required member omitsDirectoryEntries; external implementations must add it. Custom TerraformAsset assetHash values are now validated against `/^[A-Za-z0-9_.-]+$/` and throw if they contain other characters (e.g. "feature/my-branch"). * **lib:** add asset pipeline foundation types and hash utilities ([#371](#371)) ### Features * **cli:** add a typescript-pnpm init template and deprecate Yarn ([#422](#422)) ([81da164](81da164)) * **cli:** move the TypeScript template to tsx and TypeScript 6 ([#396](#396)) ([b50349d](b50349d)) * **cli:** point AI coding agents at the CDKTN docs MCP server ([#518](#518)) ([f257cd9](f257cd9)) * **cli:** rename `--cdktf-version` to `--cdktn-version` in `init` ([#228](#228)) ([dd3818e](dd3818e)) * **cli:** replace HashiCorp checkpoint telemetry with Sentry usage metrics and consent ([#411](#411)) ([e17046a](e17046a)) * **cli:** report aggregated stack, provider and command usage metrics ([#413](#413)) ([ed9b1e5](ed9b1e5)) * **cli:** report the installed binary, target versions, platform and package manager in usage metrics ([#412](#412)) ([bfe26a8](bfe26a8)) * **cli:** resolve providers from the registry the project targets ([#445](#445)) ([78898ae](78898ae)) * **deps:** adopt jsii 6.0 and TypeScript 6 ([#395](#395)) ([26fa8fa](26fa8fa)) * **gha:** Revert "run the affected unit tests against OpenTofu ([#439](#439))" ([#446](#446)) ([ae015ef](ae015ef)) * **gha:** run the affected unit tests against OpenTofu ([#439](#439)) ([823682d](823682d)) * **gha:** run the affected unit tests against OpenTofu ([#447](#447)) ([c0c4c1c](c0c4c1c)) * **lib:** add asset bundling extension interface ([#442](#442)) ([7839625](7839625)) * **lib:** add asset pipeline foundation types and hash utilities ([#371](#371)) ([a008d12](a008d12)) * **lib:** add asset staging core with no bundling ([#433](#433)) ([44d8bd1](44d8bd1)) * **lib:** add support for single-file bundler outputs ([#464](#464)) ([3592db0](3592db0)) * **lib:** pass ignore strategy through terraform asset ([#472](#472)) ([705e5b9](705e5b9)) * **tests:** add per-test runtime pinning to the integration matrix ([#405](#405)) ([17c2278](17c2278)) ### Bug Fixes * **cli:** catch failures of the background environment collection ([#531](#531)) ([c5feed8](c5feed8)) * **cli:** correct the constructs range comment in the TypeScript template hook ([#408](#408)) ([9fc0e43](9fc0e43)) * **cli:** don't crash after a successful deploy when an output is missing ([#375](#375)) ([0bbd64a](0bbd64a)) * **cli:** fail cleanly when the --outputs-file write fails ([#376](#376)) ([8f9d03a](8f9d03a)) * **cli:** make `cdktn get` remove stale files for resources removed from a provider ([#421](#421)) ([d6e28dc](d6e28dc)) * **cli:** match prebuilt providers regardless of the project's registry ([#493](#493)) ([ea82124](ea82124)) * **cli:** probe with `version` instead of `-v` ([#471](#471)) ([b87ee22](b87ee22)) * **cli:** require patched Node 22.23.4 and bump undici for security fixes ([#516](#516)) ([76436f5](76436f5)) * **cli:** resolve bundled handlers next to the entrypoint ([#414](#414)) ([49600c1](49600c1)) * **cli:** resolve upgraded providers from the CLI's own registry ([#489](#489)) ([10672ee](10672ee)) * **cli:** stop the top-level error handler racing yargs ([#410](#410)) ([5d0c2ef](5d0c2ef)) * **deps:** move jsii-rosetta to 6.0.16 for stream-json 3 ([#463](#463)) ([f1ec16f](f1ec16f)) * **gha:** authenticate to public.ecr.aws in the docker image build ([#519](#519)) ([c65bf2f](c65bf2f)) * **gha:** don't fail the pnpm upgrade jobs on ignored dep builds ([#399](#399)) ([5b66342](5b66342)) * **gha:** move the jsii package cache out of the container HOME ([#514](#514)) ([df5b490](df5b490)) * **gha:** pass -R to gh pr edit in release_github relabel step ([#372](#372)) ([358edc7](358edc7)) * **gha:** push only the prerelease tag from prepare-next ([#476](#476)) ([848f2a6](848f2a6)) * **lib:** avoid terraform init plugin-cache race in test matchers ([#467](#467)) ([26847f9](26847f9)) * **lib:** retry the staging install without --prefer-offline outside CI ([#407](#407)) ([7bb26e7](7bb26e7)) * **provider-generator:** pass the default provider configuration a module requires on get ([#466](#466)) ([a5225ab](a5225ab)) * **provider-generator:** pick the docs registry from targetVersions ([#443](#443)) ([00f2884](00f2884)) * **provider-generator:** pick the module docs registry from targetVersions ([#484](#484)) ([8200330](8200330)) * **provider-generator:** regenerate bindings when the target registry changes ([#490](#490)) ([813e022](813e022)) * **provider-generator:** satisfy module provider configuration aliases on get ([#383](#383)) ([54a8217](54a8217)) * **provider-generator:** stop a provider_functions resource clobbering the functions submodule ([#403](#403)) ([cf19de9](cf19de9)) * **tests:** await the pip install in the python provider command tests ([#478](#478)) ([1c83a36](1c83a36)) * **tests:** bound and retry the dist publish to the local registry ([#425](#425)) ([cc9f9e2](cc9f9e2)) * **tests:** build the edge provider bindings in their own cached Nx target ([#482](#482)) ([f480cc9](f480cc9)) * **tests:** drop unused ts-jest transform from shared jest preset ([#382](#382)) ([03f6e16](03f6e16)) * **tests:** mark the terraform-cloud target Terraform-only in the matrix ([#430](#430)) ([b0deefa](b0deefa)) * **tests:** run example builds with the Nx cache skipped ([#477](#477)) ([3e25e13](3e25e13)) * **tools:** declare the cdktn-cli bundle as a cached Nx output with its real inputs ([#485](#485)) ([10dd118](10dd118)) * **tools:** make the workspace build, package, lint and test on Windows ([#364](#364)) ([8dcddbe](8dcddbe)) ### Code Refactoring * **lib:** Remove the ValidateBinaryVersion synth-time binary probe ([#457](#457)) ([e60142e](e60142e)) * **lib:** Use declared targetVersions for synth-time validation of import blocks ([#454](#454)) ([c0097f4](c0097f4)) * **lib:** Use declared targetVersions for synth-time validation of resource moves ([#453](#453)) ([43edc1e](43edc1e)) * **lib:** Use declared targetVersions for synth-time validation of terraform_data ([#455](#455)) ([a1c125b](a1c125b)) * **lib:** Use declared targetVersions for synth-time validation of the cloud backend ([#456](#456)) ([4a83507](4a83507)) ### Miscellaneous Chores * add OpenTofu and current Terraform to the jsii-terraform image ([#394](#394)) ([2f29d5a](2f29d5a)) * declare complete Nx build outputs so cache hits restore them ([#474](#474)) ([794cf77](794cf77)) * **deps:** move the remaining packages to TypeScript 6 ([#406](#406)) ([c55d360](c55d360)) * **deps:** move the root TypeScript to 6 ([#417](#417)) ([3cae1e2](3cae1e2)) * **deps:** Update xstate to v5.32.4 ([#333](#333)) ([5ebf74f](5ebf74f)) * **deps:** upgrade @sentry/node to 10.x ([#409](#409)) ([2714fff](2714fff)) * **deps:** use caret ranges for unbundled runtime dependencies ([#503](#503)) ([e722357](e722357)) * **docs:** add the test-audit skill for authoring and auditing tests ([#475](#475)) ([909c612](909c612)) * **examples:** move the TypeScript examples to tsx and TypeScript 6 ([#419](#419)) ([857e182](857e182)) * Flexible python version ([#420](#420)) ([955f62a](955f62a)) * **gha:** adopt the new jsii-terraform image and test Terraform 1.5.7 + 1.16.1 ([#398](#398)) ([3ad760b](3ad760b)) * **gha:** gate dependency upgrades on release age ([#418](#418)) ([b611a87](b611a87)) * **gha:** retire Docker Hub publishing of jsii-terraform ([#388](#388)) ([e53cddf](e53cddf)) * **gha:** select the CLI by binary name instead of concatenating a version ([#438](#438)) ([914da56](914da56)) * **gha:** skip the remote Nx cache in the release workflow ([#487](#487)) ([bce91da](bce91da)) * **lib:** stop publishing build state in the cdktn package ([#397](#397)) ([51e4a95](51e4a95)) * npm-check-updates && pnpm update ([#326](#326)) ([7095f6b](7095f6b)) * **readme:** point to the cdktn-io/action GitHub Action ([#504](#504)) ([e387852](e387852)), closes [#58](#58) * **tests:** add timeout handling to spawned commands ([#432](#432)) ([ddb4a8a](ddb4a8a)) * **tests:** run the function-binding scripts with tsx ([#431](#431)) ([fec67bf](fec67bf)) ### Tests * **lib:** ensure TerraformAsset stage within stack own directory ([#434](#434)) ([9a80366](9a80366)) * **lib:** move matcher Docker fixture to a provider version OpenTofu accepts ([#429](#429)) ([7b0da12](7b0da12)) * **tests:** move the python providers fixture to Docker provider 3.9.0 ([#440](#440)) ([a678473](a678473)) * **tests:** move the remaining kreuzwerker/docker pins off unsigned versions ([#459](#459)) ([d855cc1](d855cc1)) * **tests:** move the testing-matchers fixtures to Docker provider 3.x ([#436](#436)) ([8e2c60e](8e2c60e)) * **tests:** re-enable the Python edge integration test ([#404](#404)) ([0a80626](0a80626)) </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>
Fixes #483, #488 and #494. Supersedes #479, whose commits are all included here (see below). Independent of #484, #490 and #493.
The bug is worse than the symptom
The visible failure was a stale version in
versions.json. The actual behaviour:cdktn provider upgradein an OpenTofu project downloaded the provider fromregistry.terraform.io, silently ignoring the product the project targets.readProviderSchemawrites a throwawaymain.tf.jsonand asks the CLI for the schema:A source that names a registry pins the CLI to it. The two commands disagreed about whether the source carried a host:
provider addpasses noconstraintsToGenerate, sogetderives constraints fromcdktf.json— the shorthashicorp/random. Bare → OpenTofu uses its own registry.provider upgradepassedconstraintsToUpdate.map((c) => new TerraformProviderConstraint(c)), built fromProviderConstraint.fromConfigEntry(provider)with no registry argument, so normalized toregistry.terraform.io/hashicorp/random.Hence one key per host in the same file, old one first, winning any lookup by provider name:
{ "registry.opentofu.org/hashicorp/random": "3.1.3", "registry.terraform.io/hashicorp/random": "3.2.0" }This also explains the
!explicitHostname ||guard inprovider-generator.ts: bare sources are the designed contract, and the upgrade path was violating it.Three fixes
handlers.ts— passnamespace/name, leaving resolution to the CLI. I checked all tennew TerraformProviderConstraint(...)sites; this was the only one handing on a normalized FQPN.constructs-maker.ts—updateVersionsFileno longer carries a previous entry forward for a provider regenerated this run. It matched the old entry by provider part but re-inserted it under its old fully qualified key. Defence in depth now that the host above is stable.local-provider-versions.ts— a third hardcodedregistry.terraform.io/strip, found by tracingDEFAULT_HOSTNAME. This one is a reader: under OpenTofu,versionForProvider("random")returned nothing, so version detection andprovider listcame up empty. Now strips any public registry host.Plus the restored OpenTofu pin, putting the matrix back to three tofu rows.
Verification
Run against both CLIs locally, each leaving a single correctly-keyed entry:
.gen/versions.json{ "registry.terraform.io/hashicorp/random": "3.2.0" }{ "registry.opentofu.org/hashicorp/random": "3.2.0" }typescript/provider-upgrade-commandgoes from1 failed, 3 passed, 1 skippedto4 passed, 1 skippedunder OpenTofu, and still passes under Terraform — that path matters because this changes the source for every upgrade, not just OpenTofu ones. Of the four newLocalProviderVersionstests, the two OpenTofu cases fail against the previous implementation.@cdktn/cli-core20 suites / 207 tests and@cdktn/provider-generator22 tests / 10 snapshots unchanged.Also folded in: deploy after a provider bump, and coverage for OpenTofu-targeting projects
Fixes #488 and closes #494 as well. Four more commits, from auditing what remained once the above was fixed.
cdktn deployfailed for every OpenTofu project after a provider version bump:CdktfStackcompares the synthesizedrequired_providersagainst.terraform.lock.hclto decide whether to runinitand whetherinit -upgradeis needed, using exact fully-qualified addresses — and it normalized every source toregistry.terraform.io. Synthesized sources are bare in practice: generated bindings copy the source as written in cdktf.json, and published prebuilt packages ship bare too (checked@cdktn/provider-aws25.7.2). OpenTofu locks a bare source underregistry.opentofu.org, so both checks missed:initran on every deploy, and a bump never triggeredinit -upgrade, which the plaininitthen rejected.lockAddressesFornow offers every public registry for a bare source, while an explicit host still has to match exactly. Projects that fully qualify providers were never affected.typescript/opentofu-targetis the first fixture that declares an OpenTofu target. The other OpenTofu matrix rows only change which CLI runs, so nothing exercised registry resolution that followstargetVersions— which is where this whole run of defects lived. It walks get → synth → deploy →provider list→provider upgrade→ redeploy, pinned totofu1.12.6. Reverting the lock fix makes its bump step fail with the error above.Two verification-tooling fixes, both found because they produced misleading local results:
run-against-dist.mjsinstalls the CLI from its tarball instead ofpnpm add cdktn-cli, which could resolve an earlier build of the same0.0.0version (tests: run-against-dist can install a stale CLI locally, so integration runs silently test old code #488). The glob iscdktn-cli-[0-9]*.tgz, so release builds with stamped versions still work andcdktn-cli-core-*.tgzis excluded.package:js|python|java|dotnet|gonow depend onbuild. They had no target defaults, so they repacked whatever compiled output was lying around — a fresh tarball timestamp over stale contents. No CI workflow invokes them.Matrix +1 job.
@cdktn/cli-corepasses; the fixture and the Terraformdiff-deploy-output-destroyfixture both pass locally against tarballs extracted and checked for these changes.Now also carries #479, plus review fixes
#479 is folded in and closed. On its own it regressed
provider upgradein OpenTofu-only projects: it movedCdktfConfigManageronto the project's registry while the upgrade handler still normalized against Terraform, so an installed provider looked missing. The handler fix and the regression test both live here, so the two now land together.Fixes from review:
terraform versionbefore every init. That was reverted (b668906).initalways runs, so nothing was ever skipped, and OpenTofu migrates a Terraform lock entry on its own. The filter broke the realistic case: after deploying with Terraform, a provider bump under OpenTofu got no-upgradeandtofu initfailed, as reproduced with Terraform 1.14.3 and OpenTofu 1.12.6. It also leakedterraform versionoutput into every diff/deploy/destroy log. A bare source now matches its lock entry under either public registry; an explicit host still matches only itself. The decisions stay inneedsLockfileUpdate/needsUpgrade, and the tests cover the Terraform→OpenTofu bump.provider upgradekeeps explicitly named hosts. Schema generation had dropped every host.ProviderConstraint.requiredProvidersSourcekeeps a public or private host the user wrote and drops only one added by normalization. Writing the upgraded entry back to cdktf.json had the same problem, found by sakul-learning's scenario runner (368efb3): an update now keeps the entry's own spelling of its source and changes only the version.Each new test was mutation-checked against the reviewed behaviour. Locally, against tarballs checked for these changes:
opentofu-targetpasses 6/6 under tofu 1.12.6,diff-deploy-output-destroypasses 5/5 under Terraform, and cli-core passes with lint clean.Worth knowing about the verification
Two earlier "verification" runs of this fix were invalid, and both looked like real failures:
nx run-many -t packagereported success while serving a two-week-old cachedcdktn-clitgz.--skip-nx-cache,run-against-dist.mjsstill installed an older CLI, because it doespnpm add cdktn-cliand the version is permanently0.0.0.Filed as #488 with a fix. Anyone reproducing this locally should confirm the installed CLI contains their change — and grep the shipped
*.js.map, since esbuild strips comments and minifies the bundle.Release notes
targetVersionsdeclares only OpenTofu,provider add/provider upgradewrite providers tocdktf.jsonin the short form (hashicorp/random) rather than withregistry.terraform.io/. An existing entry keeps the host it was written with.initmatch a bare provider source against a lock entry from either public registry. A provider bump after switching between Terraform and OpenTofu now runsinit -upgrade.registry.terraform.ioexplicitly fails to deploy under OpenTofu.tofu providers lockadds a second, hash-lessregistry.opentofu.orgentry, andapplythen fails withthere is no package for registry.opentofu.org/... cached in .terraform/providers. Plain HCL fails the same way (tofu init,tofu providers lock -platform=linux_amd64,tofu apply); OpenTofu 1.8.2 and 1.9.3 work. Workaround: use the barehashicorp/xsource, or runtofu initagain in the stack directory.🤖 Generated with Claude Code