Configuration Types

On this page 29

TypeScript interfaces and types for buddy configuration.

Everything on this page comes from BuddyConfig in src/types.ts and PackageRule in src/rules/engine.ts. A config file — buddy.config.ts, buddy.config.js or buddy.config.json — is merged over the packaged defaults and validated before any network or git work happens.

Core Configuration

BuddyConfig

Every key is optional, and every section is inert until you set it.

interface BuddyConfig {
  verbose?: boolean
  logLevel?: LogLevel
  repository?: RepositorySettings
  registries?: RegistrySettings
  security?: SecuritySettings
  schedule?: ScheduleSettings
  packages?: PackageSettings
  gates?: GateSettings
  issues?: IssueSettings
  reports?: ReportSettings
  analysis?: AnalysisSettings
  ai?: AiSettings
  notifications?: NotificationSettings
  maxPRsPerRun?: number
  pullRequest?: PullRequestSettings
  releaseNotes?: ReleaseNotesSettings
  workflows?: WorkflowSettings
  dashboard?: DashboardSettings
}

type BuddyOptions = Partial<BuddyConfig>

The nested type names above are for reading convenience; in source they are inline object types on BuddyConfig. Each is described below, in declaration order.

Logging

interface BuddyConfig {
  /** Enable verbose logging. Equivalent to `logLevel: 'debug'`. */
  verbose?: boolean

  /** How much output to emit. Overrides `verbose` when both are set. */
  logLevel?: LogLevel
}

type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug'

Use 'silent' when embedding buddy in another tool that owns its own output. logLevel can also be set with the BUDDY_LOG_LEVEL environment variable. The packaged defaults set verbose: true.

Repository

interface RepositorySettings {
  /** Git provider */
  provider: GitProviderName

  /** Repository owner/organization */
  owner: string

  /** Repository name */
  name: string

  /** Base branch for PRs */
  baseBranch?: string

  /** Access token for API operations */
  token?: string

  /** REST API base URL */
  apiUrl?: string

  /** Web base URL used for links in PR bodies and the dashboard */
  serverUrl?: string
}

Only github is implemented. gitlab and bitbucket type-check but are rejected at validation with a link to the issue tracking them, so a config typo cannot reach a workflow run.

apiUrl defaults to GITHUB_API_URL when set — GitHub Actions exports it on both github.com and Enterprise Server runners — otherwise <https://api.github.com>. Set it explicitly for GitHub Enterprise Server, for example <https://github.acme.com/api/v3>. serverUrl defaults to GITHUB_SERVER_URL, otherwise <https://github.com>.

Registries

Package registry endpoints, for private or self-hosted mirrors.

interface RegistrySettings {
  /** npm registry base URL (default: `.npmrc` `registry=`, else registry.npmjs.org) */
  npm?: string

  /** Per-scope npm registry overrides, keyed by scope including the `@` */
  npmScopes?: Record<string, string>

  /** Composer/Packagist base URL (default: packagist.org) */
  composer?: string

  /** Per-host container registry credentials, keyed by host */
  docker?: Record<string, {
    username?: string
    /** Environment variable holding the password */
    passwordEnv?: string
    /** Environment variable holding a pre-issued bearer token */
    tokenEnv?: string
  }>
}

Docker secrets are referenced by environment variable name, never inlined, so a registry can be committed to the repository without committing the credential that reaches it. ghcr.io falls back to GITHUB_TOKEN automatically, so private GHCR images work in Actions with no config.

Security

interface SecuritySettings {
  /** Query the OSV.dev advisory database (default: true) */
  enabled?: boolean

  /** Move advisory-resolving updates to the front of the queue (default: true) */
  prioritize?: boolean

  /** Label name associated with advisory-resolving PRs (default: `security`) */
  label?: string

  /** Minimum severity to act on (default: `low`, i.e. everything) */
  minimumSeverity?: VulnerabilitySeverity
}

type VulnerabilitySeverity = 'low' | 'moderate' | 'high' | 'critical'

Disable enabled for fully offline runs. prioritize is what lets a security update survive the maxPRsPerRun cap. label is the label name that auto-merge's security-only condition looks for on a pull request.

Schedule

interface ScheduleSettings {
  /** Cron expression for scheduled runs */
  cron?: string

  /** Time zone for scheduling */
  timezone?: string
}

Per-update scheduling windows live on packages.rules[].schedule, not here.

Packages

interface PackageSettings {
  /** Update strategy for dependencies */
  strategy: 'major' | 'minor' | 'patch' | 'all'

  /** Packages to ignore */
  ignore?: string[]

