Shared supply-chain policy for dependency updates
Multi-ecosystem • Multi-forge • Review-gated by default
A Renovate preset that encodes one decision: which dependency updates are allowed to land without a human looking at them, and which are not.
The answer is deliberately narrow. Trusted dev tooling and minor/patch CI action bumps automerge. Production dependencies, lockfile refreshes and bare digest moves do not — those are the paths a supply-chain attack travels.
It is not a JavaScript config. It covers Rust, Nix, Terraform, Ansible, Docker and CI actions alongside the JS ecosystems, because the threat model does not care what language you write.
The Shai-Hulud 2.0 npm attack in November 2025 is the origin story, not the scope. It prompted the first version and the 428-package watch list, but the policy here is general: assume any dependency can turn hostile between one release and the next, and make the blast radius a review instead of a merge.
The watch list still ships, gated behind dashboard approval. Affected version ranges are published in the Datadog IOC database.
- 7-day
minimumReleaseAge, set as a packageRule for npm so it actually applies — a top-level value is outranked by the inherited npm rule security:minimumReleaseAgeNpm— avoids freshly published and unpublished packages- No automerge for production deps — only trusted dev tooling automerges
- Groups all non-major updates into one PR, majors stay separate
- Uses Platform Automerge (GitHub Native) for faster merging of approved PRs
- Automerges only trusted dev tooling (Biome, Oxlint, TypeScript, Vitest, Jest, ESLint, Prettier), matched on anchored names so neighbouring packages in unowned npm prefixes cannot inherit the trust
- Automatic deduplication for npm/pnpm/yarn lockfiles
- Weekly lock file maintenance, review required (
minimumReleaseAgedoes not gate lockfile refreshes) - Semantic commits enabled (
chore(deps): update package) - Vulnerability alerts with security labels
- Pins GitHub Actions to digests; automerges minor/patch only, never a bare digest move
- 428 known-compromised packages gated behind dashboard approval
- Supports npm, pnpm, yarn, Bun, Deno, Rust, Nix, Terraform, Ansible, Docker, GitHub Actions
| Category | Grouped as | Managers |
|---|---|---|
| JavaScript / TypeScript | (base) | npm (covers npm, pnpm, yarn), bun, bun-version |
| Deno | Deno | deno |
| Rust | Rust | cargo |
| Go | Go | gomod |
| Python | Python | pip_requirements, pip-compile, poetry, pep621, pipenv, setup-cfg |
| JVM | JVM | gradle, gradle-wrapper, maven, maven-wrapper, sbt |
| .NET | .NET | nuget |
| PHP | PHP | composer |
| Ruby | Ruby | bundler |
| Dart / Flutter | Dart | pub |
| Swift | Swift | swift, cocoapods |
| Infrastructure | Nix / Terraform / Ansible | nix, terraform, ansible |
| Containers | Docker digests | docker |
| Kubernetes | Kubernetes | kubernetes, helmv3, helm-values, helm-requirements, helmfile, flux, argocd |
| CI/CD | CI: GitHub Actions | github-actions |
Anything not listed still gets updates — everything non-major groups into one PR by default, and the rules above only split specific ecosystems back out.
Drop this file into a new repo and you are done:
{ "extends": ["github>ownctrl/supply-chain"] }That is the whole setup. The preset carries the schedule, grouping, automerge policy and ecosystem coverage — there is nothing else to configure per repo.
Two prerequisites, both one-time:
- Renovate reaches the repo. On GitHub.com that is the Mend Renovate App, installed for the account or org. On every other forge you run Renovate yourself — see below.
- Allow auto-merge is enabled in the repo's settings. This preset sets
platformAutomerge, which uses GitHub's native auto-merge. With the repo setting off, Renovate silently falls back to merging through its own bot instead — it still works, but not the way this preset describes.
The preset is only JSON; it works wherever Renovate does. Getting Renovate to run is the part that differs, and only GitHub.com is free of setup:
| Forge | How Renovate runs |
|---|---|
| GitHub.com | Mend Renovate App — hosted, free, nothing to run |
| GitLab | self-hosted; the renovate-runner CI template runs it as a scheduled pipeline |
| Codeberg / Forgejo / Gitea | self-hosted; run the Renovate CLI on a schedule |
| Bitbucket | self-hosted |
This is a property of the Renovate ecosystem, not of this preset — you would face it with any preset, or with none. Mirroring this repo to another forge is about making the preset resolvable there; nothing runs on the mirror itself.
- Nothing happens until Monday. The schedule is
before 06:00 on monday(Europe/Prague). This is not a misconfiguration — set"schedule": ["at any time"]in your repo if you want the first run immediately. - The Dependency Dashboard issue is the control surface. Majors and known-compromised packages wait there for a click.
- What automerges on its own: trusted dev tooling (Biome, Oxlint, TypeScript, Vitest, Jest, ESLint, Prettier and their scopes) and minor/patch GitHub Action bumps. Everything else opens a PR and waits for you.
That last point is the deliberate trade-off: production dependencies, lockfile refreshes and bare digest moves are the paths a supply-chain attack travels, so they are review-gated by design. Expect a handful of clicks a week, not zero.
github> names the forge explicitly and is the right default today, because
GitHub is where this repo actually lives.
local> resolves against whichever forge Renovate is currently running on, so
one line would work on GitHub, GitLab, Codeberg and self-hosted Forgejo alike —
but only once the preset is mirrored to each of them under the same path. It is
not, yet. Reach for local> when you maintain your own mirrored copy.
Pin a release if you do not want your policy to change under you:
{ "extends": ["github>ownctrl/supply-chain#v1.0.0"] }Just extend it. Fixes reach you the moment they land. Nothing to maintain.
{ "extends": ["github>ownctrl/supply-chain"] }Extend it and override. Same, plus your own additions. Use this for a second brand or team rather than forking — one security fix should not have to be applied once per copy.
{
"extends": ["github>ownctrl/supply-chain"],
"labels": ["dependencies", "yourbrand"]
}Take a copy. This repo is a GitHub template. Use it when you need to own the policy outright — an air-gapped mirror, a compliance requirement, a disagreement with a decision here.
Be clear about the price: a copy stops receiving fixes. Four faults were found in this preset that no amount of reading the documentation would have surfaced, and a copy taken before each one kept it. For a security policy that is the expensive direction.
If you want your own address and the fixes, have your copy extend upstream:
{
"extends": ["github>ownctrl/supply-chain"],
"packageRules": [ /* your deviations, and only those */ ]
}The Setup Owner workflow rewrites the preset references and LICENSE to the
new owner when you dispatch it manually.
Renovate has three managers here, and the split is not the one you would guess:
npmcovers npm, pnpm and yarn. There is no separatepnpmoryarnmanager — all three are the same manager reading different lockfiles.bunis its own manager (bun.lock,bun.lockb). Commit the lockfile.denois its own manager (deno.json,deno.jsonc,deno.lock) and pulls from the npm, jsr and deno datasources.
.bun-version is picked up by the bun-version manager.
Nub needs nothing special. It reads and writes whichever lockfile the
project already has (package-lock.json, pnpm-lock.yaml, bun.lock), so
Renovate keeps using the matching manager and nub reads the result. Note that
lockfile refreshes are performed by the incumbent package manager, not by nub.
- Biome (
@biomejs/*) is treated as trusted dev tooling and grouped + automerged on non-major updates. - Oxlint (
oxlint,@oxc-project/*) follows the same pattern as Biome.
- Nix Flakes are supported via Renovate's
nixmanager. Commit yourflake.lockfile for reliable updates. - Nix dependencies are grouped together with higher priority (
prPriority: 5). - Renovate will automatically update inputs in your
flake.lockwhen new versions are available.
- Terraform modules and providers are managed via the
terraformmanager. Works withmain.tf,versions.tf, and other Terraform files. - Ansible Galaxy roles and collections are supported via the
ansiblemanager (looks forrequirements.ymlorgalaxy.yml). - Both are grouped separately with higher priority (
prPriority: 5) for infrastructure changes.
- Socket.dev works perfectly with Renovate - they complement each other. Socket provides supply chain security scanning, while Renovate handles updates. Socket can block problematic PRs from Renovate.
- Dependabot Alerts - keep them enabled for security notifications. Remove
.github/dependabot.ymlif you used Dependabot "version updates" to avoid duplicate PRs.
| Setting | Value | Reason |
|---|---|---|
minimumReleaseAge |
7 days | Avoid freshly published packages (set as a packageRule for npm, which outranks the top-level value) |
security:minimumReleaseAgeNpm |
enabled | Avoid freshly published and unpublished packages |
rangeStrategy |
pin | Lock exact versions (npm, bun, deno) |
prConcurrentLimit |
4 | Avoid PR storms |
schedule |
Mondays 06:00 | Weekly updates |
timezone |
Europe/Prague | Local timezone |
automerge (prod deps) |
❌ disabled | Security review required |
automerge (trusted dev) |
✅ enabled | Biome, TypeScript, Vitest, etc. |
vulnerabilityAlerts |
✅ enabled | With security labels |
lockFileMaintenance |
✅ weekly | Review required — the age gate does not apply here |
./tooling/validate.shSame list of presets CI checks. Wire it up as a pre-push hook if you want it to run on its own:
ln -s ../../tooling/validate.sh .git/hooks/pre-pushA local hook is a convenience, not a boundary — anyone can skip it. CI stays the real gate.
validate.sh runs two things, and the second matters more:
renovate-config-validator --strict— the presets are well formedtooling/test_policy.py— the presets decide what they should
The validator checks shape, not meaning. Every fault this preset has shipped passed it: an unanchored pattern granting trust to a namespace nobody owns, a rule ordered so it undid the one above it, two manager names that do not exist. The policy test freezes those as cases, and fails if lockfile maintenance ever gets automerge back.
It reimplements Renovate's matching rather than calling Renovate, so it can drift from the real engine. A failure is a reason to look; a pass is weaker evidence than a dry run.
You can test this config locally before deploying:
# Install Renovate CLI
npm install -g renovate
# Run in dry-run mode (no changes made)
LOG_LEVEL=debug renovate --platform=local --dry-run=true
# Or use npx without installing
npx renovate --platform=local --dry-run=trueEach of these is a ready preset, not a snippet to copy. Reference it directly:
| Preset | Reference | What it changes |
|---|---|---|
| base | github>ownctrl/supply-chain |
the policy described above |
| lockdown | github>ownctrl/supply-chain:lockdown |
nothing automerges, 14-day npm floor, every update waits for dashboard approval |
| no-automerge | github>ownctrl/supply-chain:no-automerge |
automerge off, everything else unchanged |
| aggressive | github>ownctrl/supply-chain:aggressive |
any time, no release-age floor, higher PR limit |
Reach for lockdown during an active supply-chain incident and aggressive only when you are certain there is not one.
{ "extends": ["github>ownctrl/supply-chain:lockdown"] }Sub-presets extend the base themselves, so you do not list both.
For teams in different timezones:
{
"extends": ["github>ownctrl/supply-chain"],
"timezone": "America/New_York",
"schedule": ["before 09:00 on monday"]
}This preset includes warnings for packages affected by the Shai-Hulud 2.0 attack. When Renovate proposes updates for these packages, the PR will include:
⚠️ Security warning banner- Checklist for verification
- Links to IOC lists
Currently monitored packages: 428
Sourced from the Datadog IOC database. These are gated behind dashboard approval with a warning attached — not blocked, so fixed versions can still land.
The full list lives in default.json. Affected version ranges are in the Datadog IOC database.
This preset decides which dependency updates may merge unattended. That is its whole scope, and it is worth being blunt about the edges:
- It gates updates, not what you already have. A hostile version already in your lockfile is outside its reach. Check the lockfile directly and rotate anything the package could have read.
- It is not a scanner. It does not inspect package contents, verify that a published artifact matches its source, or detect compromise. Pair it with something that does.
- A delay is not a guarantee.
minimumReleaseAgebuys time for someone else to notice a bad release. Nobody may notice. Shai-Hulud went unnoticed for longer than seven days in some packages. - The watch list is a snapshot. 428 packages known compromised in one attack, in November 2025. It says nothing about the next one.
- The policy test reimplements Renovate's matching rather than calling Renovate, so it can drift from the real engine.
Provided as is, without warranty, under the MIT licence. Deciding what your project may merge unattended is your decision; this preset is a starting point with reasoning attached, not a substitute for making it.
.github/workflows/setup-owner.yml exists for the copy path. Dispatch it
manually after taking a copy and it rewrites the preset references and the
LICENSE copyright to whoever now owns the repo, then deletes itself.
It runs with contents: write and pushes to the default branch, which is what
it needs to do that job. It is workflow_dispatch only — nothing triggers it
automatically. If you are extending this preset rather than copying it, you
will never run it.
main is what an unpinned github>ownctrl/supply-chain resolves to, so every
merge reaches those consumers immediately. Tags exist for the people who pinned.
Merge as often as you like; cut a tag when a pinned consumer has a reason to
move.
There is no API here to version. What a consumer gets from this preset is one thing — what merges without them looking — so that is what the number tracks.
| Meaning | Example | |
|---|---|---|
| MAJOR | something now merges unattended that did not before, or an existing config breaks | adding a package to the trusted list; renaming a sub-preset |
| MINOR | new coverage that loosens nothing | a new ecosystem group, a new sub-preset, more packages on the watch list |
| PATCH | nothing about what merges changes | documentation, PR body text, our own pinned tooling |
The asymmetry is deliberate. Tightening is minor, loosening is major, even when the diff is the same size. If something stops automerging, the worst case is a few more clicks. If something starts automerging, you have gone around the reason someone pinned in the first place — a pin is a promise that the posture will not shift underneath them.
So adding one package to the trusted list is a major, one line or not.
- 🔒 Socket.dev — Supply chain security scanning
- 📊 Datadog IOCs — Official IOC list