Skip to content

Plugin System ​

bonvoy's core is intentionally minimal: a hook system + workspace detection + config loading. All real functionality is implemented via plugins.

How It Works ​

Plugins tap into lifecycle hooks using tapable (the same library webpack uses). When you run bonvoy shipit, the hooks fire in order and each plugin does its part.

shipit
  │
  ├─ modifyConfig        ← plugins can modify config
  ├─ beforeShipIt        ← pre-flight checks
  ├─ getVersion          ← determine version bump (conventional plugin)
  ├─ validateRepo        ← verify tags/versions don't exist
  ├─ version             ← apply version bumps
  ├─ afterVersion
  ├─ beforeChangelog
  ├─ generateChangelog   ← create changelog content (changelog plugin)
  ├─ afterChangelog
  ├─ beforePublish       ← git commit/tag/push (git plugin)
  ├─ publish             ← npm publish (npm plugin)
  ├─ afterPublish
  ├─ beforeRelease
  ├─ makeRelease         ← GitHub/GitLab release (github/gitlab plugin)
  └─ afterRelease

Default Plugins ​

These are loaded automatically — you don't need to install or configure them:

PluginWhat it does
conventionalParses conventional commits → version bumps
changelogGenerates CHANGELOG.md per package
gitCommits, tags, pushes
npmPublishes to npm with OIDC provenance
githubCreates GitHub releases

Optional Plugins ​

Install and configure these when needed:

PluginWhat it does
gitlabGitLab releases and merge requests
changesetChangeset-compatible workflow
execRun custom shell commands at any hook
aiAI-generated release notes summary
notificationsSlack, Discord, Telegram, Teams

Writing a Plugin ​

A plugin is a class with a name and an apply method:

typescript
import type { BonvoyPlugin } from '@bonvoy/core';
import type { Bonvoy } from '@bonvoy/core';

export default class MyPlugin implements BonvoyPlugin {
  name = 'my-plugin';

  apply(bonvoy: Bonvoy): void {
    bonvoy.hooks.afterRelease.tap(this.name, (context) => {
      console.log(`Released ${context.changedPackages.length} packages!`);
    });
  }
}

Using Config ​

Plugins can accept configuration via the constructor:

typescript
interface MyPluginConfig {
  webhookUrl: string;
  silent?: boolean;
}

export default class MyPlugin implements BonvoyPlugin {
  name = 'my-plugin';
  private config: MyPluginConfig;

  constructor(config: MyPluginConfig) {
    this.config = config;
  }

  apply(bonvoy: Bonvoy): void {
    bonvoy.hooks.afterRelease.tap(this.name, (context) => {
      if (!context.isDryRun) {
        // send webhook notification
      }
    });
  }
}

Users configure it in bonvoy.config.js:

javascript
export default {
  plugins: [
    ['my-plugin', { webhookUrl: 'https://...' }],
  ],
};

Async Hooks ​

Use tapPromise for async operations:

typescript
apply(bonvoy: Bonvoy): void {
  bonvoy.hooks.afterRelease.tapPromise(this.name, async (context) => {
    await fetch(this.config.webhookUrl, {
      method: 'POST',
      body: JSON.stringify({ packages: context.changedPackages }),
    });
  });
}

Available Hooks ​

See the Hooks Reference for the complete list with type signatures.

Released under the MIT License.