  /** File/directory paths to ignore using glob patterns */
  ignorePaths?: string[]

  /** Packages to pin to specific versions */
  pin?: Record<string, string>

  /** Group related packages together */
  groups?: PackageGroup[]

  /** Include prerelease versions (alpha, beta, rc, etc.) */
  includePrerelease?: boolean

  /** Exclude major version updates (even if strategy allows them) */
  excludeMajor?: boolean

  /** Respect "latest" and "*" version indicators (default: true) */
  respectLatest?: boolean

  /** Minimum age in minutes a version must have before installation (default: 0) */
  minimumReleaseAge?: number

  /** Package names exempt from the minimum release age */
  minimumReleaseAgeExclude?: string[]

  /** Conditional rules applied to matching updates */
  rules?: PackageRule[]

  /** Report updates held back by a range declared elsewhere (default: true) */
  detectResolutionDrift?: boolean
}

A strategy names the greatest semver impact a run may propose, and admits everything gentler beneath it:

strategyAdmits
allmajor, minor and patch
majormajor only
minorminor and patch
patchpatch only

rules are evaluated in order and later matches override earlier ones per field, so a broad rule can set a default and a narrow one refine it.

detectResolutionDrift surfaces packages held below their latest version by a range declared elsewhere in the dependency tree. These cannot be fixed by updating this repository — somebody has to widen a range in the dependant — so they are reported on the dashboard rather than turned into pull requests.

PackageGroup

interface PackageGroup {
  /** Group name */
  name: string

  /** Package patterns to include */
  patterns: string[]

  /** Update strategy for this group */
  strategy?: 'major' | 'minor' | 'patch' | 'all'
}

A group has exactly these three fields, and only decides batching — which packages share a pull request. Anything conditional belongs in packages.rules.

Gates

Pre-merge gates and post-merge actions.

interface GateSettings {
  /** Require a conventional-commit pull request title */
  titleFormat?: 'off' | 'warning' | 'error'

  /** Require a description, optionally with named sections */
  description?: {
    mode: 'off' | 'warning' | 'error'
    requireSections?: string[]
  }

  /** Block dependencies by licence, advisory or deprecation */
  dependencyGate?: {
    mode: 'off' | 'warning' | 'error'
    licenseAllowlist?: string[]
    blockVulnerable?: boolean
    blockDeprecated?: boolean
    /** Block base images whose release cycle no longer gets security fixes */
    blockEol?: boolean
  }

  /** Check that a change addresses the issue it says it closes */
  linkedIssue?: 'off' | 'warning' | 'error'

  /** Repository-specific natural-language assertions */
  custom?: Array<{
    name: string
    assertion: string
    mode?: 'off' | 'warning' | 'error'
  }>

  /** What to do once a pull request merges */
  postMerge?: {
    changelog?: { enabled?: boolean, path?: string }
    commentOnIssues?: boolean
    refreshDashboard?: boolean
  }
}

The deterministic gates need no AI. The assertion gates — linkedIssue and custom — degrade to a neutral result rather than a pass when no provider is configured, so a check that could not run never reads as one that succeeded.

Issues

interface IssueSettings {
  /** Post the quick-links comment on new issues (default: false) */
  quickLinks?: boolean

  /** Include dependency context when the issue names a known package */
  dependencyContext?: boolean
}

Both actions are opt-in checkboxes on the posted comment: an issue is a request for a conversation as often as for code, and a bot that opens a pull request against every new issue is one a maintainer turns off in a week.

Reports

Scheduled dependency-health and activity reports.

interface ReportSettings {
  /** Generate reports (default: false) */
  enabled?: boolean

  /** Cron expression for the scheduled run */
  schedule?: string

  /** Reporting window (default: `30d`) */
  period?: '7d' | '30d' | '90d'

  /** What the AI narrative should emphasise */
  prompt?: string

  /** Title of the report issue (default: `Dependency Report`) */
  title?: string

  /** Labels applied to the report issue */
  labels?: string[]
}

The report is computed from scan results and pull request history, so it works with no AI provider. A configured provider adds a narrative around the numbers; it never produces them.

Analysis

interface AnalysisSettings {
  /** Turn all analyzers off (default: true, they are cheap and local) */
  enabled?: boolean

  /** Per-analyzer switches, keyed by name. Absent means enabled. */
  tools?: Record<string, boolean>
}

Recognised tool names are secrets, github-actions, syntax, linter, actionlint, shellcheck, hadolint and markdownlint. Analyzers run whether or not an AI provider is configured, so a repository with no key still gets secret scanning, workflow auditing and whatever external linters the runner has installed.

AI

