Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
A
Axiom
/Marketplace/christiangeorgelucas/conventional-commit-tools
Sign in

christiangeorgelucas/conventional-commit-tools

v0.1.2TypeScript

The conventional-commit-tools package provides a set of composable Axiom nodes for parsing, validating, and extracting structured information from Conventional Commits formatted git commit messages. It allows developers to ensure that commit messages adhere to the Conventional Commits specification, enabling better automation in changelog generation and versioning.

Published by Christian George Lucas· MIT

gitcommit-messagesconventional-commitsparsingvalidationautomationtypescript

Use cases

  • Validate commit messages before merging code
  • Extract issue references from commit logs for tracking
  • Generate changelogs based on commit history
  • Detect breaking changes in commit messages
  • Summarize multiple commits for release notes

Nodes (14)

DetectBreakingChangeunary
CommitMessageRequest·DetectBreakingChangeResult

The DetectBreakingChange node analyzes a raw commit message to determine if it indicates a breaking change according to the Conventional Commits v1.0.0 specification. It identifies the presence of specific markers or footers that signal breaking changes and returns relevant details about the findings.

View source
import { CommitMessageRequest, DetectBreakingChangeResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit, computeBreaking } from './lib';

/**
 * Detect whether a raw commit message is a breaking change: either a `!`
 * immediately after the type/scope (e.g. "feat(api)!: ...") or a
 * `BREAKING CHANGE:` / `BREAKING-CHANGE:` footer, per the Conventional
 * Commits v1.0.0 spec. `source` reports which signal(s) were present
 * ("marker" | "footer" | "both" | "none"); `description` is the breaking
 * -change footer's text if there is one, else empty. An empty message
 * yields breaking=false, source="none" — never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function detectBreakingChange(ax: AxiomContext, input: CommitMessageRequest): DetectBreakingChangeResult {
  const out = new DetectBreakingChangeResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    const { breaking, description, source } = computeBreaking(raw);
    out.setBreaking(breaking);
    out.setDescription(description);
    out.setSource(source);
  } catch {
    out.setBreaking(false);
    out.setDescription('');
    out.setSource('none');
  }
  return out;
}
DetermineReleaseTypeunary
CommitMessageRequest·DetermineReleaseTypeResult

The DetermineReleaseType node analyzes a commit message to identify the corresponding semantic versioning (semver) release type, categorizing it as 'major', 'minor', 'patch', or 'none' based on the content of the message. It ensures that only valid release types are passed to subsequent nodes, preventing errors from invalid inputs.

View source
import { CommitMessageRequest, DetermineReleaseTypeResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit, computeBreaking, releaseTypeFor, errorMessage } from './lib';

/**
 * Determine the semver release type a single commit implies: a breaking
 * change (marker or footer) -> "major", "feat" -> "minor", "fix" -> "patch",
 * anything else -> "none". The "major"/"minor"/"patch" values share
 * semver-tools' Increment node's `release_type` vocabulary — but Increment's
 * own release-type whitelist does NOT include "none", so a caller composing
 * the two should branch on `release_type != "none"` before calling Increment
 * (a commit that implies no release should simply not trigger a bump, not be
 * passed through as an invalid release type). On an empty message, returns
 * release_type="none" with `reason` explaining why — never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function determineReleaseType(ax: AxiomContext, input: CommitMessageRequest): DetermineReleaseTypeResult {
  const out = new DetermineReleaseTypeResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    const { breaking } = computeBreaking(raw);
    const { releaseType, reason } = releaseTypeFor(raw.type ?? '', breaking);
    out.setReleaseType(releaseType);
    out.setReason(reason);
  } catch (e) {
    out.setReleaseType('none');
    out.setReason(errorMessage(e, 'could not parse commit message'));
  }
  return out;
}
ExtractFootersunary
CommitMessageRequest·ExtractFootersResult

The ExtractFooters node processes a raw commit message to extract footers or trailers as token/value pairs, such as 'Reviewed-by: Alice' or 'Closes #123'. It handles continuation lines by folding them into the previous footer's value and guarantees an empty list as output if no footers are found or if the message is empty.

View source
import { CommitMessageRequest, ExtractFootersResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit, parseFooters } from './lib';

/**
 * Extract all footers/trailers from a raw commit message as token/value
 * pairs — e.g. "Reviewed-by: Alice" -> {token: "Reviewed-by", value:
 * "Alice"}, "Closes #123" -> {token: "Closes", value: "#123"},
 * "BREAKING CHANGE: ..." -> {token: "BREAKING CHANGE", value: "..."}. A
 * continuation line that doesn't start a new trailer is folded into the
 * previous trailer's value. Returns an empty list (never throws) when the
 * message has no footer, or is empty.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function extractFooters(ax: AxiomContext, input: CommitMessageRequest): ExtractFootersResult {
  const out = new ExtractFootersResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    out.setFootersList(parseFooters(raw.footer));
  } catch {
    out.setFootersList([]);
  }
  return out;
}
ExtractIssueReferencesunary
CommitMessageRequest·ExtractIssueReferencesResult

The ExtractIssueReferences node processes a raw commit message to identify and extract all issue and pull request references, including their associated actions and repository details. It returns a structured result containing each reference's raw text, action, issue number, prefix, and owner/repository information.

View source
import { CommitMessageRequest, ExtractIssueReferencesResult, IssueReference } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit } from './lib';

/**
 * Extract every issue/PR reference from a raw commit message — anywhere in
 * the header, body, or footer — e.g. "#123", "Closes #45", "fixes
 * octocat/hello-world#123". Each result carries the raw matched text, the
 * reference action if one preceded it ("Closes", "Fixes", "Resolves", ...,
 * empty if none), the issue number, the "#" prefix, and owner/repository if
 * the reference used the "owner/repo#N" cross-repo form. Returns an empty
 * list (never throws) on an empty message.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function extractIssueReferences(ax: AxiomContext, input: CommitMessageRequest): ExtractIssueReferencesResult {
  const out = new ExtractIssueReferencesResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    const refs = (raw.references ?? []).map((r) => {
      const ir = new IssueReference();
      ir.setRaw(r.raw ?? '');
      ir.setAction(r.action ?? '');
      ir.setIssue(r.issue ?? '');
      ir.setPrefix(r.prefix ?? '');
      ir.setOwner(r.owner ?? '');
      ir.setRepository(r.repository ?? '');
      return ir;
    });
    out.setReferencesList(refs);
  } catch {
    out.setReferencesList([]);
  }
  return out;
}
ExtractMentionsunary
CommitMessageRequest·ExtractMentionsResult

The ExtractMentions node processes a raw commit message to identify and extract all @mentions, such as '@octocat', which can be used for attributing contributions in changelogs. It guarantees a non-error response by returning an empty list if no mentions are found or if the message is empty.

View source
import { CommitMessageRequest, ExtractMentionsResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit } from './lib';

/**
 * Extract every @mention (e.g. "@octocat") anywhere in a raw commit message
 * — useful for crediting contributors/reviewers in generated changelogs.
 * Returns an empty list (never throws) on an empty message.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function extractMentions(ax: AxiomContext, input: CommitMessageRequest): ExtractMentionsResult {
  const out = new ExtractMentionsResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    out.setMentionsList(raw.mentions ?? []);
  } catch {
    out.setMentionsList([]);
  }
  return out;
}
ExtractScopeunary
CommitMessageRequest·ExtractScopeResult

The ExtractScope node processes a raw commit message to extract the parenthesized scope, such as 'api' from 'feat(api): ...'. It returns a result indicating whether a valid scope was found without throwing errors for invalid inputs.

View source
import { CommitMessageRequest, ExtractScopeResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit } from './lib';

/**
 * Extract just the parenthesized scope (e.g. "api" from "feat(api): ...")
 * from a raw commit message's header. `found` is false (with `scope` empty)
 * when the header has no scope, doesn't conform to "type(scope)!: subject"
 * at all, or the message is empty — never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function extractScope(ax: AxiomContext, input: CommitMessageRequest): ExtractScopeResult {
  const out = new ExtractScopeResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    const scope = raw.scope ?? '';
    out.setScope(scope);
    out.setFound(scope.length > 0);
  } catch {
    out.setScope('');
    out.setFound(false);
  }
  return out;
}
ExtractTypeunary
CommitMessageRequest·ExtractTypeResult

The ExtractType node processes a raw commit message to extract the commit type, such as 'feat' or 'fix', while ensuring it adheres to the Conventional Commits format. It returns a result indicating whether a valid type was found or not, without throwing errors for invalid inputs.

View source
import { CommitMessageRequest, ExtractTypeResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit } from './lib';

/**
 * Extract just the commit type (e.g. "feat", "fix", "docs", "chore",
 * "refactor", "test") from a raw commit message's header. `found` is false
 * (with `type` empty) when the header doesn't conform to
 * "type(scope)!: subject", or on an empty message — never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function extractType(ax: AxiomContext, input: CommitMessageRequest): ExtractTypeResult {
  const out = new ExtractTypeResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    const type = raw.type ?? '';
    out.setType(type);
    out.setFound(type.length > 0);
  } catch {
    out.setType('');
    out.setFound(false);
  }
  return out;
}
ParseCommitunary
CommitMessageRequest·ParseCommitResult

The ParseCommit node processes a raw commit message and extracts its structured components, including type, scope, subject, body, and various metadata such as issue references and mentions. It ensures that even non-conforming commit messages are parsed to the best of its ability, returning useful information without throwing errors for typical input issues.

View source
import { CommitMessageRequest, ParseCommitResult, ConventionalCommit } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit, toConventionalCommit, errorMessage } from './lib';

/**
 * Parse one raw commit message into its full structured representation:
 * type, scope, subject, body, footer trailers, breaking-change marker/footer,
 * issue references, @mentions, and revert info (if it is a
 * `Revert "..."` commit). `commit.header_matched` is false when the first
 * line does not conform to Conventional Commits' "type(scope)!: subject"
 * form — every other field is still populated best-effort from whatever the
 * underlying parser (conventional-commits-parser) could extract, so a
 * non-conventional message still returns useful structure. `ok` is false
 * (with `error` set and `commit` at its zero value) only for a hard input
 * problem: an empty message. Deterministic; never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function parseCommit(ax: AxiomContext, input: CommitMessageRequest): ParseCommitResult {
  const out = new ParseCommitResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    out.setCommit(toConventionalCommit(raw));
    out.setOk(true);
    return out;
  } catch (e) {
    out.setOk(false);
    out.setError(errorMessage(e, 'parsing commit message'));
    out.setCommit(new ConventionalCommit());
    return out;
  }
}
ParseCommitLogunary
ParseCommitLogRequest·ParseCommitLogResult

The ParseCommitLog node processes a string of commit messages separated by a specified delimiter, converting them into an array of structured commit objects while handling parse failures gracefully. It ensures that even if some fragments fail to parse, they are included as best-effort commits without throwing errors.

View source
import { ParseCommitLogRequest, ParseCommitLogResult, ConventionalCommit } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { splitLog, parseRawCommit, toConventionalCommit, placeholderCommit } from './lib';

/**
 * Parse a blob of multiple commit messages — separated by a caller-supplied
 * `delimiter` (required; e.g. "\n---\n" or a git-log-friendly separator) —
 * into an array of structured commits, in order. A fragment that fails to
 * parse still yields a best-effort commit with `header_matched=false` rather
 * than being dropped. `error` is set (with empty `commits`) only for a
 * log-level problem: an empty `delimiter`. Deterministic; never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function parseCommitLog(ax: AxiomContext, input: ParseCommitLogRequest): ParseCommitLogResult {
  const out = new ParseCommitLogResult();
  const { messages, error } = splitLog(input.getLog(), input.getDelimiter());
  if (error) {
    out.setError(error);
    out.setCount(0);
    out.setCommitsList([]);
    return out;
  }

  const commits: ConventionalCommit[] = messages.map((msg) => {
    try {
      return toConventionalCommit(parseRawCommit(msg));
    } catch {
      return placeholderCommit(msg);
    }
  });

  out.setCommitsList(commits);
  out.setCount(commits.length);
  return out;
}
ParseRevertunary
CommitMessageRequest·ParseRevertResult

The ParseRevert node analyzes a raw commit message to determine if it is a git-generated revert commit and extracts the original commit's header and the hash of the reverted commit if applicable.

View source
import { CommitMessageRequest, ParseRevertResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit } from './lib';

/**
 * Detect whether a raw commit message is a git-generated revert commit
 * (`Revert "<original header>"\n\nThis reverts commit <hash>.`) and, if so,
 * extract the original commit's header and the reverted commit's hash.
 * `is_revert` is false (with the other fields empty) for an ordinary commit,
 * or on an empty message — never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function parseRevert(ax: AxiomContext, input: CommitMessageRequest): ParseRevertResult {
  const out = new ParseRevertResult();
  try {
    const raw = parseRawCommit(input.getMessage());
    const isRevert = raw.revert != null;
    out.setIsRevert(isRevert);
    out.setRevertedHeader(raw.revert?.header ?? '');
    out.setRevertedHash(raw.revert?.hash ?? '');
  } catch {
    out.setIsRevert(false);
    out.setRevertedHeader('');
    out.setRevertedHash('');
  }
  return out;
}
SplitHeaderBodyunary
CommitMessageRequest·SplitHeaderBodyResult

The SplitHeaderBody node processes a raw commit message by separating it into its header (the first line) and body (everything following the first blank line), ensuring that it handles non-conventional messages without throwing errors. If the input message is empty, both the header and body fields will also be empty.

View source
import { CommitMessageRequest, SplitHeaderBodyResult } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';

/**
 * Split a raw commit message into its header (first line) and body
 * (everything after the first blank-line-delimited gap, trimmed) — a purely
 * structural split that works even on a non-conventional message, unlike
 * every other node in this package. Both fields are trimmed of surrounding
 * whitespace. A message with only one line (including an empty message)
 * returns an empty body — never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function splitHeaderBody(ax: AxiomContext, input: CommitMessageRequest): SplitHeaderBodyResult {
  const out = new SplitHeaderBodyResult();
  const message = input.getMessage();

  const normalized = message.replace(/\r\n/g, '\n');
  const newlineIndex = normalized.indexOf('\n');
  if (newlineIndex === -1) {
    out.setHeader(normalized.trim());
    out.setBody('');
    return out;
  }
  out.setHeader(normalized.slice(0, newlineIndex).trim());
  out.setBody(normalized.slice(newlineIndex + 1).replace(/^\n+/, '').trim());
  return out;
}
SummarizeCommitsunary
SummarizeCommitsRequest·SummarizeCommitsResult

The SummarizeCommits node processes a multi-commit log to generate a summary for changelog creation, including the total number of commits, counts per commit type, a list of breaking changes, and the overall release type based on the highest precedence of the commits.

View source
import { SummarizeCommitsRequest, SummarizeCommitsResult, ConventionalCommit, TypeCount } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { splitLog, parseRawCommit, toConventionalCommit, placeholderCommit, releaseTypeFor } from './lib';

const RELEASE_RANK: Record<string, number> = { major: 3, minor: 2, patch: 1, none: 0 };

/**
 * Summarize a multi-commit log (same `log`/`delimiter` contract as
 * ParseCommitLog) for changelog generation: `total` commits parsed, a count
 * per commit type, the full list of breaking-change commits, and
 * `overall_release_type` — the highest-precedence release type implied by
 * the set (major > minor > patch > none), i.e. the one version bump the
 * whole batch would justify. A commit type of "" (header didn't match
 * Conventional Commits form) is counted under "(none)". `error` is set (with
 * every other field at its zero value) only for a log-level problem: an
 * empty `delimiter`. Deterministic; never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function summarizeCommits(ax: AxiomContext, input: SummarizeCommitsRequest): SummarizeCommitsResult {
  const out = new SummarizeCommitsResult();
  const { messages, error } = splitLog(input.getLog(), input.getDelimiter());
  if (error) {
    out.setError(error);
    return out;
  }

  const counts = new Map<string, number>();
  const breaking: ConventionalCommit[] = [];
  let overall = 'none';

  for (const msg of messages) {
    let commit: ConventionalCommit;
    try {
      commit = toConventionalCommit(parseRawCommit(msg));
    } catch {
      commit = placeholderCommit(msg);
    }

    const type = commit.getType() || '(none)';
    counts.set(type, (counts.get(type) ?? 0) + 1);
    if (commit.getBreaking()) breaking.push(commit);

    const { releaseType } = releaseTypeFor(commit.getType(), commit.getBreaking());
    if (RELEASE_RANK[releaseType] > RELEASE_RANK[overall]) overall = releaseType;
  }

  const countsByType: TypeCount[] = Array.from(counts.entries()).map(([type, count]) => {
    const tc = new TypeCount();
    tc.setType(type);
    tc.setCount(count);
    return tc;
  });

  out.setTotal(messages.length);
  out.setCountsByTypeList(countsByType);
  out.setBreakingChangesList(breaking);
  out.setOverallReleaseType(overall);
  return out;
}
ValidateCommitunary
ValidateCommitRequest·ValidateCommitResult

The ValidateCommit node checks a raw commit message against the Conventional Commits v1.0.0 specification, identifying any formatting issues or missing required fields. It provides detailed feedback on errors and warnings, ensuring that commit messages adhere to established conventions.

View source
import { ValidateCommitRequest, ValidateCommitResult, ConventionalCommit, ValidationIssue } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { parseRawCommit, toConventionalCommit, mkIssue, errorMessage } from './lib';

/**
 * Validate a raw commit message against the Conventional Commits v1.0.0
 * specification and report exactly why it fails, if it does. Checks: the
 * header matches "type(scope)!: subject"; `type` is present (and, unless
 * `allow_any_type_case` is set, lowercase — the overwhelming real-world
 * convention, matching commitlint's default); `subject` is non-empty. A
 * commit marked breaking (`!` or a `BREAKING CHANGE:` footer) with no
 * breaking-change description is flagged as a warning, not an error — the
 * spec allows `!` alone with no accompanying footer text. `valid` is true
 * iff no `error`-severity issue was found. `commit` is always the best-effort
 * parse, even when invalid, so a caller can inspect what *was* extracted.
 * Deterministic; never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function validateCommit(ax: AxiomContext, input: ValidateCommitRequest): ValidateCommitResult {
  const out = new ValidateCommitResult();
  const issues: ValidationIssue[] = [];

  let commit: ConventionalCommit;
  try {
    commit = toConventionalCommit(parseRawCommit(input.getMessage()));
  } catch (e) {
    issues.push(mkIssue('UNPARSEABLE_MESSAGE', errorMessage(e, 'parsing commit message'), 'error'));
    out.setValid(false);
    out.setIssuesList(issues);
    out.setCommit(new ConventionalCommit());
    return out;
  }

  if (!commit.getHeaderMatched()) {
    issues.push(mkIssue('INVALID_HEADER_FORMAT', 'header does not match "type(scope)!: subject"', 'error'));
  } else {
    const type = commit.getType();
    if (!type) {
      issues.push(mkIssue('MISSING_TYPE', 'commit type is empty', 'error'));
    } else if (!input.getAllowAnyTypeCase() && type !== type.toLowerCase()) {
      issues.push(mkIssue('TYPE_NOT_LOWERCASE', `type "${type}" is not lowercase`, 'error'));
    }
    if (!commit.getSubject().trim()) {
      issues.push(mkIssue('EMPTY_SUBJECT', 'subject is empty', 'error'));
    }
  }

  if (commit.getBreaking() && !commit.getBreakingDescription().trim()) {
    issues.push(mkIssue(
      'EMPTY_BREAKING_DESCRIPTION',
      'commit is marked breaking but has no description (consider adding a "BREAKING CHANGE:" footer)',
      'warning',
    ));
  }

  out.setValid(!issues.some((i) => i.getSeverity() === 'error'));
  out.setIssuesList(issues);
  out.setCommit(commit);
  return out;
}
ValidateSubjectunary
ValidateSubjectRequest·ValidateSubjectResult

The ValidateSubject node checks a commit subject line against predefined mechanical rules to ensure it adheres to formatting standards. It verifies that the subject is non-empty, does not exceed a specified maximum length, has no leading or trailing whitespace, and does not end with a period, while flagging capitalized first letters as warnings.

View source
import { ValidateSubjectRequest, ValidateSubjectResult, ValidationIssue } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';
import { mkIssue, DEFAULT_SUBJECT_MAX_LENGTH } from './lib';

/**
 * Validate a commit subject line (the description after "type(scope): ")
 * against the common, well-defined mechanical rules — deliberately NOT
 * imperative-mood (that requires judgment, not a mechanical check): must be
 * non-empty; must not exceed `max_length` (<= 0 uses the common commitlint
 * default of 100); must not have leading/trailing whitespace; must not end
 * with a period. A subject starting with an uppercase letter is flagged as a
 * "warning" (the lowercase-first-letter convention is common but not
 * universal), not an "error" — `valid` is true iff no `error`-severity issue
 * was found. Deterministic; never throws.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function validateSubject(ax: AxiomContext, input: ValidateSubjectRequest): ValidateSubjectResult {
  const out = new ValidateSubjectResult();
  const subject = input.getSubject();
  out.setLength(subject.length);

  const maxLength = input.getMaxLength() > 0 ? input.getMaxLength() : DEFAULT_SUBJECT_MAX_LENGTH;
  const trimmed = subject.trim();
  const issues: ValidationIssue[] = [];

  if (!trimmed) {
    issues.push(mkIssue('EMPTY_SUBJECT', 'subject is empty', 'error'));
  }
  if (subject.length > maxLength) {
    issues.push(mkIssue('TOO_LONG', `subject is ${subject.length} characters, exceeds max_length ${maxLength}`, 'error'));
  }
  if (subject !== trimmed) {
    issues.push(mkIssue('LEADING_OR_TRAILING_WHITESPACE', 'subject has leading or trailing whitespace', 'error'));
  }
  if (trimmed.endsWith('.')) {
    issues.push(mkIssue('TRAILING_PERIOD', 'subject should not end with a period', 'error'));
  }
  if (trimmed && /^[A-Z]/.test(trimmed)) {
    issues.push(mkIssue('CAPITALIZED_FIRST_LETTER', 'subject starts with an uppercase letter (convention is lowercase)', 'warning'));
  }

  out.setValid(!issues.some((i) => i.getSeverity() === 'error'));
  out.setIssuesList(issues);
  return out;
}

Messages (24)

Download .proto
CommitMessageRequest
message:string

The commit message to be analyzed for determining the release type.

ConventionalCommit
header:string
header_matched:bool
type:string
scope:string
breaking:bool
breaking_description:string
breaking_source:string
subject:string
body:string
footer_raw:string
footers:[]Footer
references:[]IssueReference
mentions:[]string
is_revert:bool
revert_header:string
revert_hash:string
DetectBreakingChangeResult
breaking:bool

A boolean indicating whether the commit message is a breaking change.

description:string

The text of the breaking-change footer if present; otherwise, it is empty.

source:string

A string that reports which signal(s) were present: 'marker', 'footer', 'both', or 'none'.

DetermineReleaseTypeResult
release_type:string

The determined release type based on the commit message, which can be 'major', 'minor', 'patch', or 'none'.

reason:string

An explanation for the determined release type, particularly if it is 'none'.

ExtractFootersResult
footers:[]Footer
ExtractIssueReferencesResult
references:[]IssueReference
ExtractMentionsResult
mentions:[]string
ExtractScopeResult
scope:string

The extracted scope from the commit message, if present.

found:bool

A boolean indicating whether a valid scope was found in the commit message.

ExtractTypeResult
type:string

The extracted commit type, which can be 'feat', 'fix', 'docs', 'chore', 'refactor', or 'test'.

found:bool

A boolean indicating whether a valid commit type was found in the message.

Footer
token:string
value:string
IssueReference
raw:string
action:string
issue:string
prefix:string
owner:string
repository:string
ParseCommitLogRequest
log:string
delimiter:string

The string used to separate individual commit messages in the input.

ParseCommitLogResult
commits:[]ConventionalCommit

An array of structured commit objects derived from the parsed commit messages.

count:int32

The total number of successfully parsed commits.

error:string

An error message indicating a log-level problem, such as an empty delimiter.

ParseCommitResult
commit:ConventionalCommit

A structured representation of the parsed commit message, including its components.

ok:bool

A boolean indicating whether the parsing was successful; false only for an empty input message.

error:string

A message detailing the error encountered during parsing, if applicable.

ParseRevertResult
is_revert:bool

Indicates whether the commit message is a revert commit.

reverted_header:string

The header of the original commit that was reverted.

reverted_hash:string

The hash of the commit that was reverted.

SplitHeaderBodyResult
header:string

The first line of the commit message, representing the header.

body:string

The content following the first blank line in the commit message, representing the body.

SummarizeCommitsRequest
log:string

The multi-commit log string that needs to be summarized.

delimiter:string

The delimiter used to separate individual commits in the log.

SummarizeCommitsResult
total:int32

The total number of commits parsed from the log.

counts_by_type:[]TypeCount
breaking_changes:[]ConventionalCommit
overall_release_type:string
error:string

An error message indicating any issues encountered during log processing, set only for log-level problems.

TypeCount
type:string
count:int32
ValidateCommitRequest
message:string

The raw commit message to be validated against the Conventional Commits specification.

allow_any_type_case:bool

A boolean flag indicating whether the commit type can be in any case (true) or must be lowercase (false).

ValidateCommitResult
valid:bool

A boolean indicating whether the commit message is valid according to the specified rules.

issues:[]ValidationIssue

A list of validation issues encountered during the validation process, detailing errors and warnings.

commit:ConventionalCommit

A best-effort parsed representation of the commit, regardless of its validity.

ValidateSubjectRequest
subject:string

The commit subject line that needs to be validated.

max_length:int32

The maximum allowed length for the subject; if set to 0, defaults to 100 characters.

ValidateSubjectResult
valid:bool
issues:[]ValidationIssue
length:int32
ValidationIssue
code:string
message:string
severity:string

Use as a tool in any AI agent

christiangeorgelucas/conventional-commit-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.

claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"

Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.

MCP server setup & other clients →

Use this package

Sign in to import christiangeorgelucas/conventional-commit-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.

Sign in to useOpen editor

Stats

Error rate (1h)0.0%
Back to marketplace