Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.18.0] - 2026-04-10

### Added
- Global `changeDirs` field in `.goodchangesrc.json` (top-level, next to `ignores`). Matching files taint all exports (libraries) and trigger all targets in the package.

## [0.17.1] - 2026-04-10

### Fixed
Expand Down Expand Up @@ -247,6 +252,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Multi-stage Docker build
- Automated vendor upgrade workflow

[0.18.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.17.1...v0.18.0
[0.17.1]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.17.0...v0.17.1
[0.17.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.16.7...v0.17.0
[0.16.7]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.16.6...v0.16.7
Expand Down
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ Each project can optionally have a `.goodchangesrc.json` file in its root direct
}
```

### Global changeDirs

Top-level `changeDirs` apply to the entire package. When any changed file matches a global changeDir glob, all library exports are wildcard-tainted and all targets are triggered. This is useful for files that affect everything but aren't tracked by the AST analysis (e.g. locale bundles, config files).

```json
{
"changeDirs": [
{ "glob": "src/localization/bundles/en-US.json" }
],
"targets": [
{ "targetName": "my-tests", "changeDirs": [{ "glob": "**/*", "type": "fine-grained", "filter": "**/*.test.ts*" }] }
]
}
```

### Trigger conditions

Each target is triggered by any of these conditions:
Expand Down Expand Up @@ -136,19 +151,20 @@ Each `changeDirs` entry is an object with:

**Top-level fields:**

| Field | Type | Description |
|-----------|---------------|----------------------------------------------------------|
| `targets` | `TargetDef[]` | Array of target definitions (see below) |
| `ignores` | `string[]` | Glob patterns for files to exclude from change detection |
| Field | Type | Description |
|--------------|---------------|---------------------------------------------------------------------------------------------------------|
| `targets` | `TargetDef[]` | Array of target definitions (see below) |
| `ignores` | `string[]` | Glob patterns for files to exclude from change detection |
| `changeDirs` | `ChangeDir[]` | Global changeDirs. When triggered, taints all library exports and triggers all targets in this package. |

**TargetDef fields (each entry in `targets`):**

| Field | Type | Description |
|--------------|----------------|--------------------------------------------------------------------------------------------------------|
| `app` | `string` | Package name of the corresponding app this target tests |
| `targetName` | `string` | Custom output name (defaults to the package name when not set) |
| `changeDirs` | `ChangeDir[]` | Glob patterns to match files. Defaults to `**/*` (entire project). Each entry: `{"glob": "...", "filter?": "...", "type?": "fine-grained"}` |
| `ignores` | `string[]` | Per-target ignore globs. Additive with the global `ignores` -- only applies to this target's detection |
| Field | Type | Description |
|--------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `app` | `string` | Package name of the corresponding app this target tests |
| `targetName` | `string` | Custom output name (defaults to the package name when not set) |
| `changeDirs` | `ChangeDir[]` | Glob patterns to match files. Defaults to `**/*` (entire project). Each entry: `{"glob": "...", "filter?": "...", "type?": "fine-grained"}` |
| `ignores` | `string[]` | Per-target ignore globs. Additive with the global `ignores` -- only applies to this target's detection |

The `.goodchangesrc.json` file itself is always ignored.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.17.1
0.18.0
5 changes: 3 additions & 2 deletions internal/rush/rush.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ func (td TargetDef) OutputName(packageName string) string {
}

type ProjectConfig struct {
Targets []TargetDef `json:"targets,omitempty"`
Ignores []string `json:"ignores,omitempty"`
Targets []TargetDef `json:"targets,omitempty"`
Ignores []string `json:"ignores,omitempty"`
ChangeDirs []ChangeDir `json:"changeDirs,omitempty"` // global changeDirs: triggers all exports (library) or all targets (app)
}

// LoadProjectConfig reads .goodchangesrc.json from the project folder.
Expand Down
46 changes: 46 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,19 @@ func main() {
logf(" Changed external deps: %s\n", strings.Join(depNames, ", "))
}

// Global changeDirs: if triggered, taint all exports (skip expensive analysis)
libCfg := configMap[info.ProjectFolder]
if libCfg != nil && len(libCfg.ChangeDirs) > 0 {
if globalChangeDirTriggered(libCfg.ChangeDirs, changedFiles, info.ProjectFolder, libCfg) {
logf(" Global changeDirs triggered — all exports tainted\n\n")
if allUpstreamTaint[pkgName] == nil {
allUpstreamTaint[pkgName] = make(map[string]bool)
}
allUpstreamTaint[pkgName]["*"] = true
continue
}
}

// Build upstream taint for this package from its dependencies.
// allUpstreamTaint is only read here — writes happen after the level completes.
pkgUpstreamTaint := make(map[string]map[string]bool)
Expand Down Expand Up @@ -349,6 +362,20 @@ func main() {
continue
}

// Global changeDirs: if triggered, add ALL targets for this package
if len(cfg.ChangeDirs) > 0 {
if globalChangeDirTriggered(cfg.ChangeDirs, changedFiles, rp.ProjectFolder, cfg) {
for _, td := range cfg.Targets {
name := td.OutputName(rp.PackageName)
if len(targetPatterns) > 0 && !matchesTargetFilter(name, targetPatterns) {
continue
}
changedE2E[name] = &TargetResult{Name: name}
}
continue
}
}

for _, td := range cfg.Targets {
name := td.OutputName(rp.PackageName)
if len(targetPatterns) > 0 && !matchesTargetFilter(name, targetPatterns) {
Expand Down Expand Up @@ -517,6 +544,25 @@ func findLockfileAffectedProjects(config *rush.Config, mergeBase string) (map[st

// matchesTargetFilter checks if a target name matches any of the given patterns.
// Patterns support * as a wildcard matching any characters (including /).
// globalChangeDirTriggered checks if any changed file matches a global changeDir glob.
func globalChangeDirTriggered(changeDirs []rush.ChangeDir, changedFiles []string, projectFolder string, cfg *rush.ProjectConfig) bool {
for _, cd := range changeDirs {
for _, f := range changedFiles {
if !strings.HasPrefix(f, projectFolder+"/") {
continue
}
relPath := strings.TrimPrefix(f, projectFolder+"/")
if cfg.IsIgnored(relPath) {
continue
}
if matched, _ := doublestar.Match(cd.Glob, relPath); matched {
return true
}
}
}
return false
}

func matchesTargetFilter(name string, patterns []string) bool {
for _, p := range patterns {
p = strings.TrimSpace(p)
Expand Down
Loading