The plugin system provides types for creating and integrating custom rules and functionality through the commitlint plugin architecture. This enables extending commitlint with organization-specific rules and validation logic.
interface Plugin {
rules: {
[ruleName: string]: Rule | AsyncRule | SyncRule;
};
}A plugin is an object containing a collection of custom rules:
type PluginRecords = Record<string, Plugin>;Registry of loaded plugins indexed by plugin name. This is the resolved form after plugin loading and registration.
type BaseRule<Value = never, Type extends RuleType = "either"> = (
parsed: Commit,
when?: RuleConfigCondition,
value?: Value,
) => Type extends "either"
? RuleOutcome | Promise<RuleOutcome>
: Type extends "async"
? Promise<RuleOutcome>
: Type extends "sync"
? RuleOutcome
: never;Generic rule function signature:
RuleOutcome for sync rules, Promise<RuleOutcome> for async rulestype Rule<Value = never> = BaseRule<Value, "either">;
type AsyncRule<Value = never> = BaseRule<Value, "async">;
type SyncRule<Value = never> = BaseRule<Value, "sync">;Specialized rule function types:
type RuleOutcome = Readonly<[boolean, string?]>;Rule execution result:
true if rule passes, false if rule failstype RuleType = "async" | "sync" | "either";Execution type constraints for rules to ensure proper async handling.
import { Plugin, Rule, RuleOutcome } from "@commitlint/types";
// Simple synchronous rule
const requireJiraTicket: Rule = (parsed, when) => {
const hasJiraTicket = /[A-Z]+-\d+/.test(parsed.subject || "");
const shouldHave = when === "always";
if (shouldHave && !hasJiraTicket) {
return [false, "Subject must contain JIRA ticket (e.g., ABC-123)"];
}
if (!shouldHave && hasJiraTicket) {
return [false, "Subject must not contain JIRA ticket"];
}
return [true];
};
// Plugin definition
const jiraPlugin: Plugin = {
rules: {
"jira-ticket-in-subject": requireJiraTicket
}
};import { Plugin, AsyncRule } from "@commitlint/types";
// Async rule that validates against external service
const validateTicketExists: AsyncRule<{ apiUrl: string }> = async (parsed, when, value) => {
if (when !== "always" || !value?.apiUrl) {
return [true];
}
const ticketMatch = parsed.subject?.match(/([A-Z]+-\d+)/);
if (!ticketMatch) {
return [false, "No ticket found in subject"];
}
const ticketId = ticketMatch[1];
try {
const response = await fetch(`${value.apiUrl}/tickets/${ticketId}`);
if (!response.ok) {
return [false, `Ticket ${ticketId} does not exist or is not accessible`];
}
return [true];
} catch (error) {
return [false, `Failed to validate ticket ${ticketId}: ${error.message}`];
}
};
const ticketValidationPlugin: Plugin = {
rules: {
"ticket-exists": validateTicketExists
}
};import { Plugin, Rule, AsyncRule, SyncRule } from "@commitlint/types";
// Synchronous rules
const noWipCommits: SyncRule = (parsed, when) => {
const isWip = /^wip|^WIP/.test(parsed.subject || "");
if (when === "never" && isWip) {
return [false, "WIP commits are not allowed"];
}
return [true];
};
const requireCoAuthor: Rule<{ required: boolean }> = (parsed, when, value) => {
const hasCoAuthor = /Co-authored-by:/i.test(parsed.body || "");
const shouldRequire = when === "always" && value?.required;
if (shouldRequire && !hasCoAuthor) {
return [false, "Commit must include Co-authored-by trailer"];
}
return [true];
};
// Async rule for validation
const validateBranchName: AsyncRule<{ allowedPatterns: string[] }> = async (parsed, when, value) => {
if (when !== "always" || !value?.allowedPatterns) {
return [true];
}
// Simulate getting current branch (in real implementation, use git)
const currentBranch = process.env.GIT_BRANCH || "main";
const isAllowed = value.allowedPatterns.some(pattern => {
const regex = new RegExp(pattern);
return regex.test(currentBranch);
});
if (!isAllowed) {
return [false, `Branch "${currentBranch}" does not match allowed patterns`];
}
return [true];
};
const organizationPlugin: Plugin = {
rules: {
"no-wip-commits": noWipCommits,
"require-co-author": requireCoAuthor,
"validate-branch-name": validateBranchName
}
};import { Plugin, UserConfig } from "@commitlint/types";
// Plugin as object
const customPlugin: Plugin = {
rules: {
"custom-rule": (parsed, when, value) => [true]
}
};
// Configuration using plugins
const config: UserConfig = {
// Load plugins by name (must be installed)
plugins: [
"@my-org/commitlint-plugin",
"commitlint-plugin-custom"
],
// Use custom plugin object
plugins: [customPlugin],
// Mixed plugins
plugins: [
"@my-org/commitlint-plugin", // npm package
customPlugin, // inline plugin
"local-plugin" // local module
],
// Configure plugin rules
rules: {
// Standard rules
"type-enum": [2, "always", ["feat", "fix"]],
// Plugin rules (prefixed with plugin scope)
"jira-ticket-in-subject": [2, "always"],
"ticket-exists": [1, "always", { apiUrl: "https://api.company.com" }],
"require-co-author": [1, "always", { required: true }],
"validate-branch-name": [2, "always", {
allowedPatterns: ["^feature/", "^bugfix/", "^hotfix/"]
}]
}
};import { Plugin, Rule, RuleOutcome } from "@commitlint/types";
// Template for creating robust plugins
function createPlugin(options: {
prefix?: string;
defaultSeverity?: number;
}): Plugin {
const { prefix = "", defaultSeverity = 2 } = options;
// Helper to create consistent rule names
const ruleName = (name: string) => prefix ? `${prefix}-${name}` : name;
// Helper to validate rule configuration
const validateConfig = <T>(value: T, validator: (v: T) => boolean): RuleOutcome => {
if (!validator(value)) {
return [false, "Invalid rule configuration"];
}
return [true];
};
// Example rule with validation
const exampleRule: Rule<{ pattern: string; message?: string }> = (parsed, when, value) => {
// Validate configuration
if (value && typeof value.pattern !== "string") {
return [false, "Rule requires 'pattern' configuration"];
}
const pattern = value?.pattern || ".*";
const message = value?.message || `Subject must match pattern: ${pattern}`;
const regex = new RegExp(pattern);
const matches = regex.test(parsed.subject || "");
if (when === "always" && !matches) {
return [false, message];
}
if (when === "never" && matches) {
return [false, `Subject must not match pattern: ${pattern}`];
}
return [true];
};
return {
rules: {
[ruleName("example")]: exampleRule
}
};
}
// Usage
const myPlugin = createPlugin({
prefix: "myorg",
defaultSeverity: 1
});import { Plugin, Rule } from "@commitlint/types";
// Test helper for plugin rules
function testRule(
rule: Rule,
commit: { subject?: string; body?: string },
when: "always" | "never" = "always",
value?: unknown
) {
const parsed = {
subject: commit.subject || "",
body: commit.body || "",
header: commit.subject || "",
type: null,
scope: null,
footer: null,
notes: [],
references: [],
mentions: [],
revert: null
};
return rule(parsed, when, value);
}
// Example tests
const plugin: Plugin = {
rules: {
"require-ticket": (parsed, when) => {
const hasTicket = /TICKET-\d+/.test(parsed.subject || "");
if (when === "always" && !hasTicket) {
return [false, "Subject must contain ticket number"];
}
return [true];
}
}
};
// Test cases
console.log(testRule(
plugin.rules["require-ticket"],
{ subject: "fix: something" }
)); // [false, "Subject must contain ticket number"]
console.log(testRule(
plugin.rules["require-ticket"],
{ subject: "fix: TICKET-123 something" }
)); // [true]