Skip to content

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.

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

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.

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.

  1. 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/null
    ls -l /tmp/renovate/cache/others/go/pkg/mod/cache/download/golang.org/toolchain/@v/
    '
  2. Confirm the signature before removing anything. Proceed only if toolchain@v0.0.1-go1.26.5.linux-arm64 has no src/ directory. That absence is the whole predicate. Do not additionally require its .zip to be missing — a healthy entry has no .zip either, for the reason given in step 3.

  3. Confirm the intact sibling is present — by its src/ tree, not by its .zip. The extracted toolchain@v0.0.1-go1.26.6.linux-arm64 directory must carry a src/ 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-arm64
    if [ -d "$d/src" ]; then
    echo "src/: PRESENT ($(ls "$d/src" | wc -l) entries)"
    else
    echo "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 .zip presence here. An earlier revision of this step required v0.0.1-go1.26.6.linux-arm64.zip and its .ziphash to both be present. That predicate was over-strict and would have halted the 2026-09-08 purge on a demonstrably healthy cache: the .zip was 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.

  4. 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-arm64
    rm -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 intact go1.26.6 entry.

  5. Record the after state with the same two commands from step 1, and re-run step 3’s src/ check to confirm the go1.26.6 entry came through the purge intact.

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

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 gomod pull request, go mod verify succeeds on that pull request’s head commit and re-running module resolution leaves go.sum unchanged. Record the pull request number and the UTC timestamp of that reading.

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.

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.

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.

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.

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.

Terminal window
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.

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-patch rollup 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: true and reviewers: []. An attended rule must therefore set both automerge keys and an explicit reviewer list. Disabling one automerge key leaves the other in force; omitting reviewers leaves nobody named.

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 manifests27 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 file0.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 the kustomize, dockerfile, helm-values and helmv3 managers 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 own description. 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 dropped matchManagers — it now reaches those managers, and every other one, so packageRules[0] is no longer partly inert. The residual that replaces it is a coverage residual rather than a posture one, and it is registered under Forward coverage below: helmv3 and pip_requirements majors 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-pg at 0.28.3 moving to 0.28.4 resolves to automerge: true, platformAutomerge: true, a 2-day floor and reviewers: [] — unattended, into an Application running prune: 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 the argocd-minor-patch branch and every() 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 own description — 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-manifestsone 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.

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:

  1. A fourth custom.regex manager over /^\.github/workflows/.+\.ya?ml$/, with the same matchStrings shape the Ansible manager uses. The # renovate: line must sit directly above the key it annotates.
  2. 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 fileMatchmanagerFilePatterns 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.

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.

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.215.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:

  1. 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.
  2. Render both sides — helm template at 14.6.2 and at 15.1.0 — and read the diff for moved or renamed values keys before anything else.
  3. Merge attended, under the mend-renovate-ce guard’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.