Skip to content

Instantly share code, notes, and snippets.

@benlangfeld
Created September 1, 2026 15:22
Show Gist options
  • Select an option

  • Save benlangfeld/4255d50acabf053b969ffa2f1fdfaa88 to your computer and use it in GitHub Desktop.

Select an option

Save benlangfeld/4255d50acabf053b969ffa2f1fdfaa88 to your computer and use it in GitHub Desktop.
Renovate repro: a replacement escapes its packageRule and is offered on sibling deps of the same packageName (renovatebot/renovate#45603)

Renovate: a replacement escapes its packageRule and is offered on sibling deps

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.

Run it

./reproduce.sh

Self-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.

Observed

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

Expected

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")                                  // false

What the trigger needs

Removing any one of these three makes the run correct, so all three appear necessary:

  1. versioning: "semver" on a rule matching the victim dep (c). Dropping it from rule 1 makes the run clean. That rule's allowedVersions is not needed — it can be omitted entirely.
  2. A rule with an unsatisfiable allowedVersions matching a third dep (b). Remove that rule, or give it a satisfiable allowedVersions, and the run is clean.
  3. A dep that legitimately matches the replacement rule (a). With only b and c present, 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.

Ruled out

  • Not the RE2 fallback. First seen with the JS fallback, then reproduced with re2 installed and loaded. The script asserts 0 fallback 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.

Provenance

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.

{ "packageRules": [
{ "matchDatasources":["docker"], "matchPackageNames":["percona/percona-postgresql-operator"],
"matchCurrentVersion":"/^\\d+\\.\\d+\\.\\d+$/", "versioning":"semver" },
{ "matchDatasources":["docker"], "matchPackageNames":["percona/percona-postgresql-operator"],
"matchCurrentVersion":"/-postgres-gis/", "allowedVersions":"/^(nonexistent)$/" },
{ "matchDatasources":["docker"], "matchPackageNames":["percona/percona-postgresql-operator"],
"matchCurrentVersion":"/-postgres$/", "replacementName":"percona/percona-distribution-postgresql", "replacementVersion":"17.7-2" }
] }
#!/usr/bin/env bash
# Self-contained reproduction. Needs node >= 24 and network access to Docker Hub.
# Writes nothing outside its own temp directory and makes no calls to any git
# platform: RENOVATE_PLATFORM=local + RENOVATE_DRY_RUN=full only read.
set -euo pipefail
WORK="$(mktemp -d)"
echo "workdir: $WORK"
cd "$WORK"
cat > values.yaml <<'YAML'
a:
image: percona/percona-postgresql-operator:2.7.0-ppg17.5.2-postgres
b:
image: percona/percona-postgresql-operator:2.7.0-ppg17.5.2-postgres-gis3.3.8
c:
image: percona/percona-postgresql-operator:2.6.0
YAML
cat > renovate.json <<'JSON'
{ "packageRules": [
{ "matchDatasources":["docker"], "matchPackageNames":["percona/percona-postgresql-operator"],
"matchCurrentVersion":"/^\\d+\\.\\d+\\.\\d+$/", "versioning":"semver" },
{ "matchDatasources":["docker"], "matchPackageNames":["percona/percona-postgresql-operator"],
"matchCurrentVersion":"/-postgres-gis/", "allowedVersions":"/^(nonexistent)$/" },
{ "matchDatasources":["docker"], "matchPackageNames":["percona/percona-postgresql-operator"],
"matchCurrentVersion":"/-postgres$/", "replacementName":"percona/percona-distribution-postgresql", "replacementVersion":"17.7-2" }
] }
JSON
git init -q .
git add -A
git -c user.email=repro@example.com -c user.name=repro commit -qm repro
# re2 is installed so this cannot be blamed on the JS regex fallback
cat > package.json <<'JSON'
{ "name": "repro", "private": true,
"dependencies": { "renovate": "44.54.0", "re2": "^1.26.1" } }
JSON
npm install --silent
LOG_LEVEL=debug RENOVATE_PLATFORM=local RENOVATE_DRY_RUN=full \
node node_modules/renovate/dist/renovate.js > renovate.log 2>&1 || true
echo
echo "renovate version: $(grep -o '\"renovateVersion\": \"[^\"]*\"' renovate.log | head -1)"
echo "RE2 fallback warnings (expect 0): $(grep -c 'RE2 not usable' renovate.log)"
echo
echo "--- updates per dependency ---"
node -e '
const fs=require("fs");
const lines=fs.readFileSync("renovate.log","utf8").split("\n");
const i=lines.findIndex(l=>l.includes("packageFiles with updates"));
const buf=[];
for (const l of lines.slice(i+1)) {
if (l.startsWith(" ") || !l.trim()) { buf.push(l); if (l.startsWith(" }")) break; }
else break;
}
const m=buf.join("\n").match(/\{[\s\S]*\}/);
const d=JSON.parse(m[0]);
for (const files of Object.values(d)) for (const f of files) for (const dep of f.deps||[]) {
const outs=(dep.updates||[]).map(u =>
u.newName && u.newName!==dep.depName ? `${u.updateType} -> ${u.newName}:${u.newValue}`
: `${u.updateType} -> ${u.newValue}`);
console.log(`${dep.depName}:${dep.currentValue}`.padEnd(66), outs.join("; ") || "(no update)");
}
'
echo
echo "full log: $WORK/renovate.log"
a:
image: percona/percona-postgresql-operator:2.7.0-ppg17.5.2-postgres
b:
image: percona/percona-postgresql-operator:2.7.0-ppg17.5.2-postgres-gis3.3.8
c:
image: percona/percona-postgresql-operator:2.6.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment