Goal
Add a non-blocking CI check for PR titles to follow Conventional Commits.
Reference: https://www.conventionalcommits.org/en/v1.0.0/
Why
With squash merge, the PR title becomes the resulting commit on main, so title quality affects repository history.
Requirements
- Validate PR title only (not branch name, not local commit messages)
- Check is informational only (must not fail CI)
- Enforce:
- max header length: 120 chars
- allowed scopes:
timer, ui, audio (scope optional)
- lower-case subject after
:
- conventional commit type format
- If invalid, post/update a PR comment with actionable guidance
Implementation instructions
1. Install dependencies
npm i -D @commitlint/cli @commitlint/config-conventional
2. Add commitlint.config.cjs
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'header-max-length': [2, 'always', 120],
'scope-enum': [2, 'always', ['timer', 'ui', 'audio']],
'scope-empty': [0],
'subject-empty': [2, 'never'],
'subject-case': [2, 'always', ['lower-case']],
},
};
3. Add workflow .github/workflows/pr-title-conventional.yml
name: PR Title Conventional Check (non-blocking)
on:
pull_request:
types: [opened, edited, synchronize, reopened]
jobs:
pr-title-check:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
- name: Install dependencies
run: npm ci
- name: Validate PR title with commitlint
id: validate
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set +e
echo "$PR_TITLE" | npx commitlint > commitlint.out 2>&1
STATUS=$?
if [ $STATUS -eq 0 ]; then
echo "invalid=false" >> "$GITHUB_OUTPUT"
else
echo "invalid=true" >> "$GITHUB_OUTPUT"
{
echo "<!-- pr-title-conventional-check -->"
echo "### PR title does not follow Conventional Commits"
echo
echo "Current title: \`$PR_TITLE\`"
echo
echo "Please use format: \`type(scope): subject\` or \`type: subject\`"
echo "- Allowed scopes (optional): \`timer\`, \`ui\`, \`audio\`"
echo "- Max length: 120"
echo "- Subject should start lower-case"
echo
echo "Examples:"
echo "- \`feat(ui): add compact status line\`"
echo "- \`fix: correct timer reset on pause\`"
echo
echo "Reference: https://www.conventionalcommits.org/en/v1.0.0/"
echo
echo "Details:"
cat commitlint.out
} > comment.md
fi
exit 0
- name: Upsert PR comment when invalid
if: steps.validate.outputs.invalid == 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
EXISTING_ID=$(gh api "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" --jq '.[] | select(.body | contains("<!-- pr-title-conventional-check -->")) | .id' | head -n1)
if [ -n "$EXISTING_ID" ]; then
gh api "repos/${{ github.repository }}/issues/comments/$EXISTING_ID" --method PATCH --field body@comment.md >/dev/null
else
gh api "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" --method POST --field body@comment.md >/dev/null
fi
Acceptance criteria
Notes
scope-enum means if scope is present, it must be from allowlist.
- This is intentionally non-blocking to coach contributors without preventing merges.
Goal
Add a non-blocking CI check for PR titles to follow Conventional Commits.
Reference: https://www.conventionalcommits.org/en/v1.0.0/
Why
With squash merge, the PR title becomes the resulting commit on
main, so title quality affects repository history.Requirements
timer,ui,audio(scope optional):Implementation instructions
1. Install dependencies
2. Add
commitlint.config.cjs3. Add workflow
.github/workflows/pr-title-conventional.ymlAcceptance criteria
timer,ui,audio), length <= 120, lower-case subjectNotes
scope-enummeans if scope is present, it must be from allowlist.