ci: validate PR titles and sharpen review guidance

This commit is contained in:
Danila Poyarkov 2026-09-15 21:36:09 +03:00
parent 4b5f0da3bc
commit e40df128e9
6 changed files with 82 additions and 4 deletions

View file

@ -7,6 +7,10 @@ reviews:
request_changes_workflow: false
high_level_summary: true
review_status: true
sequence_diagrams: false
finishing_touches:
docstrings:
enabled: false
auto_review:
enabled: true
drafts: false
@ -15,7 +19,7 @@ reviews:
mode: "off"
title:
mode: "error"
requirements: "Apply CONTRIBUTING.md → Pull requests → PR title. Request a clearer title when it is non-English, vague, placeholder-like, or does not identify the actual change."
requirements: "Apply CONTRIBUTING.md → Pull requests → PR title. Assess whether the title clearly and accurately identifies the actual change. CI owns Conventional Commit syntax validation; do not duplicate that check. Request clarification for non-English, vague, placeholder-like, or misleading titles."
description:
mode: "warning"
custom_checks:
@ -29,5 +33,10 @@ reviews:
mode: "warning"
instructions: "Use CONTRIBUTING.md → Pull requests as the source of truth. Suggest adding context when the PR body does not explain what changed, why it changed, or how it was validated. Do not call a focused PR low-effort solely because it needs template cleanup."
knowledge_base:
code_guidelines:
filePatterns:
- "CONTRIBUTING.md"
chat:
auto_reply: true

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

@ -0,0 +1,33 @@
name: PR title
on:
pull_request:
branches: [master]
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
concurrency:
group: pr-title-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
pr-title:
name: PR title
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: ./.github/actions/setup-bun
- name: Validate PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
COMMITLINT_PR_TITLE: '1'
run: |
if ! printf '%s\n' "$PR_TITLE" | bun run check:commits; then
echo '::error title=PR title::Use a Conventional Commit title, for example fix: preserve selection. See CONTRIBUTING.md#commit-messages.'
exit 1
fi

View file

@ -105,6 +105,8 @@ For user-facing work, add one present-tense outcome under the single appropriate
Use Conventional Commits (`feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, `chore`) for regular work. Keep subjects short, imperative, and narrowly scoped; explain rationale in the body. Preserve product casing such as DOM/CSS, HTML, JSX, Tailwind, Kiwi, `.fig`, MCP, CLI, AI, ACP, and i18n. Release commits use `Release vX.Y.Z`.
PR titles use Conventional Commits because GitHub uses them as merge subjects. The separate **PR title** workflow validates titles, including title edits, without rerunning the full CI suite. Preserve the conventional subject when merging via CLI/API; if setting it explicitly with `gh pr merge --subject`, use the validated PR title. Give branch-update merges explicit subjects such as `chore: merge master into <branch>`. Commitlint's default merge exceptions are not a naming convention. Do not rewrite published history solely to normalize messages.
## CLI
- Format all output with the `agentfmt` helpers re-exported from `packages/cli/src/format.ts`; do not hand-roll terminal formatting.
@ -162,6 +164,15 @@ Private tooling belongs under `tools/<domain>/{src,tests}`, with kebab-case doma
- Keep Kiwi runtime changes minimal; prefer wrappers for project policy.
- Guard browser globals explicitly in Core. Name repeated/cross-feature constants; app-wide values belong in `src/constants.ts`.
## Code review
- Review codebase fit, not just the diff. Before judging or implementing a change, inspect the owning folder, nearby analogous implementations, shared helpers/types, public exports, callers, and tests. Check new files against the established file tree, package boundaries, naming, and local conventions. Prefer an existing abstraction when it fits; do not invent a parallel pattern or demand unrelated cleanup.
- Verify findings against the current PR head and pinned dependency APIs. Give the concrete failing scenario and consequence; distinguish demonstrated bugs from defensive hardening and preferences. If runtime validation or dependency source is unavailable, state that limitation rather than treating an assumption as a fact.
- On re-review, check later commits and the discussion before repeating a finding. Mark addressed, obsolete, or intentionally declined suggestions accurately. Green checks and resolved threads are not substitutes for reviewing the current code.
- Request evidence appropriate to the change: engine tests for state contracts, Storybook for isolated component states, browser integration tests for workflows, canvas snapshots for rendering, and native tests for platform delivery. Do not claim one proves another.
- Preserve intentional behavior unless a concrete regression is demonstrated. For example, preferences and native credentials cannot transact together; documented partial-save outcomes and retryable drafts are not inherently bugs.
- Keep review comments concise and actionable. Cite the relevant location and repository rule or existing analogue for codebase-fit findings. Independently assess automated suggestions; do not bulk-apply or bulk-resolve them merely to make a bot green.
## Code quality
Before submitting a PR, run the complete gate and relevant tests:

View file

@ -26,7 +26,7 @@ Pull requests must be reviewable without guessing the author's intent.
- Write the title in English.
- Be specific about the actual change; avoid vague titles such as `fix`, `update`, `some fixes`, `changes`, or `WIP`.
- Use Conventional Commits when it fits the change, for example `fix: handle empty exports` or `docs: clarify CLI setup`.
- Use Conventional Commits, for example `fix: handle empty exports` or `docs: clarify CLI setup`. The exact `Release vX.Y.Z` release-title exception is preserved. See [Commit messages](#commit-messages) for validation commands.
### PR body
@ -117,11 +117,14 @@ The **Commit messages** CI job checks every commit introduced by a PR, including
Use `type(optional-scope): short description`, for example `fix(MCP): preserve connection settings`. Allowed types are `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, and `chore`. Keep headers within 100 characters and omit a trailing period. Product names retain their casing; bodies and footers may contain long lines.
Standard merge/revert messages use commitlint's default exceptions. Release commits retain the exact `Release vX.Y.Z` subject convention. These checks validate structure, not whether a description is meaningful or the type is appropriate.
PR titles follow the same convention because GitHub uses them as merge subjects. The separate **PR title** workflow checks new and updated PRs, including title edits, without rerunning the full CI suite. Title validation disables commitlint's default merge/revert exceptions. Release titles and commits retain the exact `Release vX.Y.Z` convention.
Commit-range validation retains commitlint's default merge/revert exceptions, but they are not a naming convention. Preserve the validated PR title when merging via CLI/API, and use explicit conventional subjects for branch updates, for example `chore: merge master into my-branch`. Do not rewrite published history solely to normalize messages. These checks validate structure, not whether a description is meaningful or the type is appropriate.
```sh
bun run check:commits --last
bun run check:commits --from origin/master --to HEAD --verbose
printf '%s\n' 'fix(MCP): preserve connection settings' | COMMITLINT_PR_TITLE=1 bun run check:commits
```
If a message fails, use the reported rule and commit subject to locate it. Amend your latest commit with `git commit --amend`, or use an interactive rebase for earlier commits on your PR branch. Coordinate before rewriting a shared branch. No local Git hooks are installed automatically; CI is the enforcement point.

View file

@ -2,6 +2,8 @@ import type { UserConfig } from '@commitlint/types'
export default {
extends: ['@commitlint/config-conventional'],
// PR titles must not bypass validation through Git's generated-message exceptions.
defaultIgnores: process.env.COMMITLINT_PR_TITLE !== '1',
rules: {
'type-enum': [
2,

View file

@ -5,9 +5,10 @@ import { join, resolve } from 'node:path'
const root = resolve(import.meta.dir, '../../..')
async function lint(message: string, args: string[] = []) {
async function lint(message: string, args: string[] = [], prTitle = false) {
const child = Bun.spawn([process.execPath, 'run', 'check:commits', '--verbose', ...args], {
cwd: root,
env: { ...process.env, COMMITLINT_PR_TITLE: prTitle ? '1' : '0' },
stdin: new Blob([message]),
stdout: 'pipe',
stderr: 'pipe'
@ -47,6 +48,25 @@ test.each([
expect(result.output).toContain('CONTRIBUTING.md#commit-messages')
})
test.each([
'fix(MCP): preserve connection settings',
'feat!: change the tool contract',
'Release v0.14.0'
])('accepts supported PR title: %s', async (title) => {
expect((await lint(title, [], true)).code).toBe(0)
})
test.each([
'Merge pull request #700 from open-pencil/build/commitlint',
"Merge remote-tracking branch 'origin/master' into build/commitlint",
'Revert "fix: preserve selection"',
'Fixed stuff'
])('rejects non-conventional PR title: %s', async (title) => {
const result = await lint(title, [], true)
expect(result.code).not.toBe(0)
expect(result.output).toContain('type-empty')
})
test('checks the full PR range while excluding existing base history', async () => {
const directory = await mkdtemp(join(tmpdir(), 'open-pencil-commitlint-'))
async function git(...args: string[]) {