terradart_migrate 0.35.0 copy "terradart_migrate: ^0.35.0" to clipboard
terradart_migrate: ^0.35.0 copied to clipboard

HCL → Dart migrator library behind terradart migrate. Turns Terraform modules into TerraDart Stacks via the curated catalogs.

Changelog #

0.35.0 - 2026-10-03 #

  • No API changes. Lockstep release with the terradart command's --json output, fixed exit codes, --no-input, --dry-run, terradart help <topic> and bundled agent skill.

0.34.0 - 2026-10-03 #

  • migrateTree writes a terradart: section with engine: terraform into the generated pubspec.yaml — engine: tofu when a module directory holds a .tofu file or a .terraform.lock.hcl naming registry.opentofu.org — so the terradart command keeps the existing state on the engine that wrote it (sourceEngine, renderPubspec(engine:)).

0.33.0 - 2026-10-02 #

  • bin/infra.dart for one Stack calls runStack. --merge-envs calls runEnvironments over the generated Env enum (dir: (env) => 'tf-out/${env.path}'), so terradart plan --env <name> runs that environment. Several merged groups write every environment when --env is omitted, and call runEnvironments for the group a name selects.
  • The user command is terradart migrate (terradart_cli). The terradart-migrate executable still runs the same flags and prints that it is deprecated.

