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.16.0] - 2026-04-04

### Changed
- **Breaking:** Merged `target` and `virtual-target` types into a unified target definition. The `type` field is removed. All targets now support `app`, `targetName`, `changeDirs`, lockfile detection, and fine-grained mode. `targetName` defaults to the package name when not set. `changeDirs` defaults to `**/*` when not set.

## [0.15.3] - 2026-02-23

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

[0.16.0]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.15.3...v0.16.0
[0.15.3]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.15.2...v0.15.3
[0.15.2]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.15.1...v0.15.2
[0.15.1]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.15.0...v0.15.1
Expand Down
37 changes: 15 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,9 @@ Each project can optionally have a `.goodchangesrc.json` file in its root direct
{
"targets": [
{
"type": "target",
"app": "@gooddata/gdc-dashboards"
},
{
"type": "virtual-target",
"targetName": "neobackstop",
"changeDirs": [
{ "glob": "src/**/*" },
Expand All @@ -104,20 +102,16 @@ Each project can optionally have a `.goodchangesrc.json` file in its root direct
}
```

### Target
### Trigger conditions

Marks a project as an e2e test package. The package name is included in the output when any of the 4 trigger conditions are met.
Each target is triggered by any of these conditions:

**Trigger conditions:**

1. **Direct file changes** -- files changed in the project folder (excluding ignored paths)
1. **Direct file changes** -- files matching `changeDirs` globs changed (excluding ignored paths). Defaults to `**/*` (entire project) when `changeDirs` is not set.
2. **External dependency changes** -- a dependency version changed in `pnpm-lock.yaml`
3. **Tainted workspace imports** -- the target imports a tainted symbol from a workspace library
3. **Tainted workspace imports** -- a file matching `changeDirs` globs imports a tainted symbol from a workspace library
4. **Corresponding app is tainted** -- the app specified by `app` is affected (any of the above conditions)

### Virtual target

An aggregated target that uses glob patterns to match files across a project. Does not correspond to a real package name in the output -- uses `targetName` instead.
### changeDirs

Each `changeDirs` entry is an object with:

Expand All @@ -142,20 +136,19 @@ 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 |

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

| Field | Type | Used by | Description |
|--------------|----------------------------------|----------------|--------------------------------------------------------------------------------------------------------|
| `type` | `"target"` \| `"virtual-target"` | Both | Declares what kind of target this is |
| `app` | `string` | Target | Package name of the corresponding app this e2e package tests |
| `targetName` | `string` | Virtual target | Output name emitted when the virtual target is triggered |
| `changeDirs` | `ChangeDir[]` | Virtual target | Glob patterns to match files. Each entry: `{"glob": "...", "filter?": "...", "type?": "fine-grained"}` |
| `ignores` | `string[]` | Both | 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.15.3
0.16.0
56 changes: 46 additions & 10 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -1323,20 +1323,24 @@ func FindAffectedFiles(globPattern string, filterPattern string, upstreamTaint m
}
changedSymbols := findAffectedSymbolsByASTDiff(oldAnalysis, analysis, oldContent, includeTypes)
debugf(" %s: affected symbols (AST diff): %v", stem, changedSymbols)
if len(changedSymbols) > 0 || oldAnalysis == nil {
if tainted[stem] == nil {
tainted[stem] = make(map[string]bool)
if tainted[stem] == nil {
tainted[stem] = make(map[string]bool)
}
if oldAnalysis == nil {
// New file: taint all symbols
debugf(" %s: new file — tainting all symbols", stem)
for _, sym := range analysis.Symbols {
tainted[stem][sym.Name] = true
}
tainted[stem]["*"] = true
} else if len(changedSymbols) > 0 {
for _, s := range changedSymbols {
tainted[stem][s] = true
}
// New file: taint all symbols
if oldAnalysis == nil {
debugf(" %s: new file — tainting all symbols", stem)
for _, sym := range analysis.Symbols {
tainted[stem][sym.Name] = true
}
}
} else {
// File changed but no symbol-level diff detected (e.g. changes in
// test()/describe() blocks or other non-declaration code).
tainted[stem]["*"] = true
}
}

Expand Down Expand Up @@ -1699,6 +1703,38 @@ func FindAffectedFiles(globPattern string, filterPattern string, upstreamTaint m
}
}

// Mark leaf files (no tainted symbols yet) that import from tainted files.
// Covers spec/test files that use imports in test()/describe() blocks
// without declaring top-level symbols the usage checker can find.
for stem := range fileAnalyses {
if tainted[stem] != nil {
continue
}
for _, edge := range localImportGraph[stem] {
src := tainted[edge.fromStem]
if src == nil {
continue
}
if edge.isSideEffect {
tainted[stem] = map[string]bool{"*": true}
break
}
for _, origName := range edge.origNames {
if origName == "*" && len(src) > 0 {
tainted[stem] = map[string]bool{"*": true}
break
}
if src[origName] || src["*"] {
tainted[stem] = map[string]bool{"*": true}
break
}
}
if tainted[stem] != nil {
break
}
}
}

debugf("=== Final taint map (FindAffectedFiles) ===")
for stem, names := range tainted {
debugf(" %s: %v", stem, mapKeys(names))
Expand Down
19 changes: 8 additions & 11 deletions internal/rush/rush.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,18 @@ func (cd ChangeDir) IsFineGrained() bool {
}

type TargetDef struct {
Type string `json:"type"` // "target", "virtual-target"
App *string `json:"app,omitempty"` // rush project name of corresponding app
TargetName *string `json:"targetName,omitempty"` // output name for virtual targets
ChangeDirs []ChangeDir `json:"changeDirs,omitempty"` // globs to watch for virtual targets
TargetName *string `json:"targetName,omitempty"` // custom output name (defaults to package name)
ChangeDirs []ChangeDir `json:"changeDirs,omitempty"` // globs to watch (defaults to **/* if empty)
Ignores []string `json:"ignores,omitempty"` // per-target ignore globs (additive with global)
}

// IsTarget returns true if this target definition is a regular target.
func (td TargetDef) IsTarget() bool {
return td.Type == "target"
}

// IsVirtualTarget returns true if this target definition is a virtual target.
func (td TargetDef) IsVirtualTarget() bool {
return td.Type == "virtual-target"
// OutputName returns the target's output name: targetName if set, otherwise the package name.
func (td TargetDef) OutputName(packageName string) string {
if td.TargetName != nil {
return *td.TargetName
}
return packageName
}

type ProjectConfig struct {
Expand Down
Loading
Loading