interface AiSettings {
  /** Turn all AI features off even when a key is present */
  enabled?: boolean

  /** Which provider to use */
  provider?: 'anthropic' | 'openai' | 'google' | 'openrouter' | 'openai-compatible'

  /** Model alias or concrete ID */
  model?: string

  /** Reasoning depth to request (default: provider default) */
  effort?: 'low' | 'medium' | 'high'

  /** Environment variable holding the API key, when not the provider default */
  apiKeyEnv?: string

  /** Base URL override, for gateways and OpenAI-compatible endpoints */
  baseUrl?: string

  /** Hard ceiling on output tokens generated per run */
  maxTokensPerRun?: number

  majorUpgrades?: MajorUpgradeSettings
  review?: ReviewSettings
}

Every AI-powered feature is off unless a provider key is available, and degrades to a no-op rather than failing the run — the dependency bot works exactly as before with no AI configured.

With provider omitted, the first provider with an available API key is chosen, in the order anthropic, openai, google, openrouter. Model aliases (opus, sonnet, haiku) resolve to current Anthropic models; every other provider needs a concrete ID. BUDDY_MODEL overrides model per run. Once maxTokensPerRun is reached, further requests fail rather than spending more.

ai.majorUpgrades

interface MajorUpgradeSettings {
  /** Analyse major updates (default: false — opt in, it costs tokens) */
  enabled?: boolean

  /** Attempt the migration rather than only analysing it (default: false) */
  autoMigrate?: boolean

  /** Open as a draft below this confidence (default: `high`) */
  draftBelowConfidence?: 'high' | 'medium'

  /** Maximum agent attempts per upgrade */
  maxAttempts?: number

  /** Globs limiting which packages are analysed; empty means all majors */
  packages?: string[]
}

autoMigrate defaults off because a wrong migration is far more expensive than a missing one.

ai.review

interface ReviewSettings {
  /** Enable AI review (default: false until you opt in) */
  enabled?: boolean

  /** How thorough to be */
  profile?: 'chill' | 'assertive'

  /** Review draft pull requests too (default: false) */
  drafts?: boolean

  /** Review automatically on open and push (default: true when enabled) */
  autoReview?: boolean

  /** Skip pull requests whose title contains any of these, e.g. `wip` */
  ignoreTitleKeywords?: string[]

  /** Skip pull requests opened by these users */
  ignoreUsernames?: string[]

  /** Request changes at this severity, or never (default: never) */
  requestChangesOn?: 'never' | 'critical'

  /** Post only the summary and walkthrough, no inline findings */
  summaryOnly?: boolean

  /** Gitignore-style path filters; `!` excludes */
  pathFilters?: string[]

  /** Guidance applied when reviewing files matching a glob */
  pathInstructions?: Array<{ path: string, instructions: string }>

  /** Convention files read from the base branch, or `false` to disable */
  guidelineFiles?: string[] | false

  /** Global guidance prepended to every review */
  instructions?: string
}

chill reports only confident defects; assertive also reports lower-confidence findings, marked so they can be filtered downstream. pathFilters apply on top of the built-in exclusions for lock files and build output. guidelineFiles defaults to the usual convention files — CLAUDE.md, AGENTS.md, .cursorrules and similar — and a list overrides those defaults.

Notifications

interface NotificationSettings {
  slack?: { webhookEnv?: string, events?: string[] }
  discord?: { webhookEnv?: string, events?: string[] }

  /** Signed JSON POSTs to your own endpoints */
  webhooks?: Array<{ url: string, secretEnv?: string, events?: string[] }>
}

Credentials are referenced by environment variable name, never inlined, so a destination can be committed without committing its credential.

Pull Requests

interface BuddyConfig {
  /** Maximum number of PRs to create per workflow run (default: 10) */
  maxPRsPerRun?: number

  pullRequest?: PullRequestSettings
}

interface PullRequestSettings {
  /** Commit message format */
  commitMessageFormat?: string

  /** PR title format */
  titleFormat?: string

  /** PR body template */
  bodyTemplate?: string

  /** Auto-merge settings */
  autoMerge?: {
    enabled: boolean
    strategy: 'merge' | 'squash' | 'rebase'
    conditions?: string[]
    requireGreenCI?: boolean
    optOutLabel?: string
  }

  /** Reviewers to assign */
  reviewers?: string[]

  /** GitHub teams to request review from, by team slug */
  teamReviewers?: string[]

  /** Assignees to assign */
  assignees?: string[]

  /** Labels to add */
  labels?: string[]
}

