Groundwork

Daily workflow

Dependencies: an agent you configure once and supervise forever.

Every real project sits on top of packages other people update weekly. Left alone, those updates pile up into risky, months-long upgrade projects. Handled well, they arrive as a steady stream of small pull requests opened by a bot, tested by your CI, and merged under your review. That bot is a dependency-update agent, and directing it uses the exact skills this whole site teaches: write the rules down, let machinery do the repetitive work, verify before trusting.

When you'll want this Your project warns about outdated packages, a security alert lands, or you wonder how serious teams stay current without spending Fridays on upgrades.
Quick reference — the moving parts
renovate.json the bot's written instructions
Dependency Dashboard one issue listing all pending updates
cooldown ignore releases younger than N days
lockfile exact resolved versions, committed
automerge merge without a human once CI is green
grouping many small bumps, one reviewable PR

The mental model

Your project declares intent (roughly which versions it wants, in package.json or similar) and records resolution (exactly which versions it got, in a lockfile). A dependency bot watches the package registries, notices when something newer satisfies your intent, and opens a pull request that updates both files together. Your CI runs on that PR like on any other. You review and merge, or you teach the bot which categories it may merge itself.

The tool used across these repos is Renovate. Treat it exactly like the coding agents on the AI agents & rules page: it is tireless and consistent, it does exactly what its instructions say, and it has no judgment about what its changes mean. The instructions file is the contract; the PRs and dashboard are how you supervise.

What the bot actually does to a repo

Current operating contract

The shared runner and preset evaluate eligible routine updates for branch and PR creation on every daily run. Supported normal timestamped npm major, minor, and patch updates use a strict five-day floor. Vulnerability alerts explicitly bypass the routine schedule, age floor, and rate limits so the next daily run can create them immediately.

Concretely, on this repo (Groundwork) and its siblings, Renovate:

  • Opens eligible grouped PRs on the daily run, labeled dependencies. Related bumps travel together so review is one read, not twenty.
  • Maintains an always-current Dependency Dashboard issue: every pending update, what is held by the maturity floor or rate limits, and what requires approval. Ticking an approval checkbox releases that held update for a future run.
  • Opens security PRs on the next daily runner pass, explicitly bypassing the routine schedule, age floor, and rate limits. Renovate does not merge them: a human reviews and merges each PR after its required CI succeeds.
  • Updates things that are not npm packages at all: GitHub Actions pinned to commit SHAs (it moves the pin and the human-readable version comment together) and Docker base-image digests.
  • Embeds each package's release notes in the PR body, so the review starts with what changed, not with hunting for a changelog.

The bot bumps versions; it never adopts features. If a release ships a new API that would simplify your code, that judgment call is yours (or a task for a coding agent you direct at it). Reading the release notes in each PR is where those opportunities surface, which is a strong reason to keep updates small and regular instead of letting them pile up.

The cooldown: patience as a security control

