Renovate Operations
Operational guide for the self-hosted Mend Renovate CE worker, and specifically for the one piece of its state that is not GitOps-managed: the cache PVC.
Quick Reference
Section titled “Quick Reference”| Property | Value |
|---|---|
| Namespace | renovate |
| Workload | deploy/renovate-ce |
| CLI version of record | 43.202.1 |
| Cache root (preserved) | /tmp/renovate/cache |
| Go module cache | /tmp/renovate/cache/others/go/pkg/mod |
| Repo config | .github/renovate.json |
The cache-PVC toolchain purge
Section titled “The cache-PVC toolchain purge”Symptom
Section titled “Symptom”A Renovate-authored gomod pull request arrives with a go.sum that does not verify, and the
worker logs an artifact update failure whose stderr names stdlib packages:
context: package context is not in std (/tmp/renovate/cache/others/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.5.linux-arm64/src/context)errors: package errors is not in std (…/toolchain@v0.0.1-go1.26.5.linux-arm64/src/errors)fmt: package fmt is not in std (…/toolchain@v0.0.1-go1.26.5.linux-arm64/src/fmt)The identifying signature is a toolchain directory that is missing its src/ tree while Go
still treats it as complete. .ziphash is Go’s “downloaded and verified” marker. Go reads it,
concludes the extracted directory is complete, and never re-extracts — so a half-empty extraction
is trusted permanently.
Judge intactness by the src/ tree, never by the presence of the .zip. Go deletes the
archive once it has extracted it successfully, so a healthy toolchain has no .zip either.
Measured 2026-09-08 during the purge: .zip was absent for both the poisoned go1.26.5 and
the intact go1.26.6, while .ziphash was present for both — neither file discriminates. The
src/ tree does: go1.26.5 at 14 MB with only bin and go.env and no src at all, against
go1.26.6 at 69 MB with a 49-entry src/ tree. (Phase 1 recorded the intact entry at 236 MB on
2026-08-31. The two size readings disagree and the disagreement is unexplained, which is a
further reason to test for src/ rather than for a size threshold.)
This is not a missing postUpdateOptions / gomodTidy step. go get -t ./... already runs;
it fails on the broken toolchain. Adding go mod tidy adds a second command that fails
identically.
Why it survives every cleanup pass
Section titled “Why it survives every cleanup pass”mendRnvWorkerCleanupDirs is set to /tmp/renovate/repos, /home/ubuntu
(argocd/cluster-app/templates/renovate.yaml), which deliberately preserves
/tmp/renovate/cache — that preservation is the cache PVC’s whole purpose. So the corrupt
extraction outlives every mendRnvWorkerCleanup: always pass, indefinitely.
Scope warning — read before running anything
Section titled “Scope warning — read before running anything”This is cache maintenance inside a PVC. It is NOT an edit to an ArgoCD-managed object.
No kubectl apply is involved and none is permitted. The PVC is shared by four repositories, so
one rm against the wrong path degrades the cache for all of them.
The go1.26.6 entry sitting beside the poisoned one is intact and MUST NOT be removed. It is
the toolchain that apps/hl-assets-uploader/go.mod now names, and removing it converts a
zero-cost fix into a full toolchain re-download at best.
Procedure
Section titled “Procedure”-
Record the before state. Capture both listings verbatim; they are the evidence the purge is recorded against.
Terminal window kubectl --context fzymgc-house -n renovate exec deploy/renovate-ce -- sh -c 'du -sh /tmp/renovate/cache/others/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.*.linux-arm64 2>/dev/nullls -l /tmp/renovate/cache/others/go/pkg/mod/cache/download/golang.org/toolchain/@v/' -
Confirm the signature before removing anything. Proceed only if
toolchain@v0.0.1-go1.26.5.linux-arm64has nosrc/directory. That absence is the whole predicate. Do not additionally require its.zipto be missing — a healthy entry has no.zipeither, for the reason given in step 3. -
Confirm the intact sibling is present — by its
src/tree, not by its.zip. The extractedtoolchain@v0.0.1-go1.26.6.linux-arm64directory must carry asrc/tree:Terminal window kubectl --context fzymgc-house -n renovate exec deploy/renovate-ce -- sh -c 'd=/tmp/renovate/cache/others/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.6.linux-arm64if [ -d "$d/src" ]; thenecho "src/: PRESENT ($(ls "$d/src" | wc -l) entries)"elseecho "src/: ABSENT"fi'If
src/is absent, the purge scope is wider than this procedure and the directive bump costs a download rather than nothing — stop and re-measure.Do not test
.zippresence here. An earlier revision of this step requiredv0.0.1-go1.26.6.linux-arm64.zipand its.ziphashto both be present. That predicate was over-strict and would have halted the 2026-09-08 purge on a demonstrably healthy cache: the.zipwas absent for both toolchains, because Go removes the archive after a successful extraction..zip-presence therefore has no power to tell a poisoned entry from an intact one, and it was rejected as an instrument for that reason.src/-presence is the discriminating property, and it is the one this step tests. -
Remove the poisoned extraction and its marker — those two paths only.
Terminal window kubectl --context fzymgc-house -n renovate exec deploy/renovate-ce -- sh -c 'rm -rf /tmp/renovate/cache/others/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.26.5.linux-arm64rm -f /tmp/renovate/cache/others/go/pkg/mod/cache/download/golang.org/toolchain/@v/v0.0.1-go1.26.5.linux-arm64.ziphash'Never glob the version.
toolchain@v0.0.1-go1.26.*reaches the intactgo1.26.6entry. -
Record the after state with the same two commands from step 1, and re-run step 3’s
src/check to confirm thego1.26.6entry came through the purge intact. -
Record the outcome as a timestamped reading, not as a cleared state. See below.
Running the procedure a second time is safe: step 2 finds the poisoned directory already absent
and the procedure stops there, leaving the intact go1.26.6 entry untouched.
Recording the outcome
Section titled “Recording the outcome”Do not record this defect class as closed. A green reading has a half-life of one Renovate
cycle (schedule: "before 4am on monday"), and the defect has recurred repeatedly after each
hand repair — pull requests #1819, #1886, #1929 and #2000, with #2000 re-breaking go.sum six
hours after #1988’s hand repair. Only hand repairs (#1814, #1988, #2004) have ever written a
valid go.sum for this module.
Record instead:
- the UTC timestamp of the purge;
- the before and after listings from steps 1 and 5;
- the re-observation condition — on the next Renovate-authored
gomodpull request,go mod verifysucceeds on that pull request’s head commit and re-running module resolution leavesgo.sumunchanged. Record the pull request number and the UTC timestamp of that reading.
Diagnosing a red result
Section titled “Diagnosing a red result”Before concluding anything from a red go.sum check, compare three refs: the branch tip,
origin/main, and refs/pull/N/merge. GitHub’s pull_request event builds the merge of head
into base, so a pull request whose diff contains no Go changes at all can go red because it
inherits main’s broken go.sum.
The repo-side half of the fix
Section titled “The repo-side half of the fix”apps/hl-assets-uploader/go.mod declares go 1.26.6 and carries no toolchain directive.
The pod ships go1.26.3 with GOTOOLCHAIN=auto, so Go must fetch whatever the go directive
names; naming 1.26.6 points it at the intact cached toolchain. A toolchain directive is
deliberately absent — that construct is for needing a toolchain newer than the go line
requires, which is not this situation, and it is a second version-bearing line that can drift.
Renovate tracks the go directive as a dependency in its own right (depType: golang,
datasource: golang-version), so it will subsequently propose 1.26.x bumps for that line. Those
are ordinary updates, not a regression of this repair.
What this repair does NOT close
Section titled “What this repair does NOT close”The same cache PVC is shared by three other repositories, which were measured in Phase 1 with
their own artifactErrors in a 30-day window:
| Repository | artifactErrors |
|---|---|
fzymgc-house/node-bootstraps |
317 |
seanb4t/engram |
312 |
holomush/holomush |
19 |
Their causes are not established, and they are out of scope here. Do not read a green
go.sum on this repository as evidence about any of them.
Cooling-off: what actually gates a pull request
Section titled “Cooling-off: what actually gates a pull request”| Key | Committed value | Default |
|---|---|---|
prCreation |
not-pending |
immediate |
internalChecksFilter |
deliberately unset | strict |
internalChecksFilter is already strict, so writing it asserts nothing
Section titled “internalChecksFilter is already strict, so writing it asserts nothing”At the CLI version of record (43.202.1) internalChecksFilter defaults to strict. Neither
.github/renovate.json nor the account-wide config.js sets it, so strict has been in force the
whole time. Setting it explicitly changes no behaviour, and it is not a globalOnly option, so
it needs no edit to argocd/cluster-app/templates/renovate.yaml either.
It is therefore deliberately absent from the repo config. A key written at its own default value is a control that asserts nothing, and shipping one under a requirement about cooling-off would be false assurance dressed as configuration.
What strict does, and what it does not do
Section titled “What strict does, and what it does not do”Under strict Renovate still selects the highest candidate release, even when every candidate
is inside its minimumReleaseAge window. All strict does is flag that update
pendingChecks = true. Whether the branch is skipped is decided by prCreation, not by the filter.
That is the mechanism behind an instantly-green renovate/stability-days check. Under the former
default prCreation: immediate the branch and the pull request were created regardless, and the
stability check was published green or yellow after the fact. A green renovate/stability-days
therefore reports “the release age had already elapsed by the time Renovate looked”. It never
gated anything, and it must not be read as evidence that a cooling-off period was observed.
There is no escape hatch, because its input is constant here
Section titled “There is no escape hatch, because its input is constant here”prCreation: not-pending skips branch creation while every candidate release is still pending.
That much is unchanged and still true.
What turns not-pending into a delay-and-then-proceed rule elsewhere is getBranchStatus()
going from yellow to green or red once a branch has statuses on it. In this repository that
input is constant. Every push: trigger in .github/workflows/ is scoped to branches: [main]
except build-lightningstream.yml, which is tags:-only, and the pull_request: workflows cannot
fire because the pull request is precisely what is being withheld. A push to a renovate/* branch
therefore produces zero commit statuses, GitHub’s combined-status API answers pending on an
empty set, and Renovate maps that to 'yellow' forever.
The effective withholding period is therefore one full Renovate cycle — the next
before 4am on monday window — and not an hours figure. prNotPendingHours is consequently
deliberately unset: within-window re-evaluations are all under 4 hours apart and cross-week
re-evaluations are all at least 168 hours apart, so every value between the schedule’s own
granularity and that cycle length produces byte-identical behaviour. A tuned integer that no
reachable state can distinguish from any other is false assurance dressed as configuration —
the same reasoning applied to internalChecksFilter two sections above. It was committed as 72
until 2026-09-08 and deleted on measurement, not on taste.
The floor a reviewer should actually read is minimumReleaseAge on the packageRule that matches
the dependency, not anything on this page’s first table. Which rule matches is decided by array
order — see Rule ordering
and the rule bodies in .github/renovate.json; the headline floors are 7 days on major updates and
on the LiteLLM data plane, and 3 days on the all-minor-patch rollup.
RESIDUAL: this claim is derived from three preconditions, all of which can change by ordinary edit.
Gated on any workflow ever producing a commit status on a renovate/* branch, the top-level
schedule gaining a second window, or prCreation ceasing to be not-pending — on any of those
this section must be re-derived and an explicit escape hatch reconsidered.
Nothing in CI goes red when one of them happens; the re-derivation is a reviewer’s job.
Interaction with automerge
Section titled “Interaction with automerge”prCreation changes when a pull request appears, not whether it automerges. It is not an
automerge control and it does not override automerge, automergeType or platformAutomerge.
It is still a genuine, if partial, brake: an update that gets no branch also gets no automerge, because there is nothing to merge. Expect visible pull-request volume to fall after this change lands. That is this setting working, not a coverage regression and not a Renovate fault.
OQ-4: does the CE wrapper pass repo config through to the worker
Section titled “OQ-4: does the CE wrapper pass repo config through to the worker”The question. Mend Renovate CE wraps the OSS worker. OQ-4 asks whether a key committed in
.github/renovate.json actually reaches that worker. It is a question about our own
configuration being inert, not about what Renovate does once it holds the value — which is why it
survives the “do not test what we do not own” trim.
Why it is aimed at prCreation. It was originally aimed at internalChecksFilter. Aimed there
it is vacuous: that option is already strict by default, so a value passed through faithfully and
a value the CE wrapper silently dropped are byte-identical in the worker’s resolved config — the
reading could never have told the two apart. prCreation is committed as not-pending against a
default of immediate, so the two outcomes are distinguishable. Any repo-config key whose committed
value differs from its default would serve; prCreation is the one this phase changed.
Where the resolved config surfaces. The worker runs at LOG_LEVEL=info and says so on every
run. The dedicated resolved-config dump is a debug-level line and is not ingested at that level.
Measured 2026-09-08 over a 7-day window, the resolved config nevertheless reaches
default.otel_logs, embedded in the payload of higher-level lines — getChangeLogJSON error at
level 50 and Renovate is exiting with a non-zero code due to the following logged errors at level
30, 20 such lines in seven days for this repository. That makes the instrument opportunistic: it
yields a reading only on a cycle that logged one of those lines. In exchange it needs no
configuration change, and specifically no LOG_LEVEL=debug edit to the account-wide
argocd/cluster-app/templates/renovate.yaml, which would restart the pod for three organisations.
The instrument. It still extracts prNotPendingHours by name, and that is deliberate: the key
was committed as 72 until 2026-09-08 and is no longer committed at all, so its absence from a
recent row is the expected reading rather than a regression, while the historical rows that carry it
are what date the change. The query text is left exactly as it was for that reason — it reads log
bodies already written, and editing it would break the comparison against the baseline below.
kubectl exec -n clickstack "$(kubectl get pod -n clickstack \ -l clickhouse.com/role=clickhouse-server -o name | head -1 | cut -d/ -f2)" \ -- clickhouse-client --query " SELECT Timestamp, JSONExtractString(Body, 'logContext') AS logContext, extract(Body, '\"prCreation\":\"[a-z-]+\"') AS prCreation, extract(Body, '\"prNotPendingHours\":[0-9]+') AS prNotPendingHours FROM default.otel_logs WHERE ResourceAttributes['k8s.pod.name'] LIKE 'renovate-ce-%' AND JSONExtractString(Body, 'repository') = 'fzymgc-house/selfhosted-cluster' AND Body ILIKE '%prCreation%' AND Timestamp > now() - INTERVAL 7 DAY ORDER BY Timestamp DESC LIMIT 20"The label selector is clickhouse.com/role=clickhouse-server. It resolves to the live ClickHouse
pods cs-clickstack-clickhouse-clickhouse-0-0-0 and cs-clickstack-clickhouse-clickhouse-0-1-0.
The timestamp column is Timestamp, never TimestampTime.
How to read the result.
| Result | Reading |
|---|---|
"prCreation":"not-pending" |
The option reached the worker. OQ-4 is answered in the affirmative for this key. |
"prCreation":"immediate" |
Either the CE wrapper dropped the repo-config key, or the row predates the merge. Compare the row’s Timestamp against the merge commit before concluding anything. |
| No rows | No config-bearing line was logged in the window. This is not a negative — widen the window or wait for another cycle. |
Baseline, measured 2026-09-08, before this phase merged. The query above returned 14 rows for
this repository spanning 2026-09-04 15:24:33Z to 2026-09-07 11:28:42Z UTC, carrying exactly one
distinct value each: "prCreation":"immediate" and "prNotPendingHours":25 — both pre-change
defaults. The same payloads carry "prConcurrentLimit":10, which is the value committed in
.github/renovate.json, establishing that this pathway does relay repo config rather than only
built-in defaults. Both halves of the discrimination are therefore already on the record, and a
post-merge reading of not-pending is meaningful rather than ambiguous.
Taking the reading. Passively, on the first natural cycle after this phase merges to main;
Renovate cycles this repository roughly every two hours. Do not arm a live experiment for it.
Record the UTC timestamp and the logContext of the row that answered it.
If the key is absent. Record it as a measured negative with its timestamp, not as an
inconclusive result, and state the fallback: with prCreation dropped by the wrapper the
cooling-off is not enforced at all, renovate/stability-days stays decorative, and the remaining
levers are minimumReleaseAge plus withholding automerge on the affected rules. Do not assume the
setting took.
Editing .github/renovate.json
Section titled “Editing .github/renovate.json”The repo config’s rule description fields are deliberately one or two sentences each. The
non-obvious mechanics that a short description cannot carry, and that a future editor would
otherwise break, live here.
Two configs resolve, and only one of them is editable from this repository
Section titled “Two configs resolve, and only one of them is editable from this repository”The account-wide config.js embedded in argocd/cluster-app/templates/renovate.yaml contributes
the first packageRules entries in the effective list, before anything in
.github/renovate.json runs. That file is fenced from edit — it is shared by every repository
the CE worker serves, and changing it restarts the pod for three organisations.
Two consequences bind every rule written in the repo config:
- Its
all-minor-patchrollup grouping is asserted for every minor/patch update in this repository before any repo rule is evaluated. A repo rule can only re-group or override what the rollup already decided; it can never remove it. This is why the path split needs a catch-all bucket rather than an enumeration of the surfaces we happen to have today. - The root config sets
automerge: true,platformAutomerge: trueandreviewers: []. An attended rule must therefore set both automerge keys and an explicit reviewer list. Disabling one automerge key leaves the other in force; omittingreviewersleaves nobody named.
Order is behaviour
Section titled “Order is behaviour”Renovate’s applyPackageRules merges later matches over earlier ones, so a rule’s index in the
array is part of what it does. Ordering constraints currently in force:
| Rule | Must sit… | Because |
|---|---|---|
terraform core |
after terraform providers |
it peels hashicorp/terraform itself back out of the providers group |
the MAJOR guard — matchUpdateTypes: ["major"], no manager matcher |
after every entry in the array that sets automerge: true, the last of which is python dependencies, and before the mend-renovate-ce guard and the attended tail rules |
every one of those entries declares no matchUpdateTypes, so last-match-wins hands each of them the major lane for the managers it names — it is the mechanism, not the four argocd-reaching groups, that overrides packageRules[0]. Sitting after all of them makes this guard the last rule that can decide a major’s posture. A later rule weakening any of the five keys the guard sets — automerge, platformAutomerge, minimumReleaseAge, reviewers and additionalBranchPrefix — reopens the lane, and nothing asserts the ordering; this table is the record of it. The mend-renovate-ce guard and the attended tail rules must still override it for the subtrees they name. Since 2026-09-09 it also carries a constant additionalBranchPrefix, major-. A branch is branchPrefix + additionalBranchPrefix + branchTopic and for a grouped update the topic is the groupSlug, so that constant separates the attended major lane from every other lane while the slug is deliberately left to the tree — the per-tree blast-radius split survives, and no group key was added. The attended argocd|major and the unattended kubernetes|digest updates on traefik.yaml share a slug and differ only by that prefix; nothing asserts it |
the pre-1.0 guard — matchUpdateTypes: ["minor"] plus matchCurrentVersion, no manager matcher |
after container images (ghcr), container images (docker hub), helm charts and argocd manifests, after the five blast-radius buckets and the negation-written catch-all beside them, and after every entry that sets automerge: true — the last being python dependencies — and before the mend-renovate-ce guard and the attended tail rules |
it names no manager scope since 2026-09-09, so its subject is the mechanism rather than one lane: the four groups are otherwise the last word on a 0.y.z minor, sitting after the last automerge: true entry means no later rule can loosen what this one sets, and sitting after the buckets is what makes its own groupName the last one to decide the branch rather than an inert key a bucket overwrites. Nothing asserts its shape or its position: a later rule that weakens the four posture keys this guard sets reopens the lane, and a bucket after it overwrites its group. This table is the record of both; the mend-renovate-ce guard and the attended tail rules must still override it |
mend-renovate-ce guard |
after container images (docker hub), helm charts and argocd manifests |
all three automerge that chart on a 2-day floor |
key-affecting encoders, clickstack observability plane, litellm data plane |
at the tail, after every general group | they are the attended overrides; earlier is silently overridden |
litellm data plane |
last | it overrides the container images (ghcr), helm charts and argocd manifests groups with both automerge keys off and a named reviewer, and nothing after it may loosen that — a LiteLLM compromise ships as an ordinary version bump |
| the six blast-radius buckets | before the three attended rules | the buckets decide which PR a dependency lands in; the attended rules still decide its posture |
the custom.regex + ansible/** guard |
after the six blast-radius buckets, before the three attended rules | it must override the grouping-only ansible/** bucket, and must not overwrite an attended rule’s posture |
the four-pin extractVersion rule |
anywhere — its position carries nothing | it sets no merge-posture key, so no later rule can overwrite anything it decides. Its SCOPE is load-bearing instead: it is matched by matchDepNames only, and adding a matchManagers or matchFileNames to it would strip the prefix from five pins that hold theirs deliberately |
lightningstream (lockstep) |
anywhere — its position carries nothing | it sets the same four posture keys the ansible guard sets, so neither can loosen the other, and no attended tail rule names this dependency. It carries posture at all only because its second path, .github/workflows/build-lightningstream.yml, sits outside the ansible/** the guard matches |
The 27 argocd charts the manager repair made live
Section titled “The 27 argocd charts the manager repair made live”Repairing the argocd manager’s managerFilePatterns from a minimatch glob to a /-delimited
regex took it from 0 of 3922 tracked files to 391, and with them 28 chart dependencies
across 25 Application manifests — 27 distinct chart names, because opentelemetry-collector
is pinned in two sources of the same Application. That repair is correct in itself. Its side effect
was not: only mend-renovate-ce carried a downstream guard, so the rest — traefik, metallb,
vault, velero, cloudnative-pg, nats and the others enumerated in this phase’s
05-evidence-argocd-dep-surface.md — resolved to automerge: true, platformAutomerge: true, a
2-day floor and reviewers: [] for MAJOR bumps as well as minor ones. packageRules[0]
says a major is attended; it was overwritten by four later groups that match the argocd manager
and declare no matchUpdateTypes, and a rule that is overwritten asserts nothing.
That consequence is one-way. Such a bump merges, ArgoCD syncs it with prune: true, and reverting
this file undoes neither a pruned CRD nor a migrated database — so the disposition was the
operator’s to make, not an agent’s.
The 27 split 15 and 12, and the major ruling below reaches only the 15. Renovate derives
updateType from the major field, so no update to a 0.y.z pin is ever classified major and
matchUpdateTypes: ["major"] never matched one. 15 charts carry a version at or above 1.0.0
and therefore have a reachable major lane. The 12 pinned below 1.0.0 are cloudnative-pg
(0.28.3), engram (0.14.0), gha-runner-scale-set (0.14.1), gha-runner-scale-set-controller
(0.14.1), metallb (v0.15.3), opentelemetry-collector (0.154.0), router-hosts-operator
(0.13.0), temporal (0.73.1), temporal-worker-controller (0.11.0), valkey-operator (0.3.0),
vault (0.33.0) and vector (0.52.0). The split is a reading, not an inference: the postures
were taken by resolving the committed config last-match-wins, per update class, rather than by
reading the rules in array order.
Ruling, 2026-09-08: add one post-position major guard scoped to the argocd and kubernetes
managers. Three options were put: restrict all four group rules to minor/patch; add one
post-position guard; or accept the exposure with a written exit criterion. The second was chosen
because its blast radius equals the gap. Restricting the four groups would have re-armed
packageRules[0] for every container-image and Helm-chart major in the repository — a change well
outside argocd/** for a gap that is inside it — and, as scoped, would still have left the
core infrastructure components and security components groups automerging traefik, metallb
and vault at five days. Accepting the exposure would have left 15 charts on unattended MAJOR
automerge against an ArgoCD instance running prune: true — 15 rather than 27, because the other
12 have no major lane for any rule to reach, which is the gap the second ruling below closes.
kubernetes is named alongside argocd because the two managers’ managerFilePatterns are
byte-identical — both /^argocd/.*\.ya?ml$/ — so including it reaches nothing outside argocd/**
while closing the container-image half of the same surface, which the argocd manifests group
(matchManagers: ["argocd", "kubernetes"]) would otherwise have left on automerge.
Exit criterion. The guard is redundant, and should be deleted, once the four groups above carry
their own matchUpdateTypes restriction — at which point packageRules[0] is again the last match
for a major on this surface. It is written into the rule’s own description in the
Gated on … form, beside the rule it is about, so deleting the rule deletes its exit criterion.
Ruling, 2026-09-09: the MAJOR guard drops its manager matcher entirely and moves after every automerging entry. The 2026-09-08 guard closed one lane. This one closes the mechanism.
What was measured, and how. The same reading as the split above — resolving the committed
config last-match-wins, per update class — was taken for the first time over manager families the
earlier readings had not sampled. Three of them have live inputs in this tree, and all three were overriding
packageRules[0] outright:
| Lane | Real pin the reading used | Posture it resolved to before 2026-09-09 | The group that overrode packageRules[0] |
|---|---|---|---|
terraform major under tf/** |
cloudflare/cloudflare at 5.19.0 in tf/cloudflare/versions.tf |
automerge: true, platformAutomerge: true, a 3-day floor, reviewers: [] |
terraform providers |
ansible-galaxy collection major |
kubernetes.core at 6.4.0 in ansible/requirements.yml |
automerge: true, platformAutomerge: true, a 5-day floor, reviewers: [] |
ansible collections |
pep621 dependency major |
ansible at 14.3.1 in pyproject.toml |
automerge: true, platformAutomerge: true, a 3-day floor, reviewers: [] |
python dependencies |
The terraform core rule is not a mitigation for the first row: it matches
/^hashicorp/terraform$/ only — the Terraform binary itself — so every provider major rode the
terraform providers group. Each of those three groups sets automerge: true, declares no
matchUpdateTypes, and sits later in the array than packageRules[0], which is the entire
mechanism: it is not a property of the argocd surface, it is a property of any group that automerges
and does not restrict its update types.
Why nothing read red over this. Every earlier reading was drawn from the region the argocd-scoped guard protects. A reading cannot report a lane nobody sampled, so four manager families resolved to unattended MAJOR automerge unnoticed.
The premise the 2026-09-08 rejection of option one rested on was false. That round rejected
“restrict all four group rules to minor/patch” partly because it would be “a change well outside
argocd/** for a gap that is inside it”. The gap was not inside argocd/**. The reasoning was
sound on the facts then on the record — the 15-chart exposure was real, and the reading that
established it was correct as far as it went — and the fact it rested on was wrong, because nobody
had yet driven the resolver over a manager family outside that surface. Both halves stay on the
record: the 2026-09-08 paragraphs above are left as they were written, and this is what the later
measurement did to them.
The remedy chosen is neither of the two options that round put. It takes the second one further.
Rather than adding a matchUpdateTypes restriction to the three overriding groups — which fixes
three instances and leaves the mechanism intact for the sixth group somebody adds tomorrow — the
guard loses matchManagers altogether and moves to sit after every entry in the array that
sets automerge: true, the last being python dependencies. packageRules[0]’s headline claim is
then true estate-wide rather than lane by lane, and a new automerging group added later is closed by
the guard on the day it is written. kubernetes is no longer named alongside argocd because no
manager is named at all.
This supersedes the 2026-09-08 exit criterion above. That one said the guard becomes redundant
once the four groups carry their own matchUpdateTypes restriction; a manager-agnostic guard is
not redundant at that point, because it is protecting managers those four groups never matched. The
exit criterion now carried in the rule’s own description, in the Gated on … form, is every
entry that sets automerge: true carrying its own matchUpdateTypes restriction excluding major.
What changes on a real pull request. On the next Renovate cycle a MAJOR bump of a Terraform
provider under tf/**, of an Ansible Galaxy collection in ansible/requirements.yml, or of a
Python dependency in any of the three tracked pyproject.toml files (repository root,
tools/dns-aaaa-publish/, tools/dns-network-handover/) stops
automerging: it opens, waits the guard’s 7-day floor in place of the 3- and 5-day floors those
groups carried, and names fzymgc as reviewer. Every other manager’s major already resolved
automerge: false through packageRules[0]; what changes for those is that the pull request now
names a human where reviewers was previously [], so a major no longer opens attended and
unassigned. Nothing starts automerging — the fifteen pre-existing fixture rows resolve
byte-identically after the change, which is asserted by the gate rather than assumed — and no
minor or patch lane is touched: the guard carries matchUpdateTypes: ["major"] and can be the
last match for nothing else.
Ruling, 2026-09-08: option-a — attend the MINOR lane, and only the MINOR lane, for the twelve
pre-1.0 pins. The major guard above is keyed on a proxy for the property it names, and below
1.0.0 the proxy and the property come apart: for a 0.y.z chart the y bump is the breaking
one. Three options were put to the operator: (a) one post-position guard attending the minor lane
for 0.y.z pins; (b) attend both the minor and the patch lane for them; (c) accept the exposure
with a written exit criterion. The operator selected the option the checkpoint presented as
recommended and supplied no free-text reasoning of their own, so no operator quotation exists and
none is manufactured here — the ruling is the selection. The recommendation’s stated reasoning,
which the operator accepted by selecting it, was that option A is the exact semantic analogue of
the major ruling above — that ruling attends “the one bump this repository cannot take back” above
1.0.0, and below 1.0.0 that same bump is the y bump — and that its blast radius equals the
measured gap.
The resulting guard is the one rule in the array carrying matchCurrentVersion — named here by
that shape rather than by an array position, because a position is precisely what a later edit
moves, and this sentence had already been made false once by one. Its neighbourhood is the property
that matters: it sits after the five blast-radius buckets and the catch-all beside them, so its
own group name is the last one to decide the branch, and before the attended rules at the tail,
whose own postures and groups must still win. The mend-renovate-ce guard now sits before it
rather than after, and nothing is lost by that: the two are provably disjoint, because that guard’s
subject is pinned above 1.0.0 and this rule’s matchCurrentVersion reaches only 0.y.z. Its
contents: no manager matcher at all — the rule deliberately declares no matchManagers key, so
Renovate applies it to every manager it runs, and the resolver skips the manager test for it
entirely — plus matchCurrentVersion: "/^v?0\\./", matchUpdateTypes: ["minor"],
automerge: false, platformAutomerge: false, minimumReleaseAge: "7 days",
reviewers: ["fzymgc"], groupName: "pre-1.0 pins (minor)" and groupSlug: "pre-1-0-minor" — the
last two added on 2026-09-08 for the reason the paragraph below gives, and both renamed on
2026-09-09 off the argocd-specific names they were given then, because the rule had stopped being
about argocd. The 2026-09-08 provenance of that key pair stands: those keys really did arrive that
day, and what changed on 2026-09-09 is what they say. The manager list this sentence used to
enumerate was removed by the 2026-09-09 pre-1.0 ruling recorded below. matchCurrentVersion, not
matchCurrentValue: the first matches the resolved or locked version and the second the raw string
in the file, and the optional leading v is load-bearing because metallb is pinned v0.15.3 and
the resolved form may present either way. Exit criterion: the guard becomes redundant, and
should be deleted, once those charts reach 1.0.0, at which point the major guard is their last
match again. It is written into the rule’s own description in the Gated on … form, beside the
rule it is about.
Renovate does not merge dependencies, it merges branches — and until 2026-09-08 this guard did
not name one. Measured against renovate@43.202.1’s own
dist/workers/repository/updates/generate.js: a branch’s automerge is
config.upgrades.every((upgrade) => upgrade.automerge) (line 239), so one attended upgrade in a
group turns automerge off for the whole branch; the branch’s reviewers come from
{ ...config, ...config.upgrades[0] } (lines 214–218), so they are taken from one upgrade
rather than merged across the group; and minimumReleaseAge is unaffected by either, because a
branch that is not wholly pending drops its still-pending upgrades before any of this runs
(lines 120–122), so the 7-day floor holds per upgrade.
What that meant while the guard set an attended posture and named no group of its own: the safety
property survived, but by every() rather than by the guard. Every pre-1.0 minor joined the
argocd-minor-patch branch, so once one of them was open, automerge went false for every
co-grouped argocd minor and patch — traefik’s and velero’s minors, and the pre-1.0 patch lane
this section records below as option A’s deliberate scope — and the reviewer the ruling named was
delivered only by whichever upgrade happened to sort first. What changed: the guard now carries its
own groupName/groupSlug and sits after the blast-radius buckets, so its group is the last one to
decide the branch — the shape the attended tail rules (key-affecting encoders,
clickstack observability plane, litellm data plane) already had. The pre-1.0 patch lane is once
again genuinely on the bucket posture, which is what the residual bullet below has always said and
is now true of the branch as well as of the rule.
Ruling, 2026-09-09: the pre-1.0 guard drops its manager matcher too. The 2026-09-08 pre-1.0
ruling scoped its guard to the argocd and kubernetes managers on exactly the premise the
2026-09-08 MAJOR scoping used — that the two managers’ managerFilePatterns are byte-identical, so
naming both closes both halves of one surface and widens nothing. The 2026-09-09 measurement that
falsified that premise for the MAJOR guard falsifies it for this one as well, and for the same
reason: it closes an instance, not the mechanism.
Resolving the committed config last-match-wins, a pre-1.0 terraform provider minor
resolved automerge=true, platformAutomerge=true, a 3-day floor and reviewers: [] into the
tf-minor-patch bucket, on firedRules [1,2,25] — the all-minor-patch rollup, the
terraform providers group and the tf/** blast-radius bucket. The pre-1.0 guard is absent from
that list entirely: a terraform update never reached a rule that named two other managers. And for
a 0.y.z pin the minor is the breaking bump, so this was the breaking class of four live
providers merging unattended on a three-day floor with nobody named.
Those four, and what each one owns:
| Pin | Declared in | Owns | currentVersion read from |
|---|---|---|---|
tailscale/tailscale |
tf/tailscale/versions.tf |
this network’s ACLs and its split-horizon DNS configuration | no lock file — 0.29.0 is the resolved form of a ~> 0.29 constraint |
hashicorp/tfe |
tf/hcp-terraform/versions.tf |
the Terraform Cloud workspaces every other root module runs in | tf/hcp-terraform/.terraform.lock.hcl, 0.80.0 |
app.terraform.io/fzymgc-house/routerhosts |
tf/router-hosts/versions.tf |
the router’s host entries | tf/router-hosts/.terraform.lock.hcl, 0.4.0 |
breml/uptimekuma |
tf/uptime-kuma/versions.tf |
the uptime monitors | tf/uptime-kuma/.terraform.lock.hcl, 0.4.0 |
And the census beyond tf/**, taken on 2026-09-09 rather than discovered later. The guard now
reaches every manager, so what it reaches was counted before it was widened: eight v0.
requires across the one tracked go.mod (apps/hl-assets-uploader/go.mod), four 0.-shaped
dependency ranges in website/package.json (@astrojs/starlight, sharp,
starlight-links-validator, starlight-llms-txt), and four
otel/opentelemetry-collector-contrib pins under ansible/ whose upstream versions are all
0.y.z. Those lanes are reached by the guard and asserted by no fixture row — a stated
coverage residual, in the same register as the helmv3 and pip_requirements entries under
Forward coverage below, and the reason the four rows this ruling added are tf/** rows rather
than one row per surface: the tf/** four are the pins whose unattended posture was measured.
The consequence, and it is a consequence rather than a benefit. A 0.y.z minor anywhere in
this tree now lands in one attended branch. The five blast-radius buckets exist because one
bad bump used to wedge four unrelated directories, and this group sits deliberately outside that
split: a pre-1.0 argocd chart minor and a pre-1.0 Terraform provider minor now share a branch, and
every() over that branch means one of them being open holds the other. That separation is real and
it is given up here knowingly, in exchange for a single last word on the pre-1.0 breaking class —
every update in that branch is attended, floored at seven days and delivered with a human named. The
group was renamed in the same edit for the same reason: a branch called after argocd charts that
carries a Terraform provider is the class of measurably-false record this phase exists to end.
What this ruling does not change: the patch lane, which option A left on the bucket posture
on 2026-09-08 and which is still there — the residual bullet below and its open ledger entry both
stand, now over pre-1.0 pins estate-wide rather than over twelve charts. The guard keeps
matchUpdateTypes: ["minor"] and its matchCurrentVersion, which is what stops it from becoming a
second manager-agnostic MAJOR guard; assertNothingAfterTheMajorGuardWeakensIt() requires exactly
one such rule and FATALs on two, so that mistake takes the gate down rather than passing quietly.
What the two guards deliberately do not do — both residuals, stated in the same register:
- The major guard changes no minor or patch posture, and it leaves
packageRules[0]still overridden for thekustomize,dockerfile,helm-valuesandhelmv3managers those four groups also match. The headline major control therefore remains partly inert elsewhere in the repository. Its exit criterion is the one written above, in the major guard’s owndescription. Superseded 2026-09-09, and left readable because it is the record of what was known then: the second sentence stopped being true when the guard droppedmatchManagers— it now reaches those managers, and every other one, sopackageRules[0]is no longer partly inert. The residual that replaces it is a coverage residual rather than a posture one, and it is registered underForward coveragebelow:helmv3andpip_requirementsmajors are reached by the guard and asserted by no fixture row. - The pre-1.0 guard attends the minor lane only, so the patch lane for those twelve charts is
untouched. Measured on 2026-09-08 by the same resolver:
cloudnative-pgat 0.28.3 moving to 0.28.4 resolves toautomerge: true,platformAutomerge: true, a 2-day floor andreviewers: []— unattended, into an Application runningprune: true. That is option A’s deliberate scope, not an oversight: option B would have closed it and was not chosen. Read the two lanes as independently postured only since the guard named its own branch. While it did not, both lanes shared theargocd-minor-patchbranch andevery()suppressed automerge across the whole bucket whenever a pre-1.0 minor was open — so the patch lane was not on the posture recorded here, and this bullet was true of the rule and not of the branch. Its exit criterion is the same one the guard carries in its owndescription— the twelve charts reaching 1.0.0, at which point the whole rule goes and normal major/minor/patch semantics resume.
Both are known, stated residuals of the surgical options chosen, not oversights, and each has an
open ledger entry in .planning/WINDOWS.md carrying an observable close condition. The postures
these rulings produce are not asserted anywhere; a reviewer re-derives them by resolving the rules
rather than reading them in order.
Ruling, 2026-09-09: the MAJOR guard carries a constant branch prefix, and a group of its own was rejected. Every ruling above settles which posture an update carries. None of them settles which branch it lands on, and the second question is not downstream of the first.
The mechanism. Renovate composes a branch name as branchPrefix + additionalBranchPrefix + branchTopic, and for a grouped update the topic is the groupSlug. Its unit of automerge and
of reviewer assignment is the branch, not the dependency: renovate@43.202.1
dist/workers/repository/updates/generate.js sets config.automerge = config.upgrades.every((upgrade) => upgrade.automerge) and builds the branch config as
{ ...config, ...config.upgrades[0], releaseTimestamp }. The first half fails safe — one
attended upgrade suppresses automerge for the whole branch. The second half does not: a branch takes
its reviewers from one upgrade, so a co-grouped update decides who is asked to look.
What was measured. Driving this repository’s own resolver over the committed config on 2026-09-09, two rows on the same manifest:
| Row | Resolved | Fired rules |
|---|---|---|
kubernetes|digest on argocd/cluster-app/templates/traefik.yaml |
automerge=true platformAutomerge=true minimumReleaseAge=2 days groupSlug=argocd-manifests reviewers=[] |
[4,9,11] |
argocd|major on the same file |
automerge=false platformAutomerge=false minimumReleaseAge=7 days groupSlug=argocd-manifests reviewers=["fzymgc"] |
[0,4,9,10,11,16] |
Both landed in argocd-manifests — one branch. So an unattended digest sorting first left an
attended traefik MAJOR carrying reviewers=[], and the human the 2026-09-08 ruling named was
delivered only by the accident of sort order.
The mechanism that was rejected, and why. Giving the MAJOR guard a groupName/groupSlug of
its own — the same shape the pre-1.0 guard received on 2026-09-08, one line, and the obvious move —
would have separated the major lane by collapsing every attended major in the estate into one
branch. That discards the per-tree blast-radius split, and that split is not decoration: it exists
because one bad bump once held five files across four unrelated top-level directories hostage for
about forty days. A constant additionalBranchPrefix buys the same separation and touches no
group at all, so an attended traefik major lands on renovate/major-argocd-manifests while a
traefik digest stays on renovate/argocd-manifests, and a Cloudflare provider major lands on
renovate/major-terraform-providers. Measured after the edit: all eight major fixture rows still
carry their own tree’s slug — argocd-manifests five times, terraform-providers,
ansible-collections and python-deps once each.
The two sibling guards took different remedies for a reason, not by inconsistency. The pre-1.0 guard’s lane had no slug describing it and needed one — nothing in the array named “pre-1.0 breaking changes”, so a group was the only way to give that lane a branch. The MAJOR guard’s lanes already had slugs, one per tree, and needed only to be told apart from the unattended updates sharing them. Same defect, different starting position, therefore different fix.
The value is deliberately a literal. Renovate would accept a Handlebars template here; keep it
a constant so the lane split is readable from the file without evaluating a template. Note that
renovate-config-validator accepts both forms and reports a bare file as global config — run it
to catch a malformed edit, never as evidence for the design.
Exit criterion. The prefix stops earning its keep if a branch’s reviewer list ever stops being a
function of which upgrade sorts first — if Renovate merges reviewers across a branch’s upgrades
rather than taking upgrades[0], the major lane may share a branch again.
The Ansible pins are reached by one manager, and the hazard is the minor lane
Section titled “The Ansible pins are reached by one manager, and the hazard is the minor lane”An annotated Ansible pin is extracted by the custom.regex manager and by nothing else. The
string matchManagers sees for a custom manager is custom.<customType> — here custom.regex,
with the prefix — and the name-anchored groups do not reach these dependencies either: the
annotations emit names like cilium, k3s-io/k3s and ghcr.io/kube-vip/kube-vip, which match no
matchPackageNames entry in this file.
The exposure is not the major lane. Major version updates require manual review carries no
manager, name or path matcher, so it already catches custom-manager majors at automerge: false
with a seven-day floor. The exposure is the minor lane: cilium_version: "1.19.4" moving to
1.20.0 is a MINOR under both docker and semver versioning, so without a guard it rides the
account-wide all-minor-patch rollup at automerge: true, platformAutomerge: true, a three-day
floor and nobody named — for the live CNI. The required context does not brake it: an Ansible-only
diff carries no Go and no lockfile, so pr-gates passes trivially.
The guard is scoped by manager and path. matchManagers: ["custom.regex"] alone also catches
the shell _VERSION dependencies and the Dockerfile deb dependency, which are guarded by the
encoders rule instead.
A # renovate: annotation must sit directly above the key it annotates. The matchStrings
regex requires adjacency; a blank line or an intervening comment silently un-annotates the pin,
and the config still validates clean. A pin that cannot be annotated carries
# renovate-exempt: <reason> on the line above it instead — absence of governance and absence of
updates are different claims, and the exemption is what keeps them distinguishable.
Where a datasource needs a registry (helm, deb), the annotation carries
registryUrl=<url> as its last field. The optional fields are ordered versioning= then
registryUrl=, because that is the order the regex accepts.
The deb datasource — one ARG, three apt pins, one dependency
Section titled “The deb datasource — one ARG, three apt pins, one dependency”apps/hl-assets-uploader/Dockerfile pins libheif to an exact Debian package revision through
ARG LIBHEIF_VERSION. The stock dockerfile manager reads FROM lines only, so the pin is
reached by a third customManagers entry scoped to apps/*/Dockerfile, which emits a deb
dependency the same way the Ansible manager emits a docker one. The annotation sits directly
above the one declaration that carries a value; the three valueless re-declarations that carry
the ARG into later stages are deliberately unannotated, because annotating them would report four
pins where there is one.
Four scoping decisions are load-bearing, not stylistic. The registry URL is
https://deb.debian.org/debian?suite=trixie&components=main&binaryArch=amd64:
| Decision | Why the alternative is wrong |
|---|---|
suite=trixie |
golang:1.26 and debian:trixie-slim are both Debian 13. suite=bookworm carries libheif 1.15.1-1+deb12u1 against trixie’s 1.19.8, so it would propose a downgrade — schema-valid, silently wrong |
components=main only |
the suite’s Packages index is fetched onto the Renovate cache PVC, which has a documented ext4 inode-exhaustion history; contrib and non-free add index bytes for packages this repository does not install |
one binaryArch |
registryStrategy: 'merge' unions version sets across registry URLs, so a second architecture makes the candidate set more permissive rather than safer |
versioning=deb |
Debian revisions (1.19.8-1+deb13u1) are not semver; the default docker versioning splits on . and mis-orders the -N+debXuY suffix |
One ARG value feeds three apt pins across two build stages. libheif-dev is installed in the
builder stage and libheif1 plus libheif-plugin-libde265 in the runtime stage, all three
interpolating ${LIBHEIF_VERSION}, so they always move in the same commit. Only one of them is
a reported dependency: the annotation declares libheif1, and a rule naming the other two matches
nothing on its own. The encoders rule lists them anyway as forward coverage — see below.
A bump here is a key-affecting encoder change. The rule that brakes it is
key-affecting encoders (hl-assets), and it only fires because custom.regex was added to its
matchManagers in the same commit that added the datasource; with gomod and dockerfile alone
the dependency resolved to automerge: true, platformAutomerge: true, a three-day floor and
nobody named. apps/hl-assets-uploader/testdata/golden.webp.sha256 and golden.key are
stop-the-line artifacts, not tests to update: a bump that moves a golden hash is stopped and
deferred with the hash delta recorded as the deferral’s evidence, never re-baselined to make a
check pass. Encoder behaviour and the object-key derivation are described in
hl-assets Uploader.
Workflow env: pins need their own manager
Section titled “Workflow env: pins need their own manager”The stock github-actions manager reads uses: lines. A version that a workflow pins in an
env: key reaches Renovate through no manager at all, so it sits unmanaged next to the annotated
pin it has to match.
.github/workflows/build-lightningstream.yml is the case that made this real. It pins
LIGHTNINGSTREAM_VERSION, the Ansible role pins lightningstream_version, and the two are one
release train. The Ansible pin moved and the workflow pin did not, so the tag that triggers the
build was never pushed, and the next converge asked for a release that does not exist.
Two entries close it:
- A fourth
custom.regexmanager over/^\.github/workflows/.+\.ya?ml$/, with the samematchStringsshape the Ansible manager uses. The# renovate:line must sit directly above the key it annotates. - A
lightningstream (lockstep)rule matched by the two paths and the dependency name, so both pins move in one pull request.
The workflow pin holds no leading v, and the workflow prepends one at each use site. The
four-pin extractVersion rule strips the prefix and is matched by matchDepNames only, so it
reaches this lane too and cannot be scoped away from it without breaking five other pins. Writing
the value bare makes that strip correct here, and it makes the two pins equal as strings.
GOLANG_BUILDER_IMAGE in the same env: block carries # renovate-exempt: instead. It must
follow upstream’s Dockerfile at the pinned tag, not the newest golang image.
managerFilePatterns is a glob unless it is /-delimited
Section titled “managerFilePatterns is a glob unless it is /-delimited”Since the fileMatch → managerFilePatterns migration, an entry is a minimatch glob
({ dot: true, nocase: true }) unless it is wrapped in /.../ or /.../i, in which case it is a
regex. A bare argocd/**/*.ya?ml$ is therefore a glob whose trailing $ is a literal character,
and it matches nothing.
Measured 2026-09-07: that exact entry matched 0 of 3922 tracked files, and the shipped default
for both the argocd and kubernetes managers is [] — so both managers were dead, and had been
since the migration. The repaired form /^argocd/.*\.ya?ml$/ matches 391 files and exposes 28 chart
dependencies across 25 Application manifests.
Assert a pattern by file-match count against git ls-files, never by a validator exit code.
renovate-config-validator accepts a glob that matches nothing; that is how the defect survived.
Path matchers over name matchers
Section titled “Path matchers over name matchers”matchFileNames tests the package file first and, failing that, the dependency’s lock files —
which is how tf/cloudflare/.terraform.lock.hcl reaches the Terraform bucket even though it is not
the package file.
A path matcher survives a provider being renamed, re-homed to another namespace, or added from an
unanticipated registry; a name matcher does not. The Terraform rules were matched by
/^hashicorp// until 2026-09-07, which covered 5 of the 12 providers this repository declares —
cloudflare/cloudflare, breml/uptimekuma, integrations/github, mmianl/powerdns,
tailscale/tailscale, keycloak/keycloak and a private registry module all fell through to the
rollup. Renovate’s terraform manager rewrites a bare provider name to hashicorp/<name>, which is
why the name matcher looked plausible while covering less than half the surface.
matchRegexOrGlobList OR-s positive patterns and AND-s !-prefixed negative ones, which is what
makes the catch-all bucket read as “any path, provided it is in none of the five named buckets.”
Forward coverage — rules that read as guards but match nothing today
Section titled “Forward coverage — rules that read as guards but match nothing today”Each of these is listed deliberately, so the day the missing half appears the rule already reaches it. Do not describe any of them as guarded.
| Listed | Reached by no manager because | Detector until then |
|---|---|---|
helmv3 in the helm charts rule |
its only file, argocd/cluster-app/Chart.yaml, declares no dependencies: block |
— |
a helmv3 MAJOR against the post-2026-09-09 guard |
same reason — no tracked Chart.yaml declares a dependencies: block, so there is no dependency for a fixture row to resolve a posture over. The manager-agnostic guard reaches this lane; no row asserts it. Close condition: a Chart.yaml carrying a dependencies: block enters the tree, at which point the lane is live and owes a row |
— |
a pip_requirements MAJOR against the post-2026-09-09 guard |
no tracked requirements.txt or requirements.pip exists anywhere in this repository; all four Python projects declare their dependencies in pyproject.toml, which is the pep621 manager. The pep621 fixture row carries the pip_requirements half of the python dependencies group by construction — both managers name the same group and the guard carries no manager matcher — not by assertion. Close condition: a tracked requirements.txt enters the tree |
the pep621 row, for the shared group only |
/^renovate$/ in ci/cd components |
the self-hosted chart’s depName is mend-renovate-ce |
the attended mend-renovate-ce rule |
libheif-plugin-libde265 |
one ARG LIBHEIF_VERSION value pins it alongside libheif1, but only libheif1 carries the annotation and is reported; libheif-dev is not listed by the rule at all |
the golden-file test on both native runners, plus the attended libheif1 bump the other two move with |
| the ClickStack otel-collector tag | a bare image.tag with no sibling image.repository; helm-values needs both |
a human editing that key |
argocd/app-configs/litellm/** |
carries no version pin today | — |
ci/cd components is deliberately not repointed at mend-renovate-ce: that rule carries
automerge: true, and repointing it would overwrite the attended guard.
Trees deliberately excluded from the ClickStack guard
Section titled “Trees deliberately excluded from the ClickStack guard”clickstack observability plane matches argocd/app-configs/clickstack-chart/**. The in-cluster
clickhouse-client pins under argocd/app-configs/clickstack/** (bootstrap-schema-job.yaml,
watchdog-cronjob.yaml) and the MCP pin at argocd/app-configs/clickhouse-mcp/deployment.yaml are
outside that glob. They are client pins whose upgrade semantics differ from the server pins, and
they resolve to the argocd manifests group — automerge, 2-day floor, nobody named. Excluded
deliberately, not forgotten.
The vulnerability path is attended repo-wide
Section titled “The vulnerability path is attended repo-wide”config.vulnerabilityAlerts is injected last, as a force block built from the keys present in
the resolved object — and config.force is never consumed, so it re-applies at the tail of every
subsequent merge. No packageRules entry can outrank it by ordering. The only lever is the forced
value, and .vulnerabilityAlerts.automerge is now false.
A vulnerability-remediation PR therefore opens immediately and waits for a human. Two brakes
were never overridden in the first place: the injected block sets neither platformAutomerge nor
reviewers, so the three attended rules’ platformAutomerge: false and their reviewer lists always
held. One brake is still off — minimumReleaseAge is forced to null on that lane, so no
release-age floor applies to a remediation PR.
Stated narrowly: an ordinary compromised release is not a vulnerability alert and does travel the attended slow path. The genuine residual is a fix version that is itself compromised — now proposed unmerged rather than automerged.
This reverses the earlier posture recorded here and in the clickstack and litellm rule
descriptions: that the fast estate-wide remediation path was accepted deliberately and that neither
LiteLLM nor ClickStack was carved out of it. A narrower control scoped to the four encoder
dependency names is not available in repository configuration — it requires force, which is
globalOnly and would leave a permanent “Configuration Error” banner on the Dependency Dashboard
and the renovate/config-validation status. The scope is repo-wide, and that cost was accepted
knowingly.
Keep .vulnerabilityAlerts.automerge at false. The injected security block outranks the
attended LiteLLM rule, so flipping it back to true would let a remediation pull request for
LiteLLM automerge past the attended lane. Nothing asserts this value; review a change to it as a
change to the LiteLLM posture.
postUpdateOptions must stay inside packageRules
Section titled “postUpdateOptions must stay inside packageRules”postUpdateOptions is declared mergeable: true and array-typed, so a top-level entry
concatenates into every manager’s resolved config instead of applying to gomod alone. It is
written inside packageRules, scoped to matchManagers: ["gomod"], and must never be hoisted.
The tidy needs no allowedCommands entry: that allowlist is read only by
execute-post-upgrade-commands.ts, which governs postUpgradeTasks, while gomodTidy is consumed
inside the gomod manager’s own artifacts.ts. The gomodTidyE variant is rejected, not
overlooked — its -e flag turns hard failures into partial successes, and an artifact update that
reports a partial write as a completed one is the defect class this milestone exists to eliminate.
The two gomod rules are deliberately two rules. Folding matchDepTypes: ["indirect"] into the
tidy entry would narrow postUpdateOptions to indirect updates only, leaving every
direct-dependency pull request untidied.
Reviewing a mend-renovate-ce bump
Section titled “Reviewing a mend-renovate-ce bump”The pin is at argocd/cluster-app/templates/renovate.yaml:17. A 14.x → 15.x bump can move values
keys, so review it against a helm template render diff of that file before merging. Never merged
by an agent.
Expect this proposal: 14.6.2 → 15.1.0, and it has not arrived yet. The bump is deferred,
not delivered, and deliberately so. It was never proposed because the manager that reaches this
file never matched — its managerFilePatterns were written unwrapped and glob-interpreted — and
that is the defect this phase repaired. Landing the bump by hand would bypass the very control the
repair exists to prove, and this chart carries the pod that does the proposing, so a bad values-key
move takes out the mechanism that would propose its own repair.
What to do when it arrives:
- Confirm the pull request is Renovate-authored. That authorship is the evidence the manager repair worked; a human-authored bump proves nothing about the matcher.
- Render both sides —
helm templateat14.6.2and at15.1.0— and read the diff for moved or renamed values keys before anything else. - Merge attended, under the
mend-renovate-ceguard’s named reviewer. Never by an agent.
Until then the deferral is recorded in the broken-windows ledger against phase 05, with that observation as its exit criterion. If a scheduled Monday run comes and goes with no such pull request, the manager repair did not take and that is the finding — not a reason to move the pin by hand.
Related
Section titled “Related”- ArgoCD Operations — how the Renovate CE workload is deployed
- hl-assets Uploader — the Go module this defect affects