reviewers, assignees and labels are plain string arrays. teamReviewers takes team slugs without the organisation prefix. Reviewers and assignees configured here are unioned with whatever the matching packages.rules contributed for the updates in the pull request.

The labels a generated pull request carries are derived from its update types and the ecosystems it touches — dependencies, major/minor/patch, npm, composer, zig, system, github-actions, plus the package name itself on a single-package PR — unioned with the labels effect of any matching packages.rules. Per-update labelling therefore belongs in packages.rules[].labels.

pullRequest.autoMerge

conditions accepts patch-only, minor-only, security-only and all. A pull request qualifies when any listed condition accepts it. An empty or missing list means nothing auto-merges — the safe reading of a half-written config. Update types are read from the metadata manifest embedded in the PR body, not from its title, and a PR whose manifest is missing or truncated is never eligible.

requireGreenCI defaults to true and is only meaningful on repositories without branch protection, where buddy merges directly instead of handing the pull request to GitHub's own auto-merge queue. optOutLabel defaults to no-auto-merge; a pull request carrying it never merges unattended.

Release Notes

interface ReleaseNotesSettings {
  /** Enable release notes in PRs (default: true) */
  enabled?: boolean

  /** Sanitize GitHub references (#123, issue/PR URLs) to prevent spam notifications (default: true) */
  sanitizeReferences?: boolean

  /** Maximum number of releases to show per package (default: 3) */
  maxReleases?: number

  /** Maximum character length per release body (default: 1000) */
  maxBodyLength?: number

  /** Include compare links between versions (default: true) */
  includeCompareLinks?: boolean
}

Workflows

interface WorkflowSettings {
  /** Enable workflow generation */
  enabled?: boolean

  /** Output directory for workflows (default: `.github/workflows`) */
  outputDir?: string

  /** Workflow templates to generate */
  templates?: {
    comprehensive?: boolean
    daily?: boolean
    weekly?: boolean
    monthly?: boolean
    docker?: boolean
    monorepo?: boolean
  }

  /** Custom workflow configurations */
  custom?: Array<{
    name: string
    schedule: string
    strategy?: 'major' | 'minor' | 'patch' | 'all'
    autoMerge?: boolean
    reviewers?: string[]
    assignees?: string[]
    labels?: string[]
  }>
}

Dashboard

interface DashboardSettings {
  /** Enable dependency dashboard */
  enabled?: boolean

  /** Dashboard title (default: `Dependency Dashboard`) */
  title?: string

  /** Dashboard body template */
  bodyTemplate?: string

  /** Labels to add to dashboard issue */
  labels?: string[]

  /** Assignees to assign to dashboard issue */
  assignees?: string[]

  /** Include package.json dependencies */
  includePackageJson?: boolean

  /** Include dependency files (deps.yaml, etc.) */
  includeDependencyFiles?: boolean

  /** Include GitHub Actions */
  includeGitHubActions?: boolean

  /** Show open PRs section */
  showOpenPRs?: boolean

  /** Show detected dependencies section */
  showDetectedDependencies?: boolean

  /** Show deprecated dependencies section */
  showDeprecatedDependencies?: boolean

  /** Issue number to update (if it exists) */
  issueNumber?: number

  /** Pin the dashboard issue to the top of the issue list (default: false) */
  pin?: boolean
}

GitHub allows at most three pinned issues per repository; when that limit is already reached the dashboard is still created, just unpinned.

Package Rules

PackageRule

The main extension point. Rules live on packages.rules and are the place for everything that varies per update.

interface PackageRule {
  // Matchers — all present matchers must match (AND within a rule)
  /** Package names or globs */
  matchPackages?: string[]
  matchEcosystems?: RuleEcosystem[]
  matchDepTypes?: string[]
  matchUpdateTypes?: Array<'major' | 'minor' | 'patch'>
  /** Globs on the manifest path, for monorepo directories */
  matchFiles?: string[]
  /** Semver range the currently installed version must satisfy */
  matchCurrentVersion?: string
  /** Cron expression describing when these updates may be proposed */
  schedule?: string
  /** IANA time zone `schedule` is written in (default: the runner's) */
  scheduleTimezone?: string

  // Effects
  /** `false` drops matching updates entirely */
  enabled?: boolean
  strategy?: 'major' | 'minor' | 'patch' | 'all'
  groupName?: string
  labels?: string[]
  reviewers?: string[]
  assignees?: string[]
  autoMerge?: boolean
  /** Minutes a version must have been published, overriding the global */
  minimumReleaseAge?: number
  /** Ordering within the per-run PR cap; higher goes first */
  prPriority?: number
  /** Attempt the migration for matching majors, overriding the global */
  autoMigrate?: boolean
}

