Skip to content

Commit e17dccf

Browse files
authored
feat: Mark module sources as read (#5963)
* feat: Mark module sources as read * feat: Consolidating into one `internal/glob` package fix: Sticking to gobaws * chore: Integrating `vfs` docs: Docs cleanup * fix: Addressing PR review
1 parent 89bea0f commit e17dccf

19 files changed

Lines changed: 1191 additions & 32 deletions

File tree

docs/bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/content/docs/03-features/08-filter/04-attributes.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ sidebar:
88

99
import { Aside } from '@astrojs/starlight/components';
1010
import FileTree from '@components/vendored/starlight/FileTree.astro';
11+
import Since from '@components/Since.astro';
1112

1213
Match units and stacks by their configuration attributes.
1314

@@ -104,6 +105,14 @@ Due to how Terragrunt parses configurations during `run --all`, functions will o
104105
Reading a file directly in the `inputs` attribute will not mark the file as read, as the `inputs` attribute is not parsed until after the queue has already been populated to support rendering of dependency outputs, which are only available after dependencies have been run.
105106
</Aside>
106107

108+
<Since version="1.0.3">
109+
110+
To mark many files at once, use [`mark_glob_as_read`](/reference/hcl/functions/#mark_glob_as_read), which accepts a glob (with `**` support) and marks every matching file.
111+
112+
If a unit's `terraform` block points at a local module source, enable the [`mark-many-as-read`](/reference/experiments) experiment to have Terragrunt automatically mark that module's `*.tf`, `*.tf.json`, `*.hcl`, `*.tofu`, and `*.tofu.json` files as read. Reading-based filters will then cascade local module changes to every unit that consumes the module. The same experiment also enables the [`mark_glob_as_read`](/reference/hcl/functions/#mark_glob_as_read) HCL function.
113+
114+
</Since>
115+
107116
## Source-Based Expressions
108117

109118
Match units and stacks by their Terraform source URL or path specified in the `terraform` block of `terragrunt.hcl` files.

docs/src/content/docs/04-reference/01-hcl/04-functions.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ sidebar:
66
order: 4
77
---
88

9+
import { Aside } from '@astrojs/starlight/components';
910
import FileTree from '@components/vendored/starlight/FileTree.astro';
11+
import Since from '@components/Since.astro';
12+
import Before from '@components/Before.astro';
1013

1114
Terragrunt allows you to use built-in functions anywhere in `terragrunt.hcl`, just like OpenTofu/Terraform\!
1215

@@ -1042,6 +1045,62 @@ The same technique can be used to mark a file as read when a file is read using
10421045
if they are used in the `locals` block. Reading a file directly in the `inputs` block will not mark the file as read, as the `inputs`
10431046
block is not evaluated until *after* the queue has been populated with units to run.
10441047

1048+
## mark_glob_as_read
1049+
1050+
<Before version="1.0.3">
1051+
1052+
<Aside type="tip">
1053+
Coming soon!
1054+
</Aside>
1055+
1056+
</Before>
1057+
1058+
<Since version="1.0.3">
1059+
1060+
<Aside type="caution">
1061+
`mark_glob_as_read` requires the [`mark-many-as-read`](/reference/experiments) experiment. Calling it without the experiment enabled returns an error.
1062+
</Aside>
1063+
1064+
<Aside type="caution">
1065+
`mark_glob_as_read` writes to the same read tracker as [`mark_as_read`](#mark_as_read), so the same evaluation-order caveat applies: during a `run --all`, the queue is populated before `inputs` is evaluated, so a call placed in `inputs` (or any other later-evaluated block) will not influence queue construction. Call `mark_glob_as_read` from `locals` to ensure matched files are considered when the queue is built.
1066+
</Aside>
1067+
1068+
`mark_glob_as_read(pattern)` expands the given glob and marks every matching file as read. It returns the list of absolute paths that matched, so it can be composed with other expressions.
1069+
1070+
Pattern syntax follows [`gobwas/glob`](https://github.com/gobwas/glob), the same matcher used elsewhere in Terragrunt. `/` is the path separator, `*` matches within a single segment, `**` matches any sequence of characters including separators, `?` matches any single non-separator character, `[abc]` matches a character class, and `{a,b}` matches any of the listed alternatives. A backslash escapes the following metacharacter.
1071+
1072+
Relative patterns are resolved against the current working directory, the same way `mark_as_read` resolves a relative file path.
1073+
1074+
<Aside type="caution">
1075+
Glob patterns must use forward slashes (`/`) as the path separator, even on Windows. The matcher treats `\` as an escape character, not a path separator, so a backslash-separated pattern will not match any files.
1076+
</Aside>
1077+
1078+
<Aside type="caution">
1079+
`**` only collapses the separators around it when the adjacent segments are literals. `config/**/*.yaml` matches files at least one directory below `config/`, but not files directly inside `config/`, because the trailing segment `*.yaml` is a wildcard. Use brace alternation to match at any depth:
1080+
1081+
```hcl
1082+
mark_glob_as_read("${get_terragrunt_dir()}/config/{*.yaml,**/*.yaml}")
1083+
```
1084+
</Aside>
1085+
1086+
```hcl
1087+
# terragrunt.hcl
1088+
1089+
locals {
1090+
configs = mark_glob_as_read("${get_terragrunt_dir()}/config/{*.yaml,**/*.yaml}")
1091+
}
1092+
1093+
inputs = {
1094+
configs = [for f in local.configs : yamldecode(file(f))]
1095+
}
1096+
```
1097+
1098+
A typical use case is a unit that consumes a directory of configuration files indirectly (for example, through `run_cmd`, `templatefile`, or a shared helper module) and wants changes to any of them to trigger the unit through [reading-based filter expressions](/features/filter/attributes#reading-based-expressions) such as `--filter 'reading=./config/**'`.
1099+
1100+
If the pattern matches nothing, the function returns an empty list without error.
1101+
1102+
</Since>
1103+
10451104
## constraint_check
10461105

10471106
`constraint_check(version, constraint)` checks if a given version satisfies a given constraint.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
version: "v1.0.3"
3+
category: "experiments-added"
4+
---
5+
6+
#### `mark-many-as-read` — Mark many files as read in one step
7+
8+
Enable the new [`mark-many-as-read`](/reference/experiments) experiment to turn on two behaviors that each mark many files as read in a single call: automatic marking of files inside a local `terraform { source = "..." }` block, and the new `mark_glob_as_read` HCL function.
9+
10+
With the experiment on, a unit like this:
11+
12+
```hcl
13+
# live/unit/terragrunt.hcl
14+
terraform {
15+
source = "../../modules/service"
16+
}
17+
```
18+
19+
records every `*.tf`, `*.tf.json`, `*.hcl`, `*.tofu`, and `*.tofu.json` file found under `../../modules/service` (recursively) as read for the unit. Non-source files such as `README.md` are skipped. A [reading-based filter expression](/features/filter/attributes#reading-based-expressions) such as `--filter 'reading=../../modules/service/**'` then matches every unit that points at the module, so a change to any file in the module cascades to its consumers.
20+
21+
The same experiment also enables a new HCL function, `mark_glob_as_read(pattern)`, which expands a glob using the same [`gobwas/glob`](https://github.com/gobwas/glob) syntax as filter expressions and marks every matching file as read. It returns the list of absolute paths that matched, so it composes with other expressions:
22+
23+
```hcl
24+
locals {
25+
configs = mark_glob_as_read("${get_terragrunt_dir()}/config/{*.yaml,**/*.yaml}")
26+
}
27+
```
28+
29+
`**` only collapses the surrounding separators when the adjacent segments are literals, so match-at-any-depth with a wildcard trailing segment is written as `{*.yaml,**/*.yaml}`. See the [HCL reference](/reference/hcl/functions/#mark_glob_as_read) for full pattern syntax.
30+
31+
This is useful when a unit reads a collection of files indirectly (for example, via `run_cmd` or `templatefile`) and you want changes to any of them to trigger the unit through reading-based filters. Calling `mark_glob_as_read` without the experiment enabled returns an error.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
name: mark-many-as-read
3+
status: active
4+
since: "1.0.3"
5+
---
6+
7+
Mark many files as read in one step, so reading-based filter expressions cascade changes from shared files to the units that consume them.
8+
9+
### `mark-many-as-read` - What it does
10+
11+
Enabling the experiment activates two behaviors:
12+
13+
1. When a unit's `terraform` block points at a local module source, Terragrunt walks that directory and records every `*.tf`, `*.tf.json`, `*.hcl`, `*.tofu`, and `*.tofu.json` file as read for the unit. Non-source files such as `README.md` are skipped. Remote sources (Git, registry, S3, etc.) are not walked.
14+
2. The `mark_glob_as_read(pattern)` HCL function becomes available. It expands a glob using [`gobwas/glob`](https://github.com/gobwas/glob) syntax and marks every matching file as read, returning the list of absolute paths that matched. Without the experiment enabled, calling the function returns an error. See the [HCL reference](/reference/hcl/functions/#mark_glob_as_read) for pattern syntax and examples.
15+
16+
Both behaviors feed the same reading tracker that powers the [`reading=` filter attribute](/features/filter/attributes#reading-based-expressions), so a change to a local module file or a globbed configuration file is picked up by `--filter 'reading=<path>'` and matches every unit that reads it.
17+
18+
```bash
19+
terragrunt run --all --experiment mark-many-as-read -- plan
20+
```
21+
22+
### `mark-many-as-read` - How to provide feedback
23+
24+
Provide your feedback on the [GitHub Discussions](https://github.com/gruntwork-io/terragrunt/discussions) page.
25+
26+
### `mark-many-as-read` - Criteria for stabilization
27+
28+
To transition the `mark-many-as-read` feature to a stable release, the following must be addressed:
29+
30+
- [ ] Confirm local module walking handles nested modules sensibly across macOS, Linux, and Windows.
31+
- [ ] Confirm glob semantics match user expectations for common patterns, especially `**` with a wildcard trailing segment.
32+
- [ ] Positive feedback from users relying on reading-based filters in production pipelines.
33+
- [ ] Integration tests covering the interaction between module walking, `mark_glob_as_read`, and `--filter 'reading=...'`.

internal/experiment/experiment.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ const (
5151
StackDependencies = "stack-dependencies"
5252
// CatalogRedesign is the experiment that enables the redesigned catalog experience.
5353
CatalogRedesign = "catalog-redesign"
54+
// MarkManyAsRead enables behaviors that mark many files as read in one
55+
// step: automatic marking of files inside a local terraform module source
56+
// (so reading-based filter expressions detect changes to the module) and
57+
// the mark_glob_as_read HCL function.
58+
MarkManyAsRead = "mark-many-as-read"
5459
)
5560

5661
const (
@@ -115,6 +120,9 @@ func NewExperiments() Experiments {
115120
{
116121
Name: CatalogRedesign,
117122
},
123+
{
124+
Name: MarkManyAsRead,
125+
},
118126
}
119127
}
120128

internal/filter/ast.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import (
44
"path/filepath"
55
"strconv"
66

7-
"github.com/gobwas/glob"
87
"github.com/gruntwork-io/terragrunt/internal/component"
8+
"github.com/gruntwork-io/terragrunt/internal/glob"
99
)
1010

1111
// Expression is the interface that all AST nodes must implement.
@@ -31,15 +31,15 @@ type Expressions []Expression
3131

3232
// PathExpression represents a path or glob filter (e.g., "./path/**/*" or "/absolute/path").
3333
type PathExpression struct {
34-
compiledGlob glob.Glob
34+
compiledGlob glob.Matcher
3535
Value string
3636
}
3737

3838
// NewPathFilter creates a new PathFilter with eager glob compilation.
3939
func NewPathFilter(value string) (*PathExpression, error) {
4040
pattern := filepath.Clean(filepath.ToSlash(value))
4141

42-
compiled, err := glob.Compile(pattern, '/')
42+
compiled, err := glob.Compile(pattern)
4343
if err != nil {
4444
return nil, err
4545
}
@@ -48,7 +48,7 @@ func NewPathFilter(value string) (*PathExpression, error) {
4848
}
4949

5050
// Glob returns the pre-compiled glob pattern.
51-
func (p *PathExpression) Glob() glob.Glob {
51+
func (p *PathExpression) Glob() glob.Matcher {
5252
return p.compiledGlob
5353
}
5454

@@ -61,7 +61,7 @@ func (p *PathExpression) Negated() Expression { return NewPref
6161

6262
// AttributeExpression represents a key-value attribute filter (e.g., "name=my-app").
6363
type AttributeExpression struct {
64-
compiledGlob glob.Glob
64+
compiledGlob glob.Matcher
6565
Key string
6666
Value string
6767
}
@@ -78,7 +78,7 @@ func NewAttributeExpression(key string, value string) (*AttributeExpression, err
7878
pattern = filepath.Clean(filepath.ToSlash(pattern))
7979
}
8080

81-
compiled, err := glob.Compile(pattern, '/')
81+
compiled, err := glob.Compile(pattern)
8282
if err != nil {
8383
return nil, err
8484
}
@@ -96,7 +96,7 @@ func NewTypeExpression(kind component.Kind) *AttributeExpression {
9696
}
9797

9898
// Glob returns the pre-compiled glob pattern.
99-
func (a *AttributeExpression) Glob() glob.Glob {
99+
func (a *AttributeExpression) Glob() glob.Matcher {
100100
return a.compiledGlob
101101
}
102102

internal/filter/doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@
210210
// ## Evaluator
211211
//
212212
// The evaluator (evaluator.go) walks the AST and applies the filter logic:
213-
// - PathFilter: Uses glob matching (github.com/gobwas/glob) with eager compilation
213+
// - PathFilter: Uses glob matching (internal/glob Compile, backed by gobwas) with eager compilation
214214
// and caching via sync.Once for performance
215215
// - AttributeFilter: Matches attributes by key-value pairs:
216216
// - name: Matches filepath.Base(component.Path)

0 commit comments

Comments
 (0)