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.24.12] - 2026-07-30

### Fixed
- App taint propagation (added in 0.24.1) no longer over-flags downstream targets. An affected app was seeded into the upstream-taint map — tainting everything that imports it — whenever it was merely *reachable* in the affected set, i.e. a transitive dependent of any touched package. So a change to a library an app depends on flagged that app's harness/host consumers even when the change affected **no export the app actually imports** (e.g. removing unused exports from `gdc-analytical-designer-runtime` flagged `gdc-analytical-designer-harness`, `gdc-dashboards-harness`, and `gdc-host-application`). The wholesale-taint seed is now gated on the app being genuinely affected — the same conditions its own target detection uses: directly changed (has changed files), a changed lockfile dependency, or an actual tainted import from upstream (`HasTaintedImportsForGlob`). Apps that are only transitively reachable but import nothing that changed no longer propagate taint; real changes to exports an app imports still propagate as before. The per-package upstream-taint filter is also factored into a shared `buildPkgUpstreamTaint` helper used by both the library and app paths.

## [0.24.11] - 2026-07-30

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

[0.24.12]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.11...v0.24.12
[0.24.11]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.10...v0.24.11
[0.24.10]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.9...v0.24.10
[0.24.9]: https://github.com/gooddata/gooddata-goodchanges/compare/v0.24.8...v0.24.9
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.24.11
0.24.12
82 changes: 53 additions & 29 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,16 +283,30 @@ func main() {

if !lib {
log.Basicf(" Type: app (not a library) — skipping export analysis")
// Every package reaching this loop is affected (directly, via a
// lockfile dep, or transitively via the workspace graph). An app gets
// no per-symbol export diffing, but it is tainted wholesale: anything
// importing from it must be treated as tainted too (e.g. an app
// mounted by a thin harness that dynamically imports it). Seed ALL of
// the app's entrypoint exports, mirroring the global-changeDirs
// full-taint seeding used for libraries below. Downstream importers
// then match these in HasTaintedImportsForGlob / FindAffectedFiles —
// including bare/dynamic side-effect imports, which match on any
// non-empty symbol set for the package.
// An app gets no per-symbol export diffing; when it IS affected it is
// tainted wholesale (anything importing from it — e.g. a thin harness
// that dynamically imports it — must be treated as tainted too). But
// being merely reachable in the affected set is not enough: a transitive
// dependent whose upstream change touched nothing this app actually
// imports must NOT be tainted, or every downstream harness/host target
// gets flagged for unrelated changes. So gate the seed on the same
// conditions the app's own target detection uses: directly changed
// (has changed files), a changed lockfile dep, or an actual tainted
// import from upstream.
appAffected := directlyChanged || isDepAffected
if !appAffected {
appUpstreamTaint := buildPkgUpstreamTaint(info.DependsOn, allUpstreamTaint)
appAffected = analyzer.HasTaintedImportsForGlob(info.ProjectFolder, "**/*", appUpstreamTaint, configMap[info.ProjectFolder])
}
if !appAffected {
log.Basicf(" App not affected by this change (no changed files, no tainted imports) — not tainting\n")
continue
}
// Seed ALL of the app's entrypoint exports, mirroring the global-changeDirs
// full-taint seeding used for libraries below. Downstream importers then
// match these in HasTaintedImportsForGlob / FindAffectedFiles — including
// bare/dynamic side-effect imports, which match on any non-empty symbol
// set for the package.
entrypoints := analyzer.FindEntrypoints(info.ProjectFolder, pkg)
totalExports := 0
for _, ep := range entrypoints {
Expand Down Expand Up @@ -368,25 +382,7 @@ func main() {

// 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)
for _, dep := range info.DependsOn {
for specifier, names := range allUpstreamTaint {
matches := strings.HasPrefix(specifier, dep)
if !matches && strings.HasPrefix(specifier, analyzer.CSSTaintPrefix) {
// CSS taint keys are namespaced ("__css__:pkg"); match on the package name
// so a dep's CSS taint is visible while analysing this package's style @use chains.
matches = strings.HasPrefix(strings.TrimPrefix(specifier, analyzer.CSSTaintPrefix), dep)
}
if matches {
if pkgUpstreamTaint[specifier] == nil {
pkgUpstreamTaint[specifier] = make(map[string]bool)
}
for n := range names {
pkgUpstreamTaint[specifier][n] = true
}
}
}
}
pkgUpstreamTaint := buildPkgUpstreamTaint(info.DependsOn, allUpstreamTaint)

wg.Add(1)
go func(pkgName string, projectFolder string, entrypoints []analyzer.Entrypoint, pkgUpstreamTaint map[string]map[string]bool, changedDeps map[string]bool) {
Expand Down Expand Up @@ -603,6 +599,34 @@ 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.
// buildPkgUpstreamTaint filters the global taint map down to the entries that
// belong to a package's direct dependencies — matching on specifier prefix, and
// (for CSS-taint keys) on the package name inside the "__css__:" namespace. This
// is the per-package upstream taint used both to analyse a library and to decide
// whether an app actually imports any tainted upstream export.
func buildPkgUpstreamTaint(dependsOn []string, allUpstreamTaint map[string]map[string]bool) map[string]map[string]bool {
pkgUpstreamTaint := make(map[string]map[string]bool)
for _, dep := range dependsOn {
for specifier, names := range allUpstreamTaint {
matches := strings.HasPrefix(specifier, dep)
if !matches && strings.HasPrefix(specifier, analyzer.CSSTaintPrefix) {
// CSS taint keys are namespaced ("__css__:pkg"); match on the package name
// so a dep's CSS taint is visible while analysing this package's style @use chains.
matches = strings.HasPrefix(strings.TrimPrefix(specifier, analyzer.CSSTaintPrefix), dep)
}
if matches {
if pkgUpstreamTaint[specifier] == nil {
pkgUpstreamTaint[specifier] = make(map[string]bool)
}
for n := range names {
pkgUpstreamTaint[specifier][n] = true
}
}
}
}
return pkgUpstreamTaint
}

func globalChangeDirTriggered(changeDirs []rush.ChangeDir, changedFiles []string, projectFolder string, cfg *rush.ProjectConfig) bool {
for _, cd := range changeDirs {
for _, f := range changedFiles {
Expand Down
Loading