type RuleEcosystem =
  | 'npm' | 'composer' | 'github-actions' | 'docker' | 'pkgx'
  | 'zig' | 'python' | 'rust' | 'go' | 'ruby'

Matchers combine with AND, so a rule with several matchers is a conjunction. A rule with no matchers at all matches everything, which is why validation rejects it and asks for an explicit matcher rather than accepting silence. Write the deliberate catch-all as matchPackages: ['**']: a glob * stops at /, so it matches react but not @types/node.

matchCurrentVersion lets a rule target a version series rather than a package — holding back everything still on <2.0.0 while letting the rest through. Declared versions are stripped to their concrete floor first, so ^1.2.3 is tested as 1.2.3.

schedule describes a window, not a firing minute: 0 0 * * 6,0 holds updates back on a weekday run and releases them on a weekend one.

How effects combine

labels, reviewers and assignees accumulate across every matching rule. A package matching both a "security" and a "frontend" rule carries both sets, rather than whichever rule happened to be last.

rules: [
  { matchUpdateTypes: ['patch'], labels: ['patch-update'] },
  { matchPackages: ['react*'], labels: ['frontend'], reviewers: ['ui-team'] },
]
// A patch update to react-dom carries both labels and the ui-team reviewer.

Every other effect — enabled, strategy, groupName, autoMerge, minimumReleaseAge, prPriority, autoMigrate — is a per-field override, and the last matching rule wins.

When several updates share one pull request, their resolved effects are merged for the group: labels, reviewers and assignees union; prPriority takes the maximum; autoMerge requires every update to have opted in, because one package that must not merge unattended has to hold back the whole PR; autoMigrate needs only one, because migrating what can be migrated leaves the rest unchanged.

Configuration Validation

import { assertValidConfig, formatConfigIssues, validateConfig } from '@buddysh/buddy'

/** Every problem found; an empty array means the config is valid. */
function validateConfig(config: BuddyConfig): ConfigIssue[]

/** Render issues as one human-readable block, one per line. */
function formatConfigIssues(issues: readonly ConfigIssue[]): string

/** Validate and throw a ConfigurationError listing every issue. */
function assertValidConfig(config: BuddyConfig): void

interface ConfigIssue {
  /** Dotted path to the offending key, e.g. `packages.groups[0].patterns` */
  path: string

  /** What is wrong and what was expected */
  message: string
}
const issues = validateConfig(config)
if (issues.length > 0)
  console.error(formatConfigIssues(issues))

getConfig() calls assertValidConfig for you, before any network or git work happens. A malformed strategy or a group with no patterns otherwise produces a run that silently does the wrong thing and reports success.

Example Configurations

Basic Configuration

import type { BuddyConfig } from '@buddysh/buddy'

const config: BuddyConfig = {
  repository: {
    provider: 'github',
    owner: 'myorg',
    name: 'myproject',
    token: process.env.GITHUB_TOKEN,
  },
  packages: {
    strategy: 'minor',
    ignore: ['react'],
  },
  pullRequest: {
    reviewers: ['team-lead'],
  },
}

export default config

Advanced Configuration

import type { BuddyConfig } from '@buddysh/buddy'

const config: BuddyConfig = {
  maxPRsPerRun: 5,

  packages: {
    strategy: 'minor',
    ignorePaths: ['examples/**', 'packages/test-*/**'],

    // Groups decide which packages share a pull request.
    groups: [
      {
        name: 'React Ecosystem',
        patterns: ['react', 'react-dom', '@types/react'],
        strategy: 'patch',
      },
    ],

    // Rules decide everything that varies per update: labels, reviewers,
    // auto-merge, release age, ordering and scheduling windows.
    rules: [
      {
        matchPackages: ['react', 'react-dom'],
        reviewers: ['frontend-team'],
        autoMerge: false,
      },
      {
        matchUpdateTypes: ['patch'],
        labels: ['patch-update'],
        prPriority: 10,
      },
      {
        matchUpdateTypes: ['major'],
        labels: ['major-update'],
        schedule: '0 0 * * 6,0', // weekends only
        prPriority: -10,
      },
    ],
  },

  pullRequest: {
    reviewers: ['maintainer'],
    autoMerge: {
      enabled: true,
      strategy: 'squash',
      conditions: ['patch-only'],
      requireGreenCI: true,
    },
  },

  schedule: {
    cron: '0 2 * * *',
    timezone: 'UTC',
  },
}

export default config

See Configuration Guide for detailed configuration examples and best practices, and scheduling for the full matcher and effect tables.

Suggest a change to this page

Last updated: