ci: automate PR title linting (#229)

* ci: automate PR title linting

This will help with Squash & Merge when you need the squashed commit to have the correct Conventional Commits format so that it can be parsed when generating release notes

(With Squash & Merge the squashed commit message is taken from the PR title)

* chore: minor updates

---------

Co-authored-by: Johann Schopplich <mail@johannschopplich.com>
This commit is contained in:
Okinea Dev
2025-12-01 20:56:56 +01:00
committed by GitHub
parent d662c21262
commit 7ed9701028
4 changed files with 100 additions and 0 deletions

36
.github/workflows/pr-title.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Check PR Title
on:
pull_request:
types: [opened, edited]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint-pr-title:
name: Lint PR title
runs-on: ubuntu-latest
if: ${{ (github.event.action == 'opened' || github.event.changes.title != null) && github.actor != 'renovate[bot]' }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
persist-credentials: false
# Only fetch the config file from the repository
sparse-checkout-cone-mode: false
sparse-checkout: commitlint.config.ts
- name: Install dependencies
run: npm install -D @commitlint/cli @commitlint/config-conventional
- name: Validate PR title with commitlint
run: echo "$PR_TITLE" | npx commitlint
env:
PR_TITLE: ${{ github.event.pull_request.title }}

38
commitlint.config.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { Rule, UserConfig } from '@commitlint/types'
import { RuleConfigSeverity } from '@commitlint/types'
// #region Rules
/**
* Rule to ensure the first letter of the commit subject is lowercase.
*
* @param parsed - Parsed commit object containing commit message parts.
* @returns A tuple where the first element is a boolean indicating
* if the rule passed, and the second is an optional error message.
*/
const subjectLowercaseFirst: Rule = async (parsed) => {
const firstChar = parsed.subject!.match(/[a-z]/i)?.[0]
if (firstChar && firstChar === firstChar.toUpperCase()) {
return [false, 'Subject must start with a lowercase letter']
}
return [true]
}
// #endregion
const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
'subject-case': [RuleConfigSeverity.Disabled],
'subject-lowercase-first': [RuleConfigSeverity.Error, 'always'],
},
plugins: [
{
rules: {
'subject-lowercase-first': subjectLowercaseFirst,
},
},
],
}
export default Configuration

View File

@@ -18,6 +18,7 @@
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^6.2.0", "@antfu/eslint-config": "^6.2.0",
"@commitlint/types": "^20.0.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"automd": "^0.4.2", "automd": "^0.4.2",
"bumpp": "^10.3.1", "bumpp": "^10.3.1",

25
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@antfu/eslint-config': '@antfu/eslint-config':
specifier: ^6.2.0 specifier: ^6.2.0
version: 6.2.0(@vue/compiler-sfc@3.5.24)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.13(@opentelemetry/api@1.9.0)(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1)) version: 6.2.0(@vue/compiler-sfc@3.5.24)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.13(@opentelemetry/api@1.9.0)(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1))
'@commitlint/types':
specifier: ^20.0.0
version: 20.0.0
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.10.1
version: 24.10.1 version: 24.10.1
@@ -365,6 +368,10 @@ packages:
'@clack/prompts@0.11.0': '@clack/prompts@0.11.0':
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
'@commitlint/types@20.0.0':
resolution: {integrity: sha512-bVUNBqG6aznYcYjTjnc3+Cat/iBgbgpflxbIBTnsHTX0YVpnmINPEkSRWymT2Q8aSH3Y7aKnEbunilkYe8TybA==}
engines: {node: '>=v18'}
'@docsearch/css@3.8.2': '@docsearch/css@3.8.2':
resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==}
@@ -1173,6 +1180,9 @@ packages:
'@types/chai@5.2.3': '@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/conventional-commits-parser@5.0.2':
resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==}
'@types/debug@4.1.12': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -1660,6 +1670,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chalk@5.6.2:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
change-case@5.4.4: change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
@@ -3837,6 +3851,11 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
sisteransi: 1.0.5 sisteransi: 1.0.5
'@commitlint/types@20.0.0':
dependencies:
'@types/conventional-commits-parser': 5.0.2
chalk: 5.6.2
'@docsearch/css@3.8.2': {} '@docsearch/css@3.8.2': {}
'@docsearch/js@3.8.2(@algolia/client-search@5.44.0)(search-insights@2.17.3)': '@docsearch/js@3.8.2(@algolia/client-search@5.44.0)(search-insights@2.17.3)':
@@ -4442,6 +4461,10 @@ snapshots:
'@types/deep-eql': 4.0.2 '@types/deep-eql': 4.0.2
assertion-error: 2.0.1 assertion-error: 2.0.1
'@types/conventional-commits-parser@5.0.2':
dependencies:
'@types/node': 24.10.1
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
@@ -5068,6 +5091,8 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chalk@5.6.2: {}
change-case@5.4.4: {} change-case@5.4.4: {}
character-entities-html4@2.1.0: {} character-entities-html4@2.1.0: {}