renovate
Audit, write, or revise renovate.json. Use when adding Renovate, troubleshooting unexpected (or missing) update PRs, hardening against supply-chain attacks, or evolving an existing config.
SKILL.mddot_claude/skills/renovate/SKILL.md
GitHubA silent stall is a dependency that can never update yet looks current: no PR, no error, no [Updates: ...] on the dashboard.
Defaults
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:best-practices",
"security:openssf-scorecard",
"customManagers:githubActionsVersions"
],
"timezone": "Europe/London",
"reviewers": ["alunduil"],
"labels": ["dependencies"],
"pre-commit": { "enabled": true }
}
- This block is identical in every repo and is a standing candidate for a shared preset (
github>alunduil/renovate-config, adefault.json); Renovate also checks<owner>/renovate-configwhen onboarding a new repo. Until that repo exists, keep the block inline and change it everywhere at once. - Omit
baseBranchPatterns. It is auto-detected, and hard-coding it is the one field that differs per repo (mainvsmaster) — the thing that would block sharing. - Omit
scheduleunless a repo wants batching. With a bake period the PR is already delayed, and Renovate's defaultprHourlyLimitof 2 rate-limits the rest. packageRulesandcustomManagersaremergeable: true, so preset arrays concatenate with a repo's own rather than replacing them.labelsandreviewersare not mergeable — setting them in a repo replaces the inherited value.addLabelsmerges and appends;labelsin a shared preset is dropped wholesale by the first repo setting its own.labels— default[], so Renovate labels nothing without this. A stale sweep exempting Renovate PRs by label depends on it: closing a Renovate PR tells Renovate the version is unwanted and it will not re-offer it.vulnerabilityAlertstakes nolabels.reviewers— without it, Renovate PRs land silent. Useassigneesinstead for a creation-time ping with no rebase notifications.pre-commit: { enabled: true }— opt-in manager; enable unconditionally. No-op without.pre-commit-config.yaml; replaces pre-commit.ci where the file exists.config:best-practices=config:recommended+docker:pinDigests+helpers:pinGitHubActionDigests+:configMigration+:pinDevDependencies+abandonments:recommended+security:minimumReleaseAgeNpm+:maintainLockFilesWeekly. It does not include OpenSSF scorecard; that issecurity:openssf-scorecard, which adds a badge column to PR bodies.
Supply-chain hardening
config:best-practices bakes npm releases for 3 days (security:minimumReleaseAgeNpm) but nothing else. Widen it to every datasource:
{
"minimumReleaseAge": "3 days",
"minimumReleaseAgeBehaviour": "timestamp-optional",
"osvVulnerabilityAlerts": true,
"packageRules": [
{
"matchUpdateTypes": ["pin", "pinDigest", "digest", "lockFileMaintenance",
"lockfileUpdate", "rollback", "bump", "replacement"],
"minimumReleaseAge": null
}
]
}
minimumReleaseAge— 3-7 days catches the common attack shape (publish → community flags → upstream yanks within a day or two).A release with no timestamp is a silent stall. The default
timestamp-requiredtreats it as never stable, and the defaultinternalChecksFilter: "strict"cuts no branch whilerenovate/stability-dayspends. Two causes, one fix each:- Update type — the eight types in the carve-out never carry a timestamp.
config:best-practicespulls in:maintainLockFilesWeeklyand both digest-pinning presets, solockFileMaintenance,digest, andpinDigestare always in scope. - Datasource — some datasources return no timestamp for any release; with
hackage, every cabal dependency stalls. These are ordinary major/minor/patch updates the carve-out cannot reach.timestamp-optionallets a timestamp-less release through with a Dependency Dashboard warning; releases that carry a timestamp still bake. - Keep both. The carve-out removes the age check from the eight types, so the dashboard warning names only datasources that lack timestamps.
- Update type — the eight types in the carve-out never carry a timestamp.
A bake bypass is a lockfile refresh that resolves past the bake. Renovate cannot hand the package manager an exact version, so a manifest range resolves to its newest release, baked or not. The artifact update fails, and Renovate commits a lockfile holding a version the PR title does not name.
:pinDevDependenciespins only the depTypesdevDependencies,dev-dependencies, anddev.Non-runtime — a Poetry group's depType is its name (
test,typing). PEP 621 manifests yielddependency-groups,tool.uv.dev-dependencies, andtool.pdm.dev-dependencies. Pin each one:{ "matchManagers": ["poetry"], "matchDepTypes": ["test"], "rangeStrategy": "pin" }Runtime — a published library's runtime ranges stay ranges; downstream resolvers need them.
osvVulnerabilityAlerts: true— widens alerts beyond GitHub's advisory database to OSV. Defaults tofalse. Haskell repos are the exception: OSV fixes arrive as an open range (>= <fixed>),pvpaccepts only two-bound ranges, and the generated rule aborts every run. Treat afalsethere as deliberate and look for its tracking issue.Do not write
internalChecksFilter: "strict"orvulnerabilityAlerts: { "minimumReleaseAge": "0 days" }. Both are already the defaults (lib/config/options/index.ts:internalChecksFilterdefaultstrict; thevulnerabilityAlertsdefault object containsminimumReleaseAge: null, force-applied over the top-level bake).minimumReleaseAge: "0 days"is identical tonullas of Renovate 42.19.5. Prefernull.
Version pins in scripts
Annotate the pin; do not write a manager per pin. One generic manager reads them all, and a new pin needs no config change:
# renovate: datasource=github-releases depName=mikefarah/yq
YQ_VERSION="v4.53.2"
{
"customType": "regex",
"managerFilePatterns": ["script/install/*", ".chezmoiscripts/*.tmpl"],
"matchStrings": [
"# renovate: datasource=(?<datasource>[a-zA-Z0-9-._]+?) depName=(?<depName>[^\\s]+?)(?: packageName=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?\\s+[A-Za-z0-9_]+?_VERSION=\"(?<currentValue>.+?)\""
]
}
- Annotation field order is fixed by the regex: datasource, depName, packageName, versioning, extractVersion, registryUrl.
datasource,versioning,extractVersionandregistryUrlare recognised capture-group names — no*Templatefields needed. An out-of-order annotation does not match — a silent stall. - Set
versioning=looseon every plain pin whose upstream is not semver. The default,semver-coerced, reads a four-component version (Haskell PVP, some .NET and Java tools) as unstable, andignoreUnstabledrops every release — a silent stall. Two-component versions work by accident:3.10coerces to3.10.0. pvpupdates only two-bound cabal ranges underrangeStrategy: "widen"; a plain pin never updates.datasource=hackagedefaults topvp, so writeversioning=looseon a hackage pin.- For workflow YAML (
X_VERSION: "v1"underenv:) extendcustomManagers:githubActionsVersionsinstead of writing this yourself; addextractVersion=^v(?<version>.+)$when the consumer wants the tag without its leadingv. - Hoist a
with:version input toenv:only when its action is missing from the known-actions table. Renovate reads a listed action's input natively,actions/setup-*included; hoisting it removes a working dependency. - Upstream ships equivalents for Dockerfiles, Makefiles,
*.tfvars,pom.xml, and several CI formats — checkcustomManagers:*before writing a regex. - Keep a bespoke manager only where an annotation cannot go: a version embedded in a URL (capture
depNameandcurrentValuefrom the URL itself), or a snippet in user-facing docs where a# renovate:line would be copy-pasted by a reader. managerFilePatterns(renamed fromfileMatch): bare strings are globs; wrap in/.../for a regex. Prefer the glob.- Validate
matchStringsagainst the real files after adding a manager or editing an annotation — the regex is ECMAScript/RE2 (no lookahead, no backreferences), and a non-matching manager is a silent stall.
Validation
Wire renovate-config-validator as a pre-commit hook so schema typos, deprecated fields, and malformed custom-manager regex fail at commit time instead of surfacing as Repository Problems on the next Renovate run.
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: <latest>
hooks:
- id: renovate-config-validator
args: [--strict, --no-global]
--strict— fail on configs that need migration (e.g.fileMatch→managerFilePatterns), not only on outright errors. Neither flag is upstream default; both go inargs.--no-global— treat the file as repo-level config. Without it the validator interprets it as global self-hosted config and misreports repo-only fields.- The validator does not resolve remote presets, so it cannot tell you an
extendstarget is missing or that a shared preset changed under you. - Upstream docs: https://docs.renovatebot.com/config-validation/.
Liveness
A hosted Renovate job that dies is never retried and reports nowhere, so a repo with a valid renovate.json can stop getting updates unnoticed. Self-hosted Renovate reports its failures in the repo's Actions log and needs no check.
Renovate edits the open Dependency Dashboard issue as updates come and go, so a dashboard left untouched means Renovate has stopped. Add this job, taken from genshin.dungeon.studio daily.yml, to the repo's daily.yml; create that workflow if the repo has none.
renovate-liveness:
name: Check Renovate liveness
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: read
steps:
- uses: actions/github-script@<sha> # <version>
with:
script: |
const STALE_DAYS = 10;
const MS_PER_DAY = 86_400_000;
const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open in:title "Dependency Dashboard" author:app/renovate`;
const { data } = await github.rest.search.issuesAndPullRequests({ q: query });
const dashboard = data.items[0];
if (!dashboard) {
core.setFailed('No open Dependency Dashboard: Renovate is uninstalled or disabled on this repository. Reinstall the app or re-enable the repository.');
return;
}
const ageDays = (Date.now() - new Date(dashboard.updated_at)) / MS_PER_DAY;
if (ageDays > STALE_DAYS) {
core.setFailed(`Dependency Dashboard #${dashboard.number} last updated ${ageDays.toFixed(1)} days ago: Renovate runs are dying. Read the Mend job log, then trigger a run by hand.`);
}
- Set
STALE_DAYSto about twice the longest healthy quiet spell. - The failed run is the alert: GitHub emails it to whoever created the workflow or last edited its cron. Passing runs are silent, so a daily check adds no noise.
Dashboard reading
Renovate opens a "Dependency Dashboard" issue. Read it before assuming a bug:
- Detected Dependencies without
[Updates: ...]= current or silently stalled; the dashboard cannot tell them apart. - Pending Status Checks — the bake period holding an update back. A resident older than the bake points to a missing carve-out or a datasource with no timestamps.
- Repository Problems — investigate. "Base branch does not exist" usually means a stale config reference or a transient mid-run state.
- Config Migration Needed — Renovate offers an automated PR for field renames (e.g.
fileMatch→managerFilePatterns,baseBranches→baseBranchPatterns). Tick the checkbox or hand-migrate. - Open — pending PRs; the per-row checkboxes force a rebase/retry.
Procedure
- Confirm any field name, default, or preset body you plan to rely on against the source, not memory: defaults in
lib/config/options/index.ts, preset bodies inlib/config/presets/internal/*.preset.ts. Docs summaries and prior commits drift — several fields once worth writing are now defaults. - Read
renovate.jsonif present, and any preset it extends. - Greenfield — write the Defaults and Supply-chain hardening blocks. Add
customManagersonly for pins the annotation convention cannot reach. Add therenovate-config-validatorpre-commit hook (see Validation). - Audit existing — flag drift:
- Restated defaults:
internalChecksFilter,vulnerabilityAlerts.minimumReleaseAge,baseBranchPatterns. - Silent-stall causes: a no-timestamp carve-out missing or narrower than the eight update types, missing
minimumReleaseAgeBehaviour: "timestamp-optional", a non-semver plain pin withoutversioning=loose, unannotated*_VERSION=pins, a literalwith:version on an action missing from the known-actions table. - Bake bypass: a non-runtime depType on a lockfile manager with no
rangeStrategy: "pin"rule. - Structure: one manager per pin where an annotation would do, a listed action's
with:input hoisted toenv:, deprecatedfileMatch/baseBranches, missing validator hook, missinglabelswhere a workflow exempts Renovate PRs by label. - Liveness: a repo on hosted Renovate with no dashboard-staleness job in a daily workflow.
- Restated defaults:
- Cross-check — compare each detected dependency against its upstream latest release, each Pending Status Checks resident against its release date, and each tool's pins across workflows; diverging versions of one tool mean one pin is unmanaged. A config can pass step 4 and still hold a silent stall; only these comparisons show it.
- Surface findings before editing. Apply only after scope is agreed.