From 2ef3e2be876815596d365a3f003a94aa84f92af7 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sat, 5 Sep 2026 18:15:51 +0200 Subject: [PATCH] ci: check the preset the way the INHERITED path reads it, not only as repo config On 2026-09-03 a preset change stopped Renovate on all 117 organisations, and this lane was green throughout. config.js sets inheritConfig, so Renovate reads default.json through validateConfig('inherit', ...) -- a different validator from the one step 3 runs, and stricter, because it does NOT apply the migration a preset gets. A plain string `description` is legal in a preset and rejected when inherited: Configuration option `description` should be a list (Array) Configuration option `packageRules[0].description` should be a list (Array) Renovate then opens "Action Required: Fix Renovate Configuration" on every repository in the organisation and stops producing updates there. Step 3 passed the whole time because it validates default.json as REPO config -- the shape Renovate no longer reads it in. Nothing else noticed either: a per-repository config error is an ISSUE, not a failed job, so the Renovate workflow run reported success while every repository it touched was refusing its own configuration. Step 4b checks the one rule that has actually bitten. Step 4c is its self-test, built on a config shaped exactly like the one that caused the outage -- the same reason step 4 exists for step 3: a check whose subject quietly stops being checked is worse than no check, because it turns an absence into assurance. Verified both directions before rollout by extracting the two `run:` bodies exactly as YAML de-indents them and executing them: green on a real default.json, and red on that file with its descriptions turned back into strings, naming every offending field. actionlint clean. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e8ba36..a7981e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,80 @@ jobs: fi echo "self-test ok: the validator rejects what it should" + # 4b. PROVES: this preset survives the INHERITED path, which is a + # different validator from step 3 and stricter. + # + # config.js sets inheritConfig, so Renovate reads default.json + # through validateConfig('inherit', ...) -- and that runs WITHOUT + # the migration applied to a preset. A plain string `description` + # is legal in a preset, migrated to a list, and REJECTED when + # inherited: + # + # Configuration option `description` should be a list (Array) + # Configuration option `packageRules[0].description` should be a + # list (Array) + # + # Renovate then opens "Action Required: Fix Renovate Configuration" + # on EVERY repository in the organisation and stops producing + # updates there. On 2026-09-03 that landed on all 117 organisations + # at once, and step 3 above passed the whole time, because it + # validates default.json as REPO config -- the shape Renovate no + # longer reads it in. + # + # The failure is silent twice over: the Renovate workflow run still + # reports success, since a per-repository config error is an issue + # rather than a failed job. + # + # DOES NOT PROVE: that nothing else differs between the two + # validators. This checks the one rule that has actually bitten. + # If another surfaces, add it here rather than to a person's memory. + - name: The preset survives the inherited path + run: | + set -euo pipefail + [ -f default.json ] || { echo "no default.json -- nothing to check"; exit 0; } + python3 - default.json <<'CHECK' + import json, sys + bad = [] + d = json.load(open(sys.argv[1])) + if isinstance(d.get("description"), str): + bad.append("description") + for i, r in enumerate(d.get("packageRules") or []): + if isinstance(r.get("description"), str): + bad.append("packageRules[%d].description" % i) + if bad: + print("REJECTED on the inherited path:") + for b in bad: + print(" %s is a string; it must be a list (Array)" % b) + print("A string is fine in a preset and fatal when inherited.") + sys.exit(1) + print("ok: every description is a list") + CHECK + + # 4c. PROVES: step 4b can still fail, on a config shaped exactly like + # the one that caused the outage. + - name: The inherited-path check still rejects a string description + run: | + set -euo pipefail + bad="$(mktemp -d)" + printf '{"packageRules":[{"description":"a string","matchUpdateTypes":["minor"]}]}\n' \ + > "$bad/default.json" + if python3 - "$bad/default.json" <<'CHECK' + import json, sys + d = json.load(open(sys.argv[1])) + if isinstance(d.get("description"), str): + sys.exit(1) + for r in d.get("packageRules") or []: + if isinstance(r.get("description"), str): + sys.exit(1) + sys.exit(0) + CHECK + then + echo "SELF-TEST FAILED: a string description was accepted." + echo "Step 4b therefore proves nothing. Fix this before trusting it." + exit 1 + fi + echo "self-test ok: a string description is still rejected" + # 5. PROVES: every preset named in a TOP-LEVEL `extends` exists and is # parseable JSON -- the one case the validator provably skips # (measured, step 3). Every repository in the fleet carries