🐛 Bug Fixes
Fixed Unsupported attribute errors for values.* inputs that autoinclude overrides
A unit input referencing a values.* key that the unit's values file doesn't define no longer fails with Unsupported attribute when an autoinclude block supplies that input. The autoinclude value is applied as intended.
# stacks/terragrunt.stack.hcl
unit "subnet" {
source = "../units/subnet"
path = "subnet"
autoinclude {
dependency "vpc" {
config_path = unit.vpc.path
mock_outputs = { vpc_id = "mock" }
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
}
values = {
cidr_block = "10.0.0.0/24"
}
}# units/subnet/terragrunt.hcl
inputs = {
vpc_id = values.vpc_id # supplied by autoinclude, not the values file
cidr_block = values.cidr_block # still resolves from values file
}Fixed overwrite_terragrunt and remove_terragrunt on files with no trailing newline
generate blocks using if_exists = "overwrite_terragrunt" or if_disabled = "remove_terragrunt" failed to properly handle existing files when the file at the target path had no newline after its first line, empty files included.
Terragrunt now properly handles files like this, so a file carrying the Terragrunt signature is overwritten or removed as configured, and a file without it produces the usual error naming the path Terragrunt would not touch.
Dependency mock_outputs apply when the state bucket doesn't exist yet
When reading a dependency's outputs directly from remote state (--dependency-fetch-output-from-state), Terragrunt fell back to mock_outputs only when the state object was missing, not when the S3 bucket itself didn't exist. A dependency on an environment that hadn't been bootstrapped yet would fail instead of using its mocks.
A missing bucket is now treated the same as a missing state object, so commands like plan and validate can resolve mocks before the dependency's backend has been created.
Source permissions preserved on hidden directories copied by include_in_copy
With the fast-copy strict control enabled, a hidden directory that Terragrunt copied due to include_in_copy matching something within it took the permissions of the first file generated within it, instead of the permissions it had in the source.
Those directories now keep their source permissions, matching the copy Terragrunt performs with the control disabled.
Applied the positive half of a filter that begins with a negation
When a --filter query began with a negation, Terragrunt treated the whole query as an exclusion. The expressions chained after the negation stopped restricting the selection and only narrowed what got subtracted, so components matching none of them came back in the results. Those expressions are now applied.
$ terragrunt list
bar baz foo$ terragrunt list --filter '!name=foo | name=bar'
bar baz foo$ terragrunt list --filter '!name=foo | name=bar'
barThis follows the left-to-right refinement that | has everywhere else: each expression narrows what the one before it selected. A query is only treated as an exclusion when every one of its expressions is negated, such as '!name=foo' or '!name=foo | !name=bar'.
See Combining Expressions for how negation, intersection and union interact.
Fixed a race condition that left cached provider archives in the working directory
With the provider cache server enabled via --provider-cache, a race let the server start responding to requests before it had finished preparing the directories it caches into. A provider requested in that window had its archive and lock file written relative to the working directory instead of into the cache, leaving zip files behind in your project.
That race condition has been fixed. Providers now always download into the cache directory.
Fixed a race condition between concurrent Terragrunt runs downloading providers
A race condition in the logic used to synchronize provider downloads meant that two Terragrunt runs on the same machine could interfere with each other while caching the same provider. Each run staged its downloads at the same path, so a run that finished first could delete an archive another run was still unpacking, failing that run with failed to open zip archive.
That race condition is now fixed. Two runs can cache the same provider at the same time.
Fixed space-delimited flag values in providers lock
The space-delimited form, providers lock -platform linux_amd64, now reaches OpenTofu and Terraform intact. Previously it was the attached form, -platform=linux_amd64, that worked: given the value as a separate argument, Terragrunt moved it to the end of the command, where it was read as a provider address and the run failed with Invalid provider type "linux_amd64".
-fs-mirror and -net-mirror were moved the same way, and now keep their values too.
With --provider-cache enabled, platforms are also split correctly across the per-platform providers lock runs used to warm the cache.
Fixed scaffold on units and stacks
terragrunt scaffold read every source as an OpenTofu/Terraform module. Given a unit or a stack, which are Terragrunt configurations rather than OpenTofu/Terraform modules, it exited successfully having written an invalid terragrunt.hcl file.
Units and stacks are now scaffolded the way the Catalog TUI scaffolds them: their files are copied into the working directory for you to edit in place, along with a terragrunt.values.hcl listing every values.* reference the configuration makes.
terragrunt scaffold 'github.com/gruntwork-io/terragrunt-scale-catalog//units/aws/oidc/iam-oidc-role'Copying refuses to overwrite: a file that would land on an existing path stops the command before anything is written. Modules and templates are unaffected and are still scaffolded from their variables.
See Scaffold for what gets copied and how the values file is filled in.
Answered every prompt when input is piped in
A run that asks for confirmation more than once, such as terragrunt backend delete prompting for both the lock table entry and the state object, used to read only the first answer when the answers were piped in rather than typed. The remaining answers were discarded while reading ahead, and the next prompt failed with an end-of-input error. Every prompt in a run now reads from the same input, so piping yes for each one works.
Stack dependencies honor mock_outputs with --dependency-fetch-output-from-state
A dependency block that reads outputs from a stack (its config_path points at a terragrunt.stack.hcl directory) used to fail when a unit in that stack had no state yet, even when the dependency declared mock_outputs. This blocked commands like plan and validate against a stack that hadn't been applied.
Such a dependency now falls back to mock_outputs for the units that have no state yet. In a partially applied stack, applied units resolve to their real outputs while the rest use their mocks.
Mocks for a stack dependency are keyed by unit name, so mock_outputs has to be a map or object. Declaring it as any other type now reports that directly, instead of leaving the units it can't cover out of the stack outputs.
Fixed --config= being ignored by the tflint hook
The built-in tflint hook reads the configuration file out of the arguments you give it, then uses that path for tflint init and for the lint run. It only recognized the space-separated --config <path> spelling, so a hook written as:
before_hook "tflint" {
commands = ["plan"]
execute = ["tflint", "--config=custom.tflint.hcl"]
}was treated as though no configuration file had been named at all. Terragrunt searched the unit directory and its parents for a .tflint.hcl file instead, and either failed with a config-not-found error or ran tflint init against whatever unrelated configuration the search turned up. Terragrunt now recognizes --config <path>, --config=<path>, -c <path>, and -c=<path>.
The hook also builds --var arguments from the unit's inputs and from TF_VAR_ entries in extra_arguments blocks. Those arguments came out in a different order on every run, which made the logged command line, and anything comparing it between runs, needlessly unstable. They are now ordered by variable name.
🧪 Experiments Added
block-iteration experiment reserves the expansion block
The block-iteration experiment has been added as the gate for iterating a dependency, unit, or stack block over a count or for_each, declared through a nested expansion block, along with an enabled attribute on unit and stack blocks.
In this release the flag is reserved only, and enabling it has no behavioral effect. Writing an expansion block without the experiment now reports an error naming the flag, rather than leaving the block to be silently discarded:
the unit "app" block in /path/to/terragrunt.stack.hcl uses an expansion block, which requires the 'block-iteration' experiment; enable it with --experiment block-iteration
Track progress and share feedback in #4504.
bounded-discovery — Added a directory boundary for graph traversal
Filter expressions that traverse the dependency graph reach beyond the working directory: dependents (--filter '...{unit}') by walking up to the Git repository root, dependencies (--filter '{unit}...') by following declared paths. Either way, Terragrunt reads and parses every configuration it touches. In monorepos with isolated environments, that traversal can fail or do wasted work reading sibling environments.
Enable the new bounded-discovery experiment to set a boundary for that traversal. The --discovery-boundary flag (env: TG_DISCOVERY_BOUNDARY) replaces the Git repository root as the enclosure for a whole run:
cd environments/staging
terragrunt run --all plan --experiment bounded-discovery --filter '...{vpc}' --discovery-boundary .The experiment also unlocks an inline (dir) boundary operand, which bounds a single expression and overrides the flag. It occupies the same slot as a traversal depth, so it bounds discovery by location the way a number bounds it by graph hops:
cd environments/staging
terragrunt run --all plan --experiment bounded-discovery --filter '(.)...{vpc}'Any configuration that resolves outside the boundary, whether a dependent or a dependency, is not read, parsed, or returned: find does not list it and run --all does not run it. Configurations inside the boundary are discovered as usual.
The boundary must be an existing directory, and relative paths are resolved against the working directory. Dependent traversal searches upward from the working directory, so filters that use it also need the boundary to be the working directory or one of its parents. Dependency traversal follows declared paths from the units a filter matched, so dependency-only filters accept any directory, including one below the working directory:
# From the repository root, follow app's dependencies but keep them within prod
terragrunt find --experiment bounded-discovery --filter '{./prod/app}...' --discovery-boundary ./prodReserving ( and ) for the boundary operand changes how --filter reads those characters everywhere, not only when the experiment is enabled. An expression such as --filter '1...(foo | bar)' previously matched a unit literally named (foo or bar); it is now rejected as a malformed boundary. Wrap a name or path containing parentheses in braces (e.g. --filter '{./weird(name)}') to keep it literal.
browse-tui — Added an interactive browser for your estate
The new browse-tui experiment adds the terragrunt browse command. With the experiment enabled, terragrunt browse opens a three-column Terminal User Interface (TUI) browser of your infrastructure estate: the parent directory on the left, the current directory in the middle, and a detail pane on the right showing metadata for the highlighted unit, stack, or directory. The browser opens immediately and fills in metadata as discovery completes in the background.
Enable it with --experiment browse-tui or TG_EXPERIMENT=browse-tui. See the experiment documentation for the keybindings, search, and the criteria for stabilization.
mutable-generate — Deduplicated generate block output
The mutable-generate experiment has been added. With it enabled, the contents a generate block produces are stored in the Content Addressable Store (CAS), and the file written at path is a read-only link to that stored copy rather than a file of its own.
Since the stored copy is addressed by the hash of its contents, anything generating identical contents links to the same copy. A generate block inherited by several hundred units therefore costs one copy in .terragrunt-cache rather than several hundred.
The link is read-only because that copy is shared. Where a generated file does need to be edited in place, a new mutable attribute on the generate block gives it a writable file of its own:
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
mutable = true
contents = "..."
}Setting mutable without the experiment enabled is an error, since earlier Terragrunt versions reject the attribute. The CAS is required, so --no-cas writes generated files directly and mutable has no effect.
For details, see the experiment documentation.
optional-dependency-outputs — Added --no-dependency-outputs flag to skip dependency output resolution
Added a --no-dependency-outputs flag that skips all dependency output resolution globally, mirroring the existing skip_outputs = true attribute on individual dependency blocks.
The feature is gated behind the optional-dependency-outputs experiment:
TG_EXPERIMENT=optional-dependency-outputs terragrunt run --no-dependency-outputs -- initUsing --no-dependency-outputs without enabling the optional-dependency-outputs experiment will return an error.
Thanks to @pjrm for contributing this feature!
🧪 Experiments Updated
catalog-format — Added reading the catalog as JSON Lines
The catalog command draws a terminal user interface, and refuses to start where there is no terminal to draw it on. With the catalog-format experiment enabled, --format=jsonl writes the same discovery to standard output instead, as one JSON object per line:
terragrunt catalog --experiment=catalog-format --format=jsonl | jq -c '{kind, title, component_source}'Entries are written as they are discovered rather than collected first, so output is readable while the remaining repositories are still loading, and a reader that stops early ends the command quietly:
terragrunt catalog --experiment=catalog-format --format=jsonl | head -5Note
Closing the pipe
In this example, the head program exits after reading in five lines, and Terragrunt detects the SIGPIPE signal from the OS, and shuts down cleanly.
Entries appear in discovery order, which interleaves the repositories being loaded and differs between runs. Every entry carries the complete body of the component's README in the doc field. Combine usage of Terragrunt with other tools like jq to drop it.
terragrunt catalog --experiment=catalog-format --format=jsonl | jq -c 'del(.doc)'Entries follow a published JSON schema. For the fields and their meanings, see Non-interactive catalog.
--format=tui is the default, and leaves the terminal user interface exactly as it was.
catalog-format — Added reading the catalog as Markdown
The catalog-format experiment gains a second non-interactive format. Where --format=jsonl writes a record per catalog entry for a program to parse, --format=md writes one Markdown document for a person or an agent to read:
terragrunt catalog --experiment=catalog-format --format=md > catalog.mdEach entry becomes a section holding the metadata the catalog user interface shows for it, the source the component is scaffolded from, and the component's README. Sections are written as entries are discovered, so the document is readable while the remaining repositories are still loading.
READMEs are reproduced inside fenced blocks, so the headings one carries are not read as sections of the catalog document. The document closes with a table naming every component it holds and a count of what was discovered, which is how a reader tells a complete document from one that was cut short by a consumer that stopped reading.
For the fields each section carries, see Non-interactive catalog.
oci — Added OCI sources for stack units and stacks
terragrunt.stack.hcl now accepts oci:// sources in unit and stack blocks, so a stack can pull its components straight from an OCI registry. Without the oci experiment enabled, such a source fails with a clear error instead of an unsupported-scheme failure.
oci — Added OpenTofu CLI-config credentials for OCI module sources
oci:// module downloads now read OpenTofu's CLI-config credentials, so one configuration serves both OpenTofu and Terragrunt.
Terragrunt honors the oci_credentials "<registry>[/<repo-prefix>]" blocks (username and password, OAuth tokens, or a docker_credentials_helper, which like tofu may only be set on a whole registry) and the oci_default_credentials fallback helper. A TF_CLI_CONFIG_FILE or TERRAFORM_CONFIG value selects the config file outright; otherwise Terragrunt reads the first of ~/.tofurc and ~/.terraformrc that exists, and merges the *.tfrc and *.tfrc.json files in OpenTofu's config directory.
Terragrunt picks the most specific matching source across CLI config and ambient Docker config; an explicit CLI-config entry wins when both match equally. Set discover_ambient_credentials = false in the oci_default_credentials block to use CLI config only.
⚙️ Process Updates
Go bumped to v1.26.5
The version of Golang used to compile the Terragrunt binary has been updated from v1.26.0 to v1.26.5.
Thanks to @apoiget for contributing this upgrade!
Pull Requests
✨ Features
- feat: Adding graph boundary via
()syntax by @yhakbar in #6365 - feat: Adding
--discovery-boundaryflag by @yhakbar in #6355 - feat(getter): OpenTofu CLI-config credentials for oci:// sources by @denis256 in #6531
- feat: Adding
browseby @yhakbar in #6219 - feat: Adding
mutableattribute to thegenerateblock by @yhakbar in #6563 - feat: Adding
mdformat forcatalogby @yhakbar in #6608 - feat: Add --skip-dependency-outputs flag to skip dependency output resolution by @pjrm in #6422
🐛 Bug Fixes
- fix(providercache): log -lockfile=readonly skip at debug level by @bryanhorstmann in #6577
- fix: Fixing handling of
EOFingenerateblocks by @yhakbar in #6592 - fix: Fixing the
--config=form of flags used in the tflint hook by @yhakbar in #6591 - fix: Fixing fast-copy ancestor directory permissions by @yhakbar in #6593
- fix: Addressing
providers lock-platformusage with space delimited values by @yhakbar in #6597 - fix: Fixing bug with
negation | positiveexpression in the same query. by @yhakbar in #6598 - fix: Fixing provider cache server archive dir race by @yhakbar in #6620
- fix: Fixing scaffold on units and stacks by @yhakbar in #6607
- fix: Addressing feedback from #6565 and #6605 by @yhakbar in #6628
- fix: autoinclude values override for inputs by @denis256 in #6626
- fix: Fixing
mdformat catalog escaping by @yhakbar in #6638 - fix: Plumbing through
evalCtxfor discovery boundary by @yhakbar in #6632 - fix: Fixing stack dependency mock outputs by @yhakbar in #6530
- fix: Fixing issue where dependency mock outputs aren't used when bootstrapping hasn't run yet. by @yhakbar in #6534
🏎️ Performance
📖 Documentation
- docs: Add call out for terragrunt scale in quick start by @yhakbar in #6583
- docs: document oci module sources, authentication, and caching by @denis256 in #6636
- docs: Cleaning up changelog for
v1.1.3by @yhakbar in #6669 - docs: Cleaning up experiment docs by @yhakbar in #6627
- docs: address review feedback on the oci and autoinclude docs by @denis256 in #6643
✅ Tests
- test(getter): integration tests against a local OCI distribution registry by @denis256 in #6614
- test: prove oci module portability between tofu and terragrunt by @denis256 in #6629
- test(git): add unit coverage for internal/git command wrappers and parsers by @denis256 in #6661
🧹 Chores
- chore: Pin exact provider versions for terralith to terragrunt guide by @yhakbar in #6578
- chore: Using vfs handle for
ParseFromFileby @yhakbar in #6561 - chore: Walk in discovery with vfs by @yhakbar in #6564
- chore: Registring
catalog-formatexperiment by @yhakbar in #6582 - chore(deps): update AWS, Azure, GCP SDKs by @denis256 in #6590
- chore: Continuing clean-up of
go test ./...on a fresh clone of the repo by @yhakbar in #6553 - chore: Fixing usage of deprecated aws sdk by @yhakbar in #6600
- chore: Cleaning up profile tests per feedback in #6553 by @yhakbar in #6599
- chore: Clean-up by @yhakbar in #6584
- chore: Addressing feedback from #6365 and #6355 by @yhakbar in #6603
- chore: Register the
block-iterationexperiment by @yhakbar in #6562 - chore: address review feedback from #6531 by @denis256 in #6609
- chore: Addressing flake in
TestCatalogJSONLFormatCleansUpOnEarlyExitby @yhakbar in #6613 - chore: updated TestDiscovery_GraphConcurrentConfigAccessWithRacing to use VFS by @denis256 in #6622
- chore: Adding expansion detection and internal expansion logic by @yhakbar in #6565
- chore: Adding expansion blocks to the configs that accept expansion by @yhakbar in #6605
- chore: Threading venv through getters and
hcl fmtby @yhakbar in #6621 - chore: Addressing feedback from #6621 by @yhakbar in #6633
- chore: Fixing experiment tag in sidebar by @yhakbar in #6635
- chore: Updating mem exec so that it fails closed by @yhakbar in #6634
- chore: Gate real
hgusage test behind theexecbuild flag by @yhakbar in #6637 - chore: Replacing aws provider with null provider in
init-cachefixture by @yhakbar in #6639 - chore: Cleaning up
NewParsingContextconstructor by passing in venv as a param by @yhakbar in #6630 - chore: Addressing lint finding by @yhakbar in #6649
- chore: Refactoring markdown deps into
internal/mdby @yhakbar in #6640 - chore: Adding unit tests for internal packages by @denis256 in #6660
- chore: Bumping Go to
1.26.5(#6664) by @apoiget in #6666 - chore: Dropping stale tree parse test case by @yhakbar in #6672