Supply-chain attacks on package registries follow a pattern: a malicious release appears (a hijacked maintainer account, a typosquat, a compromised build), and the ecosystem usually catches and yanks it within days. A cooldown (Renovate's minimumReleaseAge) says: do not propose a supported timestamped release younger than N days. These repos use five days for supported normal timestamped major, minor, and patch updates. You give up a little freshness and buy a real defense: by the time a supported version reaches a PR, the world has had time to notice if it was poisoned.

Package managers are adopting the same idea at install time (pnpm's minimumReleaseAge refuses too-young versions during resolution), so the floor holds even when a human runs the install by hand. Security fixes get an explicit escape hatch, because for a known vulnerability the risk calculus reverses.

Where the cooldown stops — and where it doesn't yet reach

Be precise about what the floor covers, because it is not one floor — it is one per tool, and they do not automatically agree. Renovate applies the five-day cooldown only when a datasource supplies a release timestamp and the update type supports it. Normal npm versions and timestamped tag releases can qualify; pin, digest, replacement, and lockfile-maintenance updates do not inherit a universal guarantee. pnpm applies its own minimumReleaseAge during resolution. On the machine, mise applies Groundwork's separate five-day floor to runtime upgrades — update-all runs mise upgrade --minimum-release-age 5d, filtering floating versions like lts and latest while leaving an explicitly pinned version exempt.

Homebrew is the honest gap: it has no minimumReleaseAge equivalent, so a formula or cask upgrade takes whatever is published. What it does offer is --require-sha, which refuses to upgrade a cask that ships no checksum, and Groundwork's update-all uses it. Two different controls, and it pays to keep them straight: an age floor asks "has the world had time to notice this release is poisoned?", while a checksum asks "is this the artifact the formula author signed off on?" Neither substitutes for the other.

This is also a good lesson in checking a claim before acting on it. It is widely repeated that "the fast-channel AI CLIs have no checksum," because Homebrew's version :latest casks are indeed required to declare sha256 :no_check. But that is not what these casks are: claude-code@latest is a faster release channel — an ordinary versioned, checksummed cask — not a :latest cask. Verifying that with brew info --json=v2 --cask took one command and changed the design; assuming it would have added a whole subsystem to guard a risk that was not there.

So the accurate picture, and the transferable habit — know which of your paths are gated by what, and never assume a floor you have not configured:

Normal timestamped repo versions (Renovate)5-day age floor with strict internal checks; integrity via the registry/ecosystem
Action SHA pins, image tags or digests without release timestamps, lockfile maintenanceNo universal Renovate age guarantee; immutable pins, package-manager policy, required CI, and owner review are the compensating controls. Groundwork's Gitleaks container uses an exact local age-check exception because GHCR exposes its tags but no Renovate releaseTimestamp; the tag remains digest-pinned and CI-gated
mise runtimes5-day age floor (--minimum-release-age 5d); pinned versions exempt, which is the security escape hatch. If a mise is too old to enforce the floor, Groundwork stops the run there — printing the fix, brew upgrade mise — rather than upgrading without it
Homebrew casksChecksum required (--require-sha, at install and upgrade); no age floor. Automatic upgrades are limited to ordinary casks; self-updating or privileged-replacement casks remain owner-run Review items
Homebrew formulaeHomebrew's own formula/bottle integrity; no age floor. --require-sha is cask-specific — it does not speak to formulae

One browser-family exception, stated rather than hidden: the official Google Chrome stable and Beta casks ship sha256 :no_check upstream because Google's updater owns their binaries. They cannot satisfy --require-sha, so Groundwork renders the profile-selected tokens into a separate browser manifest and installs that exact bounded set outside the checksum-safe Brewfile. Current and work profiles select stable; preview adds Beta while retaining stable. scripts/audit-brew-casks verifies both exceptions and fails if their metadata no longer justifies the lane.

And a manually-run updater is not a substitute for any of this. Running it yourself controls the timing and prevents an unattended rollout — but nobody recognizes a malicious binary by watching upgrade output scroll past.

One policy, many repos

Once two repos both run the bot, their configs drift: one repo gets a better cooldown, the other keeps the old value, and nobody decided that. The fix is the same one you use for agent rules: define shared policy once, in one place, and have each repo reference it. Renovate supports this with shareable presets. These repos extend a small public preset repo, renovate-config, which holds only the policy that must stay identical everywhere: schedule, cooldown, PR limits, labels, rebase behavior, and the security-PR rules. Anything repo-specific (which packages need human review, which groups make sense) stays in that repo's own renovate.json.

Copy, don't depend. That preset repo is MIT-licensed and deliberately public as a worked example: read it, fork it, copy the files into your own automation repo. Don't extends someone else's living policy, including this one; it changes without notice, and your dependency policy should answer to you.

Hosted or self-hosted: a real choice

Renovate runs two ways, and both are legitimate:

  • The hosted Mend app: two clicks to install, zero credentials to manage. Right for a first project and for anyone who never wants to read a job log. One trap: in the onboarding wizard the default mode can be "Scan Only," which runs silently and opens nothing. Pick "Scan and Alert" (Interactive), or the bot looks installed but broken.
  • A self-hosted runner: a small GitHub Actions workflow in an automation repo that runs Renovate on a schedule with a token you control. You gain what hosted hides: every run is a log you can open, grep, and re-dispatch with debug logging. These repos moved to self-hosted after three separate "why is the bot ignoring me?" mysteries whose answers all lived in logs only the hosted portal could see.

The trade is the same one this site keeps pointing at: convenience versus observability. Start hosted if you're new; move to self-hosted the first time you can't answer "what is the bot doing?" from where you sit. The config file is identical either way, so switching is an executor swap, not a migration.

These repositories run the self-hosted process daily. Every run evaluates eligible routine updates after their maturity floor and notices vulnerable updates promptly; there is no second weekly branch-creation window. A slower executor clock would delay both ordinary maintenance and urgent fixes after their other controls have cleared.

Supervising the bot

The human stays the senior partner. In practice that means the config encodes different trust levels for different blast radii:

  • High-blast-radius majors are held for approval. Groundwork treats an Ubuntu base-image major as a platform migration, so it appears on the dashboard for approval instead of opening a routine PR. A major upgrade with that compatibility impact is deliberate work with its own plan.
  • High-blast-radius packages always get human review, even for patches. Deployment engines and infrastructure adapters can break everything at once; the config forbids automerging them.
  • Automerge is earned, not default. This policy leaves security and routine dependency PRs for human review and merge after required CI. Any future automerge category would need its own evidence and an explicit policy decision.
  • The dashboard is the console. When something seems stuck, the answer is almost always there: held by maturity or rate limits, awaiting approval, or blocked by a config error the bot is reporting.

Set it up on your own project

The five-minute version, for a project on GitHub:

  1. Install the Mend Renovate app on your repo (choose Renovate Only, mode Interactive).
  2. Merge the onboarding PR it opens, or commit a minimal renovate.json yourself:
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:best-practices", "schedule:weekly"],
  "minimumReleaseAge": "5 days",
  "internalChecksFilter": "strict",
  "packageRules": [{
    "matchDatasources": ["npm"],
    "matchUpdateTypes": ["major", "minor", "patch"],
    "minimumReleaseAge": "5 days",
    "internalChecksFilter": "strict"
  }],
  "labels": ["dependencies"],
  "vulnerabilityAlerts": {
    "enabled": true,
    "schedule": ["at any time"],
    "minimumReleaseAge": null,
    "prHourlyLimit": 0,
    "prConcurrentLimit": 0,
    "prCreation": "immediate",
    "automerge": false,
    "platformAutomerge": false
  }
}
  1. Make sure CI runs on pull requests, so bot PRs get the same verification as yours. If there is no CI yet, that is the first thing to fix; a dependency bot without tests is just automated risk.
  2. Watch the Dependency Dashboard issue it opens, and read your first few PRs end to end, release notes included.

Never add a dependabot.yml alongside Renovate. GitHub's Dependabot would open duplicate PRs for the same updates. Keep Dependabot's alerts on (they're a data source Renovate reads for security PRs) and leave version updates to one bot.

Practice

  • Open the Dependency Dashboard issue on a repo that has one. For each pending update, say out loud why it hasn't merged yet: schedule, cooldown, or awaiting review.
  • Read one Renovate PR top to bottom, including the embedded release notes. Find one changed thing that could matter to the code, even if the answer is "nothing here affects us."
  • Tick a dashboard checkbox to force a PR ahead of schedule, watch CI run on it, and merge it yourself.
  • In any project, run your package manager's outdated command (pnpm outdated, npm outdated) and compare what it shows against what the bot has proposed. If they differ, work out which policy (cooldown, grouping, held majors) explains the gap.

Going deeper