diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da908c..dbef83e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/README.md b/README.md index 9924fe5..ffbdfdb 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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. diff --git a/VERSION b/VERSION index 7cca771..47d04a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.17.1 +0.18.0 \ No newline at end of file diff --git a/internal/rush/rush.go b/internal/rush/rush.go index 379a187..0558380 100644 --- a/internal/rush/rush.go +++ b/internal/rush/rush.go @@ -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. diff --git a/main.go b/main.go index cade810..e3d0686 100644 --- a/main.go +++ b/main.go @@ -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) @@ -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) { @@ -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)