Automated dependency updates for the catalog: protocol. Replaces Dependabot for monorepos that centralize dependency versions in a catalog — Bun, pnpm, and Yarn are all supported.
Dependabot doesn't understand the catalog: protocol, so it can't update the centralized version catalog in your monorepo. This action fills that gap.
- Reads catalog definitions from
package.json(catalog/catalogs, Bun),pnpm-workspace.yaml(pnpm), or.yarnrc.yml(Yarn) — auto-discovered, including named catalogs - Queries npm for the latest stable versions and groups updates into configurable batches
- Creates and syncs PRs via the GitHub CLI — closes stale ones, rebuilds conflicting ones, and includes GitHub Releases notes
- Detects vulnerable transitive dependencies via the package manager's audit and creates override PRs (bun/pnpm →
overrides, yarn →resolutions) - Optionally enforces a minimum release age (supply chain protection) and turns on GitHub auto-merge
- Runs as a GitHub Action or a standalone CLI
| Manager | Catalog definition | Default catalog | Named catalogs | Install | Vulnerability audit |
|---|---|---|---|---|---|
| Bun | package.json → catalog / catalogs |
✓ | ✓ | bun install |
✓ (bun audit) |
| pnpm | pnpm-workspace.yaml → catalog / catalogs |
✓ | ✓ | pnpm install |
✓ (pnpm audit) |
| Yarn (Berry 4.10+) | .yarnrc.yml → catalog / catalogs |
✓ | ✓ | yarn install |
✓ (yarn npm audit) |
The package manager is detected from the definition file that declares the catalog — there is no packageManager config option (it was removed; existing configs that set it are ignored with a warning).
- Bun runtime (used to run this action; Bun catalogs also use it to install)
- The package manager matching your catalogs (
pnpm/yarn) available on the runner for non-Bun catalogs. For Yarn that means Berry 4.10+ onPATH— catalogs are built in from 4.10.0, and the Yarn 1.x binary that GitHub runners ship by default understands neither catalogs nor.yarnrc.yml. Add a setup step, e.g.corepack enablewith apackageManager: yarn@4.xfield inpackage.json ghCLI (pre-installed on GitHub Actions runners)- A GitHub token with
contents: writeandpull-requests: writepermissions
# .github/workflows/catalog-update.yml
name: Catalog Updates
on:
schedule:
- cron: '0 6 * * 1-5' # Weekdays at 06:00 UTC
push:
branches:
- master
paths:
- '**/package.json'
- '**/pnpm-workspace.yaml'
- '**/.yarnrc.yml'
- '**/bun.lock'
- '**/pnpm-lock.yaml'
- '**/yarn.lock'
workflow_dispatch:
# Rebuilds of open update PRs must run one at a time; queued (not cancelled)
# runs are what let the cascade converge after each merge.
concurrency:
group: catalog-update
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: brandhaug/catalog-update-action@v1Tip: The
pushtrigger makes the action rebuild open PRs immediately after every merge to the default branch. With auto-merge enabled this cascades — each merge rebuilds the remaining PRs, their CI runs, they merge, and the process repeats until all groups are up to date — instead of waiting for the next scheduled run.
Tip: PRs created with the default
GITHUB_TOKENwon't trigger downstream workflows (e.g. CI checks). Use a GitHub App token instead:- uses: actions/create-github-app-token@v1 id: app-token with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} - uses: brandhaug/catalog-update-action@v1 with: token: ${{ steps.app-token.outputs.token }}
| Input | Default | Description |
|---|---|---|
config |
.catalog-updaterc.json |
Config filename, resolved in each catalog directory and then its ancestors up to the repository root |
dry-run |
false |
Preview updates without creating PRs |
token |
github.token |
GitHub token for creating PRs. Use a PAT or GitHub App token to trigger downstream workflows |
exclude-directories |
'' |
Comma-separated directories to exclude from catalog discovery (supports glob patterns) |
bun-version |
1.4.2 |
Bun version used to resolve and install dependencies. Match your project's Bun — Bun 1.4+ writes a lockfile format unreadable by Bun <= 1.3 |
bunx catalog-update-action --dry-run # preview without installing
catalog-update # full run (creates PRs)
catalog-update -c path/to/config.json # custom config pathInstall globally with bun add -g catalog-update-action to use the catalog-update binary.
| Flag | Short | Description |
|---|---|---|
--help |
-h |
Show help message and exit |
--version |
-v |
Show version and exit |
--dry-run |
-d |
Preview updates without creating PRs |
--config <path> |
-c |
Path to config file (default: .catalog-updaterc.json) |
--exclude <dirs> |
-e |
Comma-separated directories to exclude from catalog discovery (supports glob patterns) |
Create a .catalog-updaterc.json in your repository root:
{
"$schema": "https://raw.githubusercontent.com/brandhaug/catalog-update-action/master/schema.json",
"branchPrefix": "catalog-update",
"defaultBranch": "master",
"maxOpenPrs": 20,
"concurrency": 10,
"minReleaseAgeDays": 3,
"groups": [
{ "name": "react", "patterns": ["react", "react-dom"] },
{ "name": "all-patch-updates", "patterns": ["*"], "updateTypes": ["patch"] }
],
"ignore": [],
"audit": {
"enabled": true,
"minimumSeverity": "moderate"
},
"autoMerge": {
"enabled": false,
"mergeMethod": "squash"
}
}Tip: Add the
$schemafield to get autocomplete and validation in your IDE.
A catalog uses the nearest config found in its directory or an ancestor, stopping at the repository root. A directory-local config replaces the inherited config. If no config exists, each catalog uses defaults independently. An absolute --config path explicitly shares that file.
Catalogs with the same package manager, catalog name and resolved config share an update scope. For example, root package.json and milkyway/package.json both inherit a root .catalog-updaterc.json. Each update group gets one PR containing changes to both catalogs where applicable. All affected workspaces run their own install before the commit, so both lockfiles travel with shared dependency updates. The PR limit applies to the whole scope. Named catalogs remain separate.
Identical shared pins advance together. If a lookup or provider restriction prevents one copy from updating, the affected group is skipped across the scope. Overlapping groups are combined when they update the same package. Audit overrides remain workspace-specific.
PR synchronization only manages branches belonging to the scope. When moving a config to an ancestor, an existing matching workspace PR can be reused and rebuilt with all affected catalogs. Independently configured workspaces keep their own PRs and policies.
| Option | Type | Default | Description |
|---|---|---|---|
branchPrefix |
string |
"catalog-update" |
Prefix for PR branches (e.g., catalog-update/react) |
defaultBranch |
string |
"master" |
Base branch for PRs |
maxOpenPrs |
number |
20 |
Maximum number of open PRs at any time |
concurrency |
number |
10 |
Max concurrent npm registry requests |
minReleaseAgeDays |
number |
0 |
Minimum days a release must be published before creating a PR (supply chain protection). 0 = disabled. Does not apply to audit overrides |
groups |
array |
[] |
Dependency grouping rules |
ignore |
array |
[] |
Dependency ignore rules |
audit |
object |
{} |
Transitive vulnerability audit settings (Bun catalogs only) |
autoMerge |
object |
{} |
GitHub auto-merge settings |
Groups batch updates into PRs. Each group has a name, patterns (glob patterns, * wildcard supported), and an optional updateTypes list restricted to "major", "minor", or "patch". Groups are evaluated in order — first match wins — so put specific groups first and a catch-all all-patch-updates group last. Packages not matched by any group get individual PRs.
Patch updates in a named group collapse into all-patch-updates unless the group has a minor or major update, reducing PR noise.
{
"groups": [
{ "name": "react", "patterns": ["react", "react-dom"] },
{ "name": "all-patch-updates", "patterns": ["*"], "updateTypes": ["patch"] }
]
}Ignore rules prevent updates for matching packages. pattern is a glob; updateTypes optionally limits which change types are ignored (omit to ignore all).
{
"ignore": [
{ "pattern": "*storybook*", "updateTypes": ["major"] },
{ "pattern": "typescript" }
]
}Require releases to be published for a minimum number of days before the action creates a PR, giving the community time to flag compromised packages. When the latest version is too young, the action falls back to the newest version that meets the age requirement, or skips the package entirely. Audit overrides are never delayed by this setting.
{ "minReleaseAgeDays": 3 }Runs the package manager's audit (bun audit, pnpm audit, or yarn npm audit) to detect vulnerable transitive dependencies and creates a PR pinning them to patched versions. Configure with enabled (default true) and minimumSeverity ("info", "low", "moderate", "high", "critical"; default "moderate").
{ "audit": { "enabled": false } }Where the pins land depends on the manager:
| Manager | Override file | Key format |
|---|---|---|
| Bun | package.json → overrides |
pkg@<vulnerable-range>: <fixed> |
| pnpm | pnpm-workspace.yaml → overrides |
pkg@<vulnerable-range>: <fixed> |
| Yarn | package.json → resolutions |
pkg: <fixed> |
Note the Yarn differences: resolutions selectors are keyed by package name (Yarn ignores range selectors), so multiple advisory ranges for one package collapse into the highest fixed version. Entries are treated as tool-managed when their value is an exact semver version (the format this action writes); range-valued entries are always treated as user-owned and preserved — but that also means stale exact pins for packages that are no longer vulnerable get cleaned up on the next run. Keep user resolutions range-valued if you want them left alone.
Override PRs are created with security priority (before catalog PRs), share the maxOpenPrs budget, and exclude direct catalog dependencies (handled by the catalog pipeline).
Turns on GitHub auto-merge for each PR it opens or rebuilds. Configure with enabled (default false) and mergeMethod ("squash", "merge", or "rebase"; default "squash").
{ "autoMerge": { "enabled": true, "mergeMethod": "squash" } }Two repository settings are required: Allow auto-merge under Settings > General > Pull Requests, and required status checks on the base branch (via ruleset or branch protection), which auto-merge waits for. The token needs pull-requests: write and contents: write.
Safety: auto-merge lands dependency changes with no human read. Pair it with minReleaseAgeDays (the action warns if autoMerge is on and the age is 0), and note that PRs carrying human-authored content commits are skipped (merge commits, such as GitHub's "Update branch", are ignored).
- Discover catalog definitions —
package.jsonwith acatalogfield (Bun),pnpm-workspace.yaml(pnpm),.yarnrc.yml(Yarn) — including named catalogs - Query npm for the latest stable versions, applying ignore rules, semver classification, and minimum release age
- Group updates into batches (unmatched packages get individual PRs)
- Audit vulnerable transitive dependencies and open override PRs (bun/pnpm →
overrides, yarn →resolutions) - Sync existing PRs — close stale ones, rebuild conflicting ones
- Create new PRs (override PRs first for security priority), respecting
maxOpenPrs
Each catalog location (a directory + definition file + catalog name) is processed independently, with its own branch namespace: catalog-update/<directory>/<catalog-name>/<group> for non-default catalogs. Each catalog PR includes a table of updated packages with version changes and GitHub Releases notes. Each override PR includes a summary table and collapsible advisory details. Edits to YAML definition files preserve comments and formatting.
git clone https://github.com/brandhaug/catalog-update-action.git
cd catalog-update-action
bun install
bun test
bun run lint # oxlint
bun run fmt # oxfmtMIT — see LICENSE for details.