Shared Renovate preset for lexfrei repositories — one source of truth for dependency-automation policy.
In a repository's renovate.json:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>lexfrei/renovate-config"]
}Add per-repo overrides after the extends as needed.
config:recommended+ semantic-commit chore prefixes, no PR rate/concurrency limits, every PR labelleddependencies.- Automerge (squash) for every update type, majors included, performed by Renovate itself rather than by GitHub auto-merge (
platformAutomerge: false). CI is the gate: Renovate merges a branch only when it is up to date with the base branch and passing all tests, so a major that breaks the build stays open. A repository that wants a human on majors overridesautomergeformatchUpdateTypes: ["major"]locally, and a repository with no CI at all needsignoreTests: trueor nothing will ever merge. GitHub auto-merge only exists for a PR that is not yet mergeable, so on a branch with no required status check the API rejects enabling it withPull request is in clean status, and in a private repository below GitHub Pro the setting cannot be turned on at all —PATCH /repos/{owner}/{repo}acceptsallow_auto_merge: trueand leaves it false. Renovate does not fall back from either case, so those PRs sat open and green indefinitely. Doing the merge itself works the same way with or without branch protection. A repository that wants the platform behaviour can still setplatformAutomerge: true, and needs a required check for it to mean anything. - GitHub Action refs pinned to digests (
helpers:pinGitHubActionDigests). docker:enableMajor, which states the default rather than changing it: Renovate already proposes major image updates, and nothing inconfig:recommendedturns them off. It is here so a futuredocker:disableMajorin some consumer repo has to be a deliberate, visible override.postUpdateOptions: gomodTidyandgomodUpdateImportPaths, sogo.mod/go.sumstay tidy and a Go major bump rewrites the/vNimport paths in the code.godirective ingo.modbumped (rangeStrategy: bump) — CI must useGOTOOLCHAIN=autoor a matchingsetup-goso the runner can build the new directive.controller-runtimegrouped withk8s.io/*under onekubernetesgroup, so they always bump together (a split bump breaks compilation).- A regex
customManagerthat bumps any tool version annotated with a# renovate: datasource=… depName=…comment in workflows or the repository's rootMakefile(golangci-lint, go, templ, …). Seed the workflow with a concreteversion: vX.Y.Zonce; Renovate keeps it current after that. minimumReleaseAgeBehaviour: timestamp-optional, which only bites in a repo that setsminimumReleaseAgeitself. Renovate 42 madetimestamp-requiredthe default, so a release with no timestamp is never treated as stable — and thedockerdatasource reports timestamps for Docker Hub only. A quarantine on major updates then freezes every ghcr image and everygithub-tagsdependency permanently: no error, nothing above debug level in the log, one line under Pending Status Checks on the dashboard that reads like it is waiting for CI.timestamp-optionalkeeps the wait effective where release dates exist and drops it where they cannot be had.- A second regex
customManagerfor apk packages pinned in aContainerfileorDockerfile— see below. pip_requirementswidened to the.insources thatpip-compilelocks are generated from. The stock patterns match the compiled.txtonly, so a bump rewrote the lock and left the source pinned to the old version — harmless where nothing reads the source, CI-red in a repo whose tests assert the two agree, and in both cases the file that documents the intended version stops being true.- Vulnerability fixes, including indirect Go modules — see below.
Renovate marks every // indirect entry in go.mod as disabled, so on a stock config a transitive module with a known advisory is never proposed at all. Enabling vulnerability alerts is the whole mechanism: an alert-driven update carries a force block, with alerts enabled that block holds enabled: true, and package rules run before the disabled check, so the dependency is re-opened for that one fix and stays open through the update stage.
One shape stays out of reach and it is worth knowing which. A Go pseudo-version like v0.0.0-20230101000000-abcdef123456 carries a digest, so the gomod extractor pins that dep to loose versioning, and loose cannot evaluate a range at all. GitHub's alerts state a range (< firstPatchedVersion), OSV states the exact affected version, so a vulnerable pseudo-version dep is remediated through the OSV feed and not through a GitHub advisory alone. That is one of the reasons osvVulnerabilityAlerts is on here.
security:gomodIndirectSecurityUpdates is deliberately not extended, which deserves an explanation because it reads like the obvious answer. Driving Renovate's own applyPackageRules through both rule stages gives the same outcome with and without it in every alert shape, including the pseudo-version case it looks aimed at: the preset re-opens the lookup, then its own blanket disable takes the update away again. What it does add is a datasource lookup for every // indirect entry on every run, which on a controller-runtime tree is a few hundred per run for no extra PRs. hack/check-presets.mjs pins all four outcomes so this claim stays checkable.
Advisories come from two independent sources: GitHub's Dependabot alerts, which need the Dependency graph and Dependabot alerts switched on in the repository's security settings, and the OSV database (osvVulnerabilityAlerts), which carries the Go vulnerability database in addition to GitHub's advisories and is still marked experimental upstream. Fixes carry a security label on top of the usual dependencies one — create that label in the repository if it isn't there — and a minor or patch fix automerges like any other.
Renovate's own documentation says OSV alerts cover direct dependencies only, which reads like a contradiction here. It is not: go.mod lists indirect requirements explicitly, unlike a package.json, so they arrive as ordinary extracted dependencies and the OSV scan sees them.
Pin the linter once per repo so a new release can't silently turn the required Lint check red:
- uses: golangci/golangci-lint-action@v9
with:
# renovate: datasource=github-releases depName=golangci/golangci-lint
version: v2.12.2The customManager above then bumps v2.12.2 via controlled PRs.
Both annotation managers read the line directly below the comment, so the annotation has to sit immediately above the version, with nothing but whitespace between them. Above the uses: line, or with another key in between, it matches nothing and reports nothing.
An unpinned apk add gives a different image on every build. Pin the version and annotate it, and Renovate keeps the pin current from Repology:
RUN apk add --no-cache \
# renovate: datasource=repology depName=alpine_3_22/upx
upx=5.0.1-r0depName is alpine_<major>_<minor>/<package>, matching the Alpine release the base image comes from. Without the annotation the manager does nothing, so this is opt-in per package.
The annotation has to sit directly above the pinned token, with nothing but whitespace in between — that is what the manager's regex looks for. Put it above the whole RUN line instead and the match silently fails, and if the annotation also carries versioning=, the regex matches the wrong half and tracks a dependency whose version is the string loose.