Drop Dependabot naïvely into a monorepo, and dozens of PRs line up on Monday morning — and then no one looks. The cause is clear: in a monorepo the update targets balloon to "package count × dependency count." But this can be solved by design. Consolidate with directories, bundle with groups, let things age with cooldown, and flow patch/minor through auto-merge — with these four points, dependency updates are quietly automated even in a huge monorepo.
This article focuses on monorepo-specific design as a topic of the Dependabot production-operations guide. It uses Turborepo / pnpm workspaces as the main subject, but the thinking is the same for npm/yarn workspaces and Nx.
Rules for this article: config keys and behavior are based on the GitHub official documentation (as of June 2026).
directories(globs) andgroup-byare relatively new features. Always confirm the latest in the official options reference before production.
0. Why it breaks on a monorepo
通常リポジトリ: 1 つの package.json → 依存 N 個 → 最大 N 本のPR
モノレポ(素朴): M 個の package.json → 依存 N 個ずつ → 最大 M×N 本のPR
With 5 in apps/, 10 in packages/, each holding dozens of dependencies, you easily reach 3-digit PR counts. Papering over this with "lower the frequency" then delays vulnerability response. The right answer is consolidation and bundling. Let's build it up in order.
1. Consolidate into one entry with directories globs
It used to be that you wrote as many updates entries as you had directories. Now you can consolidate into one entry with directories (plural) + globs.
version: 2
updates:
- package-ecosystem: "npm"
directories:
- "/" # ルート(ワークスペース定義・共有devDeps)
- "/apps/*" # 各アプリ
- "/packages/**" # 共有パッケージ(再帰)
schedule:
interval: "weekly"
* is one level, ** is recursive. With this, adding a new app/package needs no config change (ETC: change stays in one place).
2. The relationship between workspaces and the lockfile (this is the crux)
A monorepo's dependency resolution is determined by the location of the lockfile.
- pnpm workspaces:
pnpm-lock.yamlis a single file at the repository root. All workspaces' dependencies are resolved here. - npm / yarn workspaces: similarly, a single lockfile at the root (
package-lock.json/yarn.lock).
So in many monorepos, you can first update all workspaces' external dependencies with just a single root (/) entry. Each apps/*'s package.json declares "which external dependencies it uses," but the source of truth for resolution is the root lockfile.
# pnpm/npm/yarn workspaces:まずはルート1エントリで十分なことが多い
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
Expand with
directoriesonly when you first want to separate different update policies (frequency, groups, ignore) per individual app. YAGNI: not splitting per app from the start, but splitting once you need to, is the trick to keeping operations light.
3. Bundle PRs with groups
Even after consolidating, without bundling you get as many PRs as dependencies. Group them into meaningful units with groups.
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
groups:
types:
patterns: ["@types/*"]
testing:
patterns: ["vitest", "@testing-library/*", "playwright", "msw"]
linting:
patterns: ["eslint*", "prettier", "@typescript-eslint/*"]
# 上記以外の minor/patch を1本に(major は個別PRのまま)
minor-and-patch:
update-types: ["minor", "patch"]
exclude-patterns: ["@types/*", "vitest", "eslint*"]
This consolidates into the 4–5 PRs of "types," "testing," "lint," and "other minor/patch," dramatically reducing the review target. For all groups options, see the complete configuration guide.
4. Consolidate across directories with group-by: dependency-name
When you've split directories per app, it's self-defeating to have "every app's react update turned into separate PRs." With group-by: dependency-name, bundle the same dependency across directories.
- package-ecosystem: "npm"
directories: ["/apps/*"]
schedule:
interval: "weekly"
groups:
react-across-apps:
patterns: ["react", "react-dom", "next"]
group-by: "dependency-name" # 全 app の react/next 更新を1PRに
It's a setting that pairs well with the operation of aligning framework versions across the whole monorepo (preventing version skew).
5. Internal packages are out of scope (so it's easy)
In a monorepo you reference internal packages like "@acme/ui": "workspace:*". This workspace:* reference is not an update target for Dependabot — because the version of an internal package is something the monorepo itself manages.
So Dependabot focuses solely on tracking external dependencies. No need to worry "I have to ignore internal dependencies." Consistency among internal packages is the job of other tools like Turborepo/Nx's build graph or changesets (SRP: don't mix responsibilities).
6. Per-package policy (updates proportional to risk)
Not everything needs to be updated with the same intensity. The closer to production, the more cautious the assignment.
updates:
# アプリ:積極的に追従(minor/patch は groups でまとめ auto-merge 前提)
- package-ecosystem: "npm"
directories: ["/apps/*"]
schedule:
interval: "weekly"
groups:
app-minor-patch:
update-types: ["minor", "patch"]
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"] # major は人間が棚卸し
# 共有ライブラリ:保守的(月次・cooldown 長め)
- package-ecosystem: "npm"
directories: ["/packages/**"]
schedule:
interval: "monthly"
cooldown:
default-days: 14
A breaking change in a shared library ripples to all apps. So lower the frequency for libs and make cooldown longer. Have apps track fast and small. This division solves the stability-vs-freshness trade-off in a grounded form.
7. CI and auto-merge: run only the affected scope
Full-testing all packages on a monorepo wastes CI time (= Actions billing) on every Dependabot PR. Test only the affected packages.
# .github/workflows/ci.yml(抜粋)— Turborepo の filter で影響範囲のみ
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run lint test build --filter="...[origin/main]"
--filter="...[origin/main]" specifies running "only the packages affected by the change and their dependents." It becomes the minimum necessary test according to the Dependabot PR's diff — faster, cheaper, and more reliable.
On top of that, combine auto-merge to auto-merge grouped patch/minor conditioned on CI.
- name: Auto-merge grouped patch/minor
if: |
steps.meta.outputs.dependency-group != '' &&
steps.meta.outputs.update-type != 'version-update:semver-major'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8. Monorepo operations checklist
- Start from a single root entry (the workspace lockfile is a single root file)
- Expand with
directoriesonly when you need a per-app policy (YAGNI) - Bundle types/testing/lint/other minor-patch with
groups - Align versions across apps with
group-by: dependency-name - Shared libraries are monthly + longer cooldown, apps are weekly + aggressive tracking
- CI runs only the affected scope (Turborepo/Nx filter) to minimize time and cost
- Grouped patch/minor with auto-merge, major with human review
9. FAQ
Q. apps and packages all have package.json. Should I split the entries too?
A. A single root entry is often enough first (the workspace lockfile is a single root file). Split with directories once you want to change frequency or groups individually.
Q. Every app's react is turned into separate PRs.
A. If you've split directories, bundle the same dependency across directories with group-by: dependency-name. It also prevents version skew.
Q. Are internal packages (workspace:*) updated too? A. They aren't. Dependabot looks only at external dependencies. Internal consistency is the job of changesets or Turborepo/Nx.
Q. CI for all packages runs per PR — it's slow and expensive. A. Test only the affected scope with Turborepo/Nx's filter. Dependabot is free on standard runners, but CI itself consumes Actions minutes as usual.
Q. Is pnpm's pnpm-lock.yaml updated correctly?
A. Yes. Dependabot resolves and updates the root lockfile. Always commit the lockfile.