0.32.1 - 2026-10-02 #

  • No API changes. Republishes the 0.32.0 workspace so terradart_appwrite, terradart_cloudflare, terradart_aws and terradart_migrate reach pub.dev; the 0.32.0 publish workflow stopped them at a wrapper-count check that also counted hand-written files, and now counts only generated wrappers (#877).

0.32.0 - 2026-10-02 #

  • A migrated attribute reference is the plain getter (labels: other.labels, addOutput('id', topic.id)), and an argument the manifest types RefTo<R> — Magic Modules ResourceRef inputs, AWS IAM policy ARNs, Cloudflare user group members — is written x.ref.
  • A migrated IAM adjunct passes its parent as one reference (service: api.ref) and leaves out the location / project / region / zone the HCL reads off that same parent. An IAM grant is member: sa.principal or a constructor of its kind (.user('a@example.com')); the manifest records these slots as principal.
  • Appwrite permissions are written as AppwritePermissions (.read(.any)), with .literal('...') for a string the roles do not spell.
  • An output whose value is an object mapping each output's environment variable to that output ({ API_URL = google_cloud_run_v2_service.api.uri, ... }, a non-String one under jsonencode) becomes addDartDefineOutput(name: ..., only: [...]), so a Stack that declares a define file round-trips. Any other object output stays addOutput.
  • A child module that selects a provider alias (provider = google.eu) with no provider block of its own migrates the resource and registers the alias with addConfigurationAlias, so synth emits configuration_aliases. A provider block inside the child still stays in Terraform, with any resource that selects it. A child that passes an alias it does not configure down to a nested module (providers = { google = google.eu }) declares that alias the same way.
  • user_project_override migrates as userProjectOverride on GoogleProvider and GoogleBetaProvider. Any other non-credential provider argument the class does not model keeps that whole configuration in the sidecar; the Stack registers it with addExternalProvider and emits no second provider block. Credentials stay dropped, including a nested block of that name (assume_role_with_web_identity, Google external_credentials) and Appwrite api_key / organization_api_key.
  • A migrated backend and required_version are Stack constructor arguments (super(requiredVersion: ...)), a timeouts block is const TfTimeouts(create: Duration(minutes: 30)) (a duration finer than a microsecond keeps the block in Terraform), and a variant of an empty block is written without an argument (.avroFormat()).
  • A migrated Stack imports only its provider barrels, which re-export terradart_core, and GoogleProject data sources become DataGoogleProject. Reads hasTemplateSequence / templateVariableNames from package:terradart_core/internal.dart. See MIGRATING.md.
  • A provider configuration a block selects is registered with addProvider (final googleEuProvider = addProvider(const GoogleProvider(alias: 'eu', ...))) and passed as provider: googleEuProvider, in a module call's providers map too; unselected configurations stay in super(providers: [...]).
  • A migrated lifecycle is .new(...): ignore_changes = all is ignoreChanges: .all, a list is .of([...]), a whole-resource replace_triggered_by entry is the Dart variable, and precondition / postcondition blocks are conditions: [.pre(...), .post(...)].
  • A migrated variable block is final x = variable<T>('x', ...): T follows its type (string → String, number → num, list(string) → List<String>, map(...) → Map<String, ...>), and what Dart cannot say is spelled out (type: .set(.string), .object({...}), .any). An argument that takes a TfArg<T> reads the handle (location: region); an enum or RefTo slot takes .arg(region). An undeclared reference is externalVariable('x'), and a type the migrator cannot read keeps the variable in the sidecar.
  • A migrated enum value is the member itself (routingMode: .regional, [.http, .https]); a reference or an expression in an enum slot is .arg(...) / .expression(...).
  • Migrated factories and module calls pass the local name positionally (GooglePubsubTopic('orders', ...)), and a generated module wrapper takes super.localName first.
  • A migrated data source is registered with add(...).
  • A migrated depends_on lists the Dart objects (dependsOn: [schema, api]).
  • A kept block the migrated Stack still reads is declared with addExternalBlock('<address>'), which synth now requires for a reference to a block the Stack does not hold.
  • Migrated strings are plain '...'; only a string holding $ or \ stays raw (r'${google_x.y.id}').
  • A generated module wrapper forwards ModuleCall's parameters as super parameters (required super.localName, required super.source, super.version, ...).
  • A merged Stack declares a block only some environments have as a nullable local (final backups = env.isProd ? add(...) : null;), and a block that reads it tests it in its guard (if (env.isProd && backups != null)), instead of late final plus an assignment. Env.byName is values.asNameMap()[name].
  • A migrated Stack builds a helper nested inside another helper or inside a sealed variant with .new(...) (.pushConfig(.new(pushEndpoint: ...))); a resource's own block arguments keep the class name.

0.31.0 - 2026-10-01 #

  • A reference to an input attribute (${google_pubsub_topic.x.labels}) becomes its <name>Ref getter (.ref(x.labelsRef)) instead of TfRef.attribute<Object?>(x, r'labels'); the fallback remains for an attribute the wrapper has no getter for. The five migration manifests list the new getters.
  • A translated output block becomes addOutput(name, .ref(x.getter), description: ..., sensitive: ...) under its own name — any getter type, with TfRef.attribute<Object?> when there is none — instead of addExport + ResourceIdExport (whose Dart-identifier key needed terraformOutputName), and the Stack no longer calls setAppExportsOutputPath.
  • A migrated Stack uses Dart 3.10 dot shorthands wherever the argument has a static type: .literal(...), .variable(...), .expression(...), .ref(...), .workspace(), RefTo's .literal(...) / .variable(...) / .arg(...), and an enum's .member (inside .literal(...), in a bare List<E>, and on an Env field). A bare ModuleCall's Object?-valued inputs map keeps the long form (TfArg.literal(...)).
  • A reference slot migrates a reference to a migrated block of the slot's type (or a data source reading that type) to x.ref, pinned (x.ref.pinned('id')) when it reads another attribute than the slot emits; a block of another type to an unchecked RefTo.arg(...) with a warning; a string to RefTo.literal, a variable to RefTo.variable, other expressions to RefTo.expression. A list slot takes a literal list element by element and a whole-list value as TfArg.variable / TfArg.expression. MigrateSlotKind.reference and MigrateSlot.attribute are new.
  • MigrateHelper.shorthand: a sealed variant whose sealed type declares a factory constructor for it migrates to the dot shorthand .member(value) (code: .filename(.literal(r'f.zip')), replication: .auto()) instead of naming the variant class.
  • A migrated package's generated pubspec.yaml declares sdk: ^3.10.0 (was ^3.6.0), the minimum of the terradart_* packages it depends on.
  • An optional merged sealed slot (the nullable at-most-one sealed arguments terradart wrap derives) migrates to nothing when none of its keys is set, to the variant of the one that is set, and keeps a block that sets more than one in Terraform.
  • pub.dev: add example/main.dart (migrateModule on an inline module), dartdoc on every public member, and a pubspec.yaml description short enough for the pub.dev score (180 characters at most). No API changes.

0.30.0 - 2026-09-28 #

  • MigrateSlot.keyed: a helper slot typed Map<String, Helper> (a nesting_mode: "map" block) migrates from an object of blocks, one helper per key. A * segment in a sensitive path matches every key.
  • Fix: a list-of-enum argument set to a reference to a whole list (selected_regions = var.regions) migrated to a TfArg.variable / TfArg.ref the constructor's List<...> parameter does not accept, so the Stack did not compile. It now stays in Terraform with the reason; a list literal still migrates one member per element.
  • MigrateManifest.caseInsensitiveEnums: for a provider whose validators accept enum values in any case, a raw value that differs from exactly one member only in case ("AVRO" written "avro") migrates to that member instead of staying in Terraform, and the report warns that it synthesizes in the member's canonical case. The cloudflare manifest sets it, next to the lane's 579 new enums.
  • --report runs the migration in memory and prints, per resource / data type, how many blocks translate and how many stay in Terraform (each with its reason), the factory each type maps to and the module calls whose source is outside the tree; --json prints it as JSON. It writes nothing. MigrationCoverage.of(project) is the library side. It replaces terradart-coverage.
  • Removed --update, --in-place, --allow-todo and --inline-locals (each now exits 64), with rerunProject, planInPlace / writeInPlace and the allowTodo / inlineLocals parameters of migrateModule / migrateTree. Everything that stays in Terraform lands in the sidecar, so MigrationResult.sidecar and MigratedModule.sidecar are never null; the report JSON drops allowTodo, planDiffers and todos.
  • Published on pub.dev: dart pub global activate terradart_migrate installs terradart-migrate. The release binaries and the Homebrew formula are gone, and so are release-binary.yml, tool/render_formula.dart and tool/render_to_file.dart.

0.29.0 - 2026-09-27 #

  • aws_* resources and data sources migrate to terradart_aws through its generated manifest (lib/src/manifest/aws.g.dart). The provider "aws" block translates to AwsProvider, nested settings included (default_tags, assume_role, ignore_tags, endpoints); credential arguments (access_key, secret_key, token, assume_role_with_web_identity) are dropped with a warning, never written into Dart.
  • time_sleep migrates to terradart_time (package:terradart_time/terradart_time.dart, a terradart_time dependency in the migrated pubspec) instead of terradart_google, so a migrated AWS or Cloudflare module no longer depends on the Google package. googleExtrasMigrateManifest is now timeMigrateManifest (package terradart_time), and the time provider recipe reads the pin from kTimeProviderVersionConstraint instead of repeating it.

0.28.1 - 2026-09-13 #

  • Fix: a passthrough slot whose parameter is a bare Map / List rather than a TfArg<Map<...>> — advancedExtra on SqlDatabaseInstanceSettings, the raw-map escape hatch a hand-written helper spreads into its block — was emitted as TfArg.literal({...}), so a migrated Stack that carried an insights_config did not compile while the report counted the resource as migrated. The emitter now honours the manifest's wrapped flag (the manifests already recorded it), shapes the payload to the parameter (a block written once reads as one object: a List<...> parameter gets a one-element list, a Map<...> parameter takes the single element of a one-object list) and types empty payloads from the manifest. The round-trip gate had never exercised a passthrough slot because no quickstart used one; cloud_sql_quickstart now sets insights_config through advancedExtra, so it does.

0.28.0 - 2026-09-13 #

  • --inline-locals declares the locals entries whose value is a scalar literal — or a template made only of literal text and locals that are themselves inlined, resolved as a fixpoint — as Dart finals in the Stack (final prefix = r'acme'; final bucketName = '$prefix-assets';), and rebuilds locals.tf so it keeps only the entries something that stays in Terraform still reads (a kept block, a terraform setting, another local, or a ${local.x} the Stack still emits as an expression); a local nothing in the Stack reads is never declared. A list, an object, a reference, a %{ ... } directive, or a name two locals blocks both declare stays, with its reason. Names come from the Stack's allocator after the blocks, types are carried (a bare ${local.port} only fills a slot that takes a number), and --lift-workspace now shares the same dartTemplate rewrite. Refused together with --merge-envs (#672).
  • Comment carry-over: the #, // and /* */ comments above a resource, data or module block are written as // lines above the add(...) it became; a count / for_each block documents its unrolled instances once, above the first. Not carried: the migrator's own # terradart-migrate: annotations, and the bodies of a merged Stack (--merge-envs compares code, not prose) — which is why the comments are a field of StackStatement rather than part of its text (#672).
  • --in-place rewrites the tree under --dir, after the package is written, so its .tf files keep only the blocks that stayed in Terraform: source ranges are cut rather than re-rendered, so every surviving line keeps its own bytes and git diff is the review of the migration; a file whose every block became Dart is deleted, and terraform { } / locals blocks are split entry by entry the way the sidecar splits them. It refuses unless --dir is inside a git working tree with nothing uncommitted (untracked files included), checked before anything — --out included — is written. Never touched, and listed in the report: *.tf.json files, moved blocks, and anything reached through a symbolic link. writeInPlace enforces the same path allowlist as writeRerun (#672).
  • tool/migrate_fixture_gates.dart gains the inlined-locals gate (#672): the inline_locals fixture is migrated with and without the flag, both packages are planned, and the plans must match resource for resource.
  • --update <package dir> re-runs the migrator over a package it already generated, for the day a catalog wave covers something that stayed in Terraform. It reads each Terraform directory's sidecar — every .tf / .tf.json there except the main.tf.json a Stack writes — migrates it against today's catalog, and writes what translates now as lib/<stack>.snippets.dart: an extension on Stack whose method body is exactly the statements to paste into the constructor, so the file compiles where it sits and the paste is mechanical. Beside it goes terradart_leftover.next.tf, the sidecar as it looks once they are pasted, and RERUN.md with the swap steps per directory. Resources, data sources, module calls and their moved entries are pasted; a provider configuration, a variable, an output and the terraform settings are the Stack's own structure, so they stay in the sidecar instead of vanishing from it, and a reason comment is not stacked again on every re-run. Hidden directories are never entered, so the modules terraform init leaves under .terraform/ are neither read nor written into. Nothing else is written: the writer refuses any path that is not one of those three, so no Dart of yours is ever overwritten. A local the Stack already declares is reserved, so a pasted one never shadows it; a reference to a block the Stack owns stays a Terraform expression, since the re-run reads the sidecar, not your Dart (#669).
  • tool/migrate_fixture_gates.dart gains the re-run gate (#669): config_tree/ is migrated against a catalog with google_storage_bucket removed — the world before the wave that added it — then re-run against the full catalog. The five sidecar blocks must come back as snippets, the sidecar must shrink, every file the first run wrote must be byte-identical afterwards, and the Stack with the snippet pasted (the extension called on it) must synthesize exactly what a one-shot migration of the whole tree does. Re-running a fully migrated package must find nothing, which is also what proves the re-run never reads the main.tf.json a Stack writes.
  • --merge-envs folds each group of sibling environment roots into one Stack taking a generated Env enum: AppStack({required Env env}) in lib/<group>_stack.dart next to lib/env.dart, one member per root (Env.dev, Env.prod) carrying its path (tf-out/<path>) and every value the roots disagree on. A top-level argument two roots write differently — a resource argument, a module call input, a variable default or description, a provider argument, a backend argument — becomes a constant on the enum (name: TfArg.literal(env.assetsName), GcsBackend(bucket: env.backendBucket)), typed as the argument takes it: an enum-valued argument becomes a typed member (storageClass: TfArg.literal(env.assetsStorageClass), BucketStorageClass.nearline), and lib/env.dart imports the barrel that exports it. A block only some roots declare sits behind a flag (if (env.isProd) { ... }), and a guarded local another guarded block reads is declared ahead of its if (late final GoogleStorageBucket backups;). bin/infra.dart writes every environment, or the ones --env <name> picks — a name no group declares is a usage error, one only some groups declare selects those. A group that cannot be merged keeps one Stack per root and says why in MIGRATION.md: a value that differs but is not a plain scalar (a nested block, a list, a reference, an interpolated string), a sensitive variable whose default differs (never copied into Dart), different providers or backends, blocks declared in a different order, a root where nothing translates. The merged-environment gate in tool/migrate_fixture_gates.dart proves the merged Stack synthesizes, per environment, exactly the JSON one Stack each did (#668).
  • --lift-workspace turns terraform.workspace into a workspace parameter on the Stack, so dart run bin/infra.dart --workspace <name> synthesizes for one workspace by name instead of leaving ${terraform.workspace} for terraform workspace select: a bare reference becomes TfArg.literal(workspace), a template around it becomes Dart interpolation (TfArg.literal('orders-$workspace')), and one inside a list or map becomes the value. A template mixing it with another reference is left as a Terraform expression, with a warning naming it. Off by default — the synthesized JSON then names a workspace rather than deferring to Terraform (#668).
  • MigratedProject.merged (merged in --json, an Environments section in MIGRATION.md) lists every environment group with its constants, flags and per-environment values — or the reason its roots stayed one Stack each.
  • timeouts { ... } becomes timeouts: const TfTimeouts(create: '30m', ...) on the factory instead of keeping the block in Terraform; a bare terraform.workspace becomes TfArg.workspace<T>() (inside a larger template it stays a verbatim expression); and a partial backend — backend "gcs" {}, or an s3 block without bucket / key — becomes the typed backend with those parameters left out, for terraform init -backend-config. Still blockers, with a reason: a timeouts key that is not a Terraform operation, a value that is not a duration string (a reference included — Terraform forbids one there), and a timeouts block that sets nothing (#671).
  • module blocks become addModule(...) instead of staying in the sidecar. A call whose source points at a directory in the scanned tree uses a generated typed wrapper — one lib/<module>_module.dart per local module directory, a ModuleCall subclass with a named TfArg parameter per variable block (string / number / bool typed, everything else Object?; required when the variable has no default) and a TfRef<String> getter per output (CloudRunModule(localName: 'cloud_run_bff', source: '../modules/cloud_run', name: TfArg.literal('app-bff')), bff.serviceName). A module that declares neither a variable nor an output has nothing to type, so its calls keep the bare ModuleCall, as do registry, git and out-of-tree sources — source and version travel verbatim and the inputs as an untyped inputs map. module.x.out references resolve to the call's Dart local wherever a resource attribute would (typed arguments, collections, depends_on, lifecycle, and single-attribute outputs, which now become exports), and the call is ordered with the resources it passes values to and reads them from. Blockers, with a reason: a count / for_each on the call (its instances are addressed module.x[0]), a non-literal source, no source, an input the module does not declare, a providers = { ... } value naming an alias the module does not configure (or any alias inside a child module), and a directory that translates nothing else and registers no provider — a root that only calls modules is the exception, and gets a Stack with providers: [] (#665).
  • A literal count / for_each is unrolled into one resource (or data source) per instance instead of keeping the block in Terraform: google_pubsub_topic.t[0] becomes google_pubsub_topic.t_0, google_pubsub_topic.t["eu"] becomes google_pubsub_topic.t_eu (keys sanitized to Terraform names); count.index, each.key and each.value are substituted in the instance bodies — in parsed expressions and in the text of raw ones — and every reference in the module is pointed at the new addresses: t[1].name → t_1.name, t[*].id → [t_0, t_1][*].id, a bare t → the tuple (count) or object (for_each) of instances, depends_on / replace_triggered_by entries spread per instance; blocks that stay in Terraform get the same rewrite in the sidecar. A moved entry per resource instance (addMoved) carries the state, so terraform plan shows moves only. Translation stays resource-atomic: one instance that cannot become Dart keeps the whole block as written, with the instance's reason. The module's own moved blocks become addMoved when their target is migrated (a target that was unrolled yields one move per instance); those whose target stays in Terraform stay with it. Still blockers: a count / for_each that is not a literal (a variable, a conditional, a tuple), one declaring no instance, an instance name colliding with another block, a reference to an instance the block does not declare (#663).
  • MigrationReport.expanded (ExpandedItem / ExpandedInstanceItem, expanded in --json) lists every unrolled block with its instances; renderText and MIGRATION.md show them under "Unrolled".
  • tool/migrate_moved_gates.dart (agent_verify.sh full mode, CI migrate moved gate): migrates test/fixtures/moved_state/, synthesizes it, and plans against the fixture's state of indexed instances (state.json, copied in as terraform.tfstate) — the plan must be moves only (#663).
  • Provider aliases are translated instead of keeping a resource in Terraform: every provider "<name>" { alias = "x" ... } block becomes a second <Name>Provider(alias: 'x', ...) registration on the Stack, provider = <name>.x on a resource or data source becomes provider: '<name>.x' on its factory, and provider = google-beta on a GA type registers GoogleBetaProvider and selects it. Still blockers, with a reason: provider = <name>.x when the module has no such alias block, and an alias inside a child module (which needs configuration_aliases) (#666).
  • Expressions the emitter cannot type — templates, function calls, conditionals, local.x, module.x.y, references to kept blocks — become TfArg.expression on every TfArg-typed argument (number, bool, enum, list and sensitive arguments included) instead of TfArg.literal(r'${...}') on string arguments only; the var.<name> references inside them are declared like plain references. What still keeps a block in Terraform: an expression inside a typed Dart collection and on a bare (non-TfArg) parameter (#662).
  • terradart-migrate CLI (bin/terradart_migrate.dart: --dir <tree> --out <package>): scans a Terraform source tree, infers roots, children and environment siblings (--roots, --env-dirs override), and writes one Dart package — a Stack per module directory, a tf-out/ tree mirroring the source so module sources keep resolving, the leftover sidecar beside each main.tf.json (terradart_leftover.tf plus backend.tf / variables.tf / locals.tf / outputs.tf, verbatim with a reason each), copies of terraform.tfvars, *.auto.tfvars and .terraform.lock.hcl, and MIGRATION.md. Child-module mode for referenced directories (bare providers, addVariable / exports, provider configurations and backends kept); a directory where nothing translates gets no Stack and stays Terraform. --json, --allow-todo, --force; never writes into --dir; sysexits exit codes (#661).
  • Library: scanModuleTree, migrateTree, migrateStack, buildSidecar, MigrationReport.providers; migrateModule gains childModule / allowTodo and returns the sidecar files under tf-out/ (#661).
  • tool/migrate_fixture_gates.dart (agent_verify.sh full mode, CI migrate fixture gate): migrates the coverage fixtures, synthesizes the package and terraform-validates every directory; test/golden/ pins their output (#661).
  • Distribution: release-binary.yml builds terradart-migrate (macOS arm64 / amd64, Linux amd64, Windows amd64) on every v* tag and tool/render_to_file.dart renders its Homebrew formula into nozomi-koborinai/homebrew-tap — brew install nozomi-koborinai/tap/terradart-migrate; website guide Migrating from HCL, README install sections (#664).
  • migrateModule: a TfModule (tf.json or HCL via terradart_hcl) in, a Dart package out — lib/<name>_stack.dart, bin/infra.dart, pubspec.yaml — plus a MigrationReport of what became Dart and what stays in Terraform with a reason. Resource-atomic translation following the migration manifests: literals, enums, nested helpers (single / repeated / exactly-one-of), passthrough maps, typed references and variables, verbatim expressions on string arguments, depends_on, lifecycle, backends, providers (the four packages plus time), variables and single-attribute outputs (#660).
  • tool/migrate_roundtrip_gates.dart and tool/migrate_roundtrip_debt.yaml: the round-trip gate (synth(migrate(synth(S))) == synth(S) over every quickstart), wired into agent_verify.sh full mode and CI (#660).
  • A hand-written googleExtrasMigrateManifest (time_sleep → TimeSleep) rides along with the generated manifests in allMigrateManifests.
  • New package (publish_to: none): the migration manifests of the four curated catalogs (googleMigrateManifest, googleBetaMigrateManifest, appwriteMigrateManifest, cloudflareMigrateManifest), generated by terradart wrap --migrate-manifest into lib/src/manifest/, with the hand-written runtime types (MigrateManifest, MigrateEntry, MigrateSlot, MigrateHelper, MigrateEnum, MigrateGetter) moved here from the provider packages, plus allMigrateManifests, manifestForPackage and findMigrateEntry (#658).
1
likes
160
points
277
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

HCL → Dart migrator library behind terradart migrate. Turns Terraform modules into TerraDart Stacks via the curated catalogs.

Repository (GitHub)
View/report issues
Contributing

License

Apache-2.0 (license)

Dependencies

args, dart_style, path, terradart_appwrite, terradart_aws, terradart_cloudflare, terradart_core, terradart_google, terradart_google_beta, terradart_hcl, terradart_time

More

Packages that depend on terradart_migrate