nemui

commitlint でコミットコメントをルール化する

2026/02/17
  • #github
  • #commitlint

commitlint というOSSでコミットコメントのルール化ができます。

commitlint の導入方針

以下の方針で commitlint を導入していきます。

  • 基本的なルールはConventional Commitsに従う
  • type は共通リポジトリで管理し、scope は個別リポジトリで管理する

共通リポジトリの作成

リポジトリの参照方法

GitHub でソースコードを管理する場合、共通リポジトリの npm パッケージを個別リポジトリから参照する方法がいくつかあります。

  1. GitHub Packages に publish し、参照元リポジトリ側で regisry を追加して参照する
  2. GitHub リポジトリを SSH/HTTP で直接参照する
  3. その他 (npm に publish する等)

1の場合はPersonal access token (classic)を、2の場合は SSHキー等を認証に使用します。 1の方が SemVer を使用した細やかなバージョン管理や、PAT による権限管理ができるメリットがある為、今回は1の方法で、privateリポジトリを使用します。
※ private リポジトリの場合は利用制限があるので注意が必要です。

共通ルールの作成

mise を使用して環境構築していきます。

mise use node@24 pnpm prek

npm パッケージを追加します。

pnpm init
pnpm add -D @commitlint/cli @commitlint/config-conventional @commitlint/lint vitest

commitlint の設定を行います。 日本語の subject, body に対応する為、caseのチェックを外しておきます。

commitlint.config.js

export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', [
      'build',
      'ci',
      'docs',
      'feat',
      'fix',
      'perf',
      'refactor',
      'style',
      'test',
      'chore',
    ]],
    'subject-case': [0],
    'subject-full-stop': [0],
    'body-case': [0],
    'subject-max-length': [2, 'always', 72],
  }
};

テストも書いておきます。

commitlint.config.test.js

import { describe, it, expect } from 'vitest';
import lint from '@commitlint/lint';
import config from './commitlint.config.js';

const lintMessage = (message) => lint(message, config.rules);

describe('commitlint.config', () => {
  describe('type-enum', () => {
    const validTypes = [
      'build',
      'ci',
      'docs',
      'feat',
      'fix',
      'perf',
      'refactor',
      'style',
      'test',
      'chore',
    ];

    it.each(validTypes)('should pass for valid type: %s', async (type) => {
      const result = await lintMessage(`${type}: add new feature`);
      expect(result.valid).toBe(true);
    });

    it('should fail for invalid type', async () => {
      const result = await lintMessage('invalid: some message');
      expect(result.valid).toBe(false);
      expect(result.errors.some((e) => e.name === 'type-enum')).toBe(true);
    });
  });

  describe('subject-full-stop', () => {
    it('should pass for subject ending with a period', async () => {
      const result = await lintMessage('feat: 句読点を含む日本語を許可する。');
      expect(result.valid).toBe(true);
    });
  });

  describe('body-case', () => {
    it('should pass for body with uppercase text', async () => {
      const result = await lintMessage('feat: add new feature\n\nBODYに日本語を使用可能。');
      expect(result.valid).toBe(true);
    });
  });

  describe('subject-max-length', () => {
    it('should pass for subject within 72 characters', async () => {
      const subject = 'a'.repeat(72);
      const result = await lintMessage(`feat: ${subject}`);
      expect(result.valid).toBe(true);
    });

    it('should pass for subject with Japanese characters', async () => {
      const result = await lintMessage('feat: 日本語で説明を記載可能');
      expect(result.valid).toBe(true);
    });

    it('should fail for subject exceeding 72 characters', async () => {
      const subject = 'a'.repeat(73);
      const result = await lintMessage(`feat: ${subject}`);
      expect(result.valid).toBe(false);
      expect(result.errors.some((e) => e.name === 'subject-max-length')).toBe(true);
    });
  });
});

package.json の main, files, scripts, type, repository, publishConfig を追加・修正し、テストを流しておきます。

package.json

{
  "name": "@user_name/commitlint-config",
  "version": "1.0.0",
  "description": "",
  "main": "commitlint.config.js",
  "files": [
    "commitlint.config.js"
  ],
  "scripts": {
    "test": "vitest run"
  },
  "type": "module",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.29.1",
  "repository": {
    "type": "git",
    "url": "https://github.com/<user_name>/<repository_name>.git"
  },
  "publishConfig": {
    "registry": "https://npm.pkg.github.com"
  },
  "devDependencies": {
    "@commitlint/cli": "^20.4.1",
    "@commitlint/config-conventional": "^20.4.1",
    "@commitlint/lint": "^20.4.1",
    "vitest": "^4.0.18"
  }
}
pnpm test

結果

 commitlint.config.test.js (16 tests) 18ms
 commitlint.config (16)
 type-enum (11)
 should pass for valid type: build 5ms
 should pass for valid type: ci 1ms
 should pass for valid type: docs 1ms
 should pass for valid type: feat 0ms
 should pass for valid type: fix 1ms
 should pass for valid type: perf 0ms
 should pass for valid type: refactor 1ms
 should pass for valid type: style 0ms
 should pass for valid type: test 1ms
 should pass for valid type: chore 1ms
 should fail for invalid type 1ms
 subject-full-stop (1)
 should pass for subject ending with a period 1ms
 body-case (1)
 should pass for body with uppercase text 2ms
 subject-max-length (3)
 should pass for subject within 72 characters 1ms
 should pass for subject with Japanese characters 1ms
 should fail for subject exceeding 72 characters 1ms

 Test Files  1 passed (1)
      Tests  16 passed (16)
   Start at  16:06:31
   Duration  281ms (transform 36ms, setup 0ms, import 141ms, tests 18ms, environment 0ms)

