Minimal reproduction for renovatebot/renovate#45603
A dependency that matches no packageRules entry containing replacementName
is nevertheless offered that replacement. Here the bare operator tag
percona/percona-postgresql-operator:2.6.0 is offered
percona/percona-distribution-postgresql:17.7-2 — a different image entirely.
That Docker repository hosts several unrelated components distinguished only by tag suffix, so a mis-applied replacement swaps one component's image for another's rather than merely picking a wrong version.
./reproduce.shSelf-contained: it creates its own temp directory, writes the two config files,
installs renovate@44.54.0 and re2, and runs. Needs node >= 24 and network
access to Docker Hub. All three images are public, so no credentials are needed.
RENOVATE_PLATFORM=local with RENOVATE_DRY_RUN=full only reads — it contacts
no git platform and writes nothing outside its temp directory.
renovate.json and values.yaml are included separately for reading; the
script embeds identical copies.
percona/percona-postgresql-operator:2.7.0-ppg17.5.2-postgres replacement -> percona/percona-distribution-postgresql:17.7-2
percona/percona-postgresql-operator:2.7.0-ppg17.5.2-postgres-gis3.3.8 replacement -> percona/percona-distribution-postgresql:17.7-2
percona/percona-postgresql-operator:2.6.0 minor -> 2.9.0; major -> 3.1.0; replacement -> percona/percona-distribution-postgresql:17.7-2
Only dep a (2.7.0-ppg17.5.2-postgres) matches the replacement rule's
matchCurrentVersion of /-postgres$/. Deps b and c should receive no
replacement update.
The minor / major targets on dep c are legitimate and will drift as Percona
publishes new operator tags — ignore them. The signal is the replacement ->
suffix on deps b and c.
The regexes are correct in isolation. Tested directly against the three tag
strings, /-postgres$/ matches only 2.7.0-ppg17.5.2-postgres, so the rule's
own matching is not the problem:
> /-postgres$/.test("2.7.0-ppg17.5.2-postgres") // true
> /-postgres$/.test("2.7.0-ppg17.5.2-postgres-gis3.3.8") // false
> /-postgres$/.test("2.6.0") // falseRemoving any one of these three makes the run correct, so all three appear necessary:
versioning: "semver"on a rule matching the victim dep (c). Dropping it from rule 1 makes the run clean. That rule'sallowedVersionsis not needed — it can be omitted entirely.- A rule with an unsatisfiable
allowedVersionsmatching a third dep (b). Remove that rule, or give it a satisfiableallowedVersions, and the run is clean. - A dep that legitimately matches the replacement rule (
a). With onlybandcpresent, the run is clean.
Both b and c receive the replacement belonging to a, which is why this
reads as a replacement escaping its rule and attaching to sibling deps sharing a
packageName.
- Not the RE2 fallback. First seen with the JS fallback, then reproduced with
re2installed and loaded. The script asserts0fallback warnings. - Not a stale cache. Reproduced with the cache directory removed before each run, and from a fresh temp dir with no cache at all.
- Not flaky. Deterministic across repeated runs.
- Not version-specific. Reproduced on 44.52.1 and 44.54.0.
Reduced from a real shared Renovate preset — 28 rules and 8 dependencies — down to 3 and 3, by dropping one rule at a time and keeping the drop whenever the wrong replacement survived.