diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f9b018..8c6b628 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.24.1] - 2026-07-21 + +### Fixed +- Affected **apps** now propagate taint to packages that import them. When a package is not a library, its per-symbol export analysis is skipped — but it was also seeding no taint at all, so the taint chain died at the app boundary. A consumer importing from an affected app (e.g. a thin harness that dynamically imports its app package: `() => import("gdc-analytical-designer-module")`) saw a clean upstream-taint map and was never flagged, so app/harness targets sitting behind an intermediate app went undetected. Now an affected app is tainted **wholesale**: all of its entrypoint exports are seeded into the upstream-taint map (mirroring the existing global-`changeDirs` full-taint seeding for libraries), so downstream importers match via the normal import graph — including bare/dynamic side-effect imports (empty import names), which match on any non-empty symbol set for the package. Example: a change in `gdc-analytical-designer-runtime` now correctly reaches `gdc-analytical-designer-harness` through the intermediate `gdc-analytical-designer-module` app. + ## [0.24.0] - 2026-06-08 ### Added diff --git a/VERSION b/VERSION index 286d5b0..1fee947 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.24.0 \ No newline at end of file +0.24.1 \ No newline at end of file diff --git a/main.go b/main.go index 82c5dec..b998a20 100644 --- a/main.go +++ b/main.go @@ -282,7 +282,42 @@ func main() { } if !lib { - log.Basicf(" Type: app (not a library) — skipping export analysis\n") + 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. + entrypoints := analyzer.FindEntrypoints(info.ProjectFolder, pkg) + totalExports := 0 + for _, ep := range entrypoints { + specifier := pkgName + if ep.ExportPath != "." { + specifier = pkgName + strings.TrimPrefix(ep.ExportPath, ".") + } + exports := analyzer.CollectEntrypointExports(info.ProjectFolder, ep) + if allUpstreamTaint[specifier] == nil { + allUpstreamTaint[specifier] = make(map[string]bool) + } + for _, name := range exports { + allUpstreamTaint[specifier][name] = true + } + // A side-effect-only entrypoint enumerates zero exports, which would + // leave an empty (== untainted) set and stop propagation to bare/dynamic + // importers. Fall back to the "*" whole-package marker (as used for + // version-changed and CSS-tainted packages) so the app still taints + // wholesale. + if len(exports) == 0 { + allUpstreamTaint[specifier]["*"] = true + } + totalExports += len(exports) + } + log.Basicf(" App is affected — tainting all %d exports across %d entrypoint(s) (whole-app taint)\n", totalExports, len(entrypoints)) continue }