コミット前テストの追加

GitのHooksを使用して、コミット前のテストを追加します。

.pre-commit-config.yaml

repos:
  - repo: local
    hooks:
      - id: commitlint
        name: commitlint
        entry: pnpm exec commitlint --edit
        language: system
        stages: [commit-msg]

設定後、Gitにフックをインストールします。

prek install --hook-type commit-msg

コミット前のテストが動いていることを確認しておきます。

git add .
git commit -m "invalid: test"

結果

commitlint...............................................................Failed
- hook id: commitlint
- exit code: 1

   input: invalid: test
   type must be one of [build, ci, docs, feat, fix, perf, refactor, style, test, chore] [type-enum]

   found 1 problems, 0 warnings
   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

GitHub Actions の設定 (commitlint)

main へのPRについて、test と commitlint を実行するように設定を行います。

.github/workflows/commitlint.yaml

name: Commitlint

on:
  pull_request:
    branches: [main]

jobs:
  commitlint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - run: pnpm test

      - name: Validate commits
        env:
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        run: pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA" --verbose

GitHub Actions の設定 (publish)

Release を発行した際に GitHub Packages に publish するように GitHub Actions の設定を行います。

.github/workflows/publish.yaml

name: Publish to GitHub Packages

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: 'pnpm'
          registry-url: 'https://npm.pkg.github.com'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - run: pnpm test

      - run: pnpm publish --no-git-checks
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

dependabot の設定

dependabot を設定しておきます。 サプライチェーン攻撃を緩和する為、cooldown を設定します。
また、dependabot が 適当なコミットメッセージを書いてチェックに引っ掛からないように commit-message を設定します。参考

.github/dependabot.yaml

version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    cooldown:
      default-days: 2
    commit-message:
      prefix: "build"

同様に pnpm の minimumReleaseAge も設定しておきます。

pnpm-workspace.yaml

packages: []
pnpm:
  updateConfig:
    minimumReleaseAge: 2880

GitHub に push し、PRを作成する

push して、PRを作成してみます。

git switch -c add-config
git add .
git commit -m 'feat: add commitlint config'
git push

gh pr new -f

チェックが動いていることを確認し、マージしておきます。

GitHub Packages の発行

Release を作成すると、自動的に GitHub Actions が起動し、GitHub Packages が発行されます。
tag と Release 名はv1.0.0にしておきます。

発行が完了したら、GitHubのリポジトリトップページの右下にある Packages のリンクからパッケージを開き、右下の[Packages settings]からパッケージの設定を開きます。

Manage Actions access の [Add Repository] から参照したいリポジトリを追加します。


参照元リポジトリの設定

Personal Access Token (classic) を作成する

read:packages権限を持ったPATを作成し、ghp_から始まるトークン文字列をNPM_TOKEN等の環境変数に設定しておきます。
※ 必要に応じて、.bashrc等にも設定しておきます。

npmパッケージを設定する

GitHub Packages のパッケージを追加する為にリポジトリの設定等を入れていきます。
共通リポジトリは minimumReleaseAge の対象外としておきます。

.npmrc

@<user_name>:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
minimum-release-age=2880
minimum-release-age-exclude=@<user_name>/<repository_name>

pnpm-workspace.yaml

packages: []
pnpm:
  updateConfig:
    minimumReleaseAge: 2880
    minimumReleaseAgeExclude:
    - "@<user_name>/<repository_name>"

設定後、pnpm addを実行します。

pnpm add -D @commitlint/cli @commitlint/config-conventional @<user_name>/<repository_name>

個別リポジトリのルールを設定します。
extends に共通リポジトリのパッケージを指定し、rulesに追加のルールを入れていきます。 追加ルールはマージではなく、上書きになるので注意が必要です。

commitlint.config.js

export default {
  extends: ['@<user_name>/<repository_name>'],
  rules: {
    'scope-enum': [2, 'always', ['controller', 'repository', 'service']],
  }
};

設定が想定通りになっているかを確認しておきます。

echo 'feat(invalid): add xxx' | pnpm exec commitlint

結果

   input: feat(invalid): add xxx
   scope must be one of [controller, repository, service] [scope-enum]

   found 1 problems, 0 warnings
   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

GitHub Actions の設定 (commitlint)

共通リポジトリの設定とほぼ同じですが、envを設定する必要があります。

.github/workflows/commitlint.yaml

name: Commitlint

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  packages: read

jobs:
  commitlint:
    runs-on: ubuntu-latest

    env:
      NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint commit messages
        run: pnpm exec commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.event.pull_request.head.sha }}"

ここまでできたらPRを作成し、動作を確認します。

git switch -c add-commitlint
git add .
git commit -m "ci: add commitlint"
git push

gh pr new -f

同様に失敗するパターンも確認しておきます。

git switch -c invalid-msg
touch README.md
git add .
git commit --no-verify -m 'invalid: invalid commit msg'
git push

gh pr new -f

このリポジトリには Pre-Commit を設定していませんが、このように--no-verifyを指定すると突破できるので、Pre-Commit だけでは実はあまり効果がありません。 ただ、ブランチプロテクションを有効にしているとforce pushはできず、PR再作成の手間が発生するので、開発者体験向上のために Pre-Commit は設定しておくのが良いと思います。
また、省略しましたが、参照元リポジトリでも dependabot を設定しておくと共通ルール更新の適用が効率化できます。


以上です。