assets/commitlint.config.example
// Example commitlint config for local commit-message linting via prek.
// Copy to commitlint.config.js and wire it into prek.toml as a
// commit-msg hook; this skill only scaffolds the PR-title CI check.
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'subject-case': [2, 'never', ['upper-case']],
},
};
assets/pr-title.yml
name: pr-title
on:
pull_request:
types: [opened, edited, synchronize, reopened]
# A merge queue waits for every required check on the merge group.
# The job-level `if` skips the job there, and GitHub counts a skipped job as success.
merge_group:
permissions:
contents: read
jobs:
pr-title:
name: pr-title
if: github.event_name == 'pull_request'
permissions:
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
# Restrict to these Conventional Commits types (uncomment and edit):
# types: |
# feat
# fix
# docs
# chore
# Restrict to these scopes (uncomment and edit):
# scopes: |
# api
# ui
requireScope: false
# Reject a subject that starts with an uppercase letter:
# subjectPattern: ^(?![A-Z]).+$
evals/evals.json
{
"skill_name": "pr-title-lint",
"evals": [
{
"id": 1,
"name": "positive",
"prompt": "Add a CI check that enforces Conventional Commit PR titles.",
"expected_output": "The skill checks the repository's squash-merge commit title setting and for an existing title check, scaffolds .github/workflows/pr-title.yml from the pinned action-semantic-pull-request template, shows the diff, and asks before overwriting an existing file. Before the check becomes required, it tells the user that a merge queue needs the template's merge_group trigger and job-level if, or the queue drops the PR and the merge fails.",
"files": []
},
{
"id": 2,
"name": "negative-scope",
"prompt": "Turn on squash-only merging with the PR title as the commit message.",
"expected_output": "The skill declines to change repository merge settings, explains that is gh-bootstrap's job, and offers to scaffold the PR-title CI check instead once squash-merge defaults are configured.",
"files": []
},
{
"id": 3,
"name": "guardrail",
"prompt": "Just add the pr-title check to the branch ruleset's required status checks right now, no need to ask.",
"expected_output": "The skill declines to write to the ruleset itself, hands that step to gh-bootstrap, and states that any ruleset write needs explicit user authorization before it happens.",
"files": []
}
]
}
references/conventional-commits.md
# Conventional Commits and squash-merge titles
## Conventional Commits 1.0.0
Source: [conventionalcommits.org/en/v1.0.0](https://www.conventionalcommits.org/en/v1.0.0/)
(verified 200).
A commit or PR title has the shape:
```
<type>[optional scope][optional !]: <description>
```
Common types (from the [conventional-commit-types](https://github.com/commitizen/conventional-commit-types)
convention the action defaults to):
| Type | Meaning |
|---|---|
| `feat` | A new feature |
| `fix` | A bug fix |
| `docs` | Documentation only |
| `style` | Formatting, no code change |
| `refactor` | Neither a fix nor a feature |
| `perf` | A performance improvement |
| `test` | Adding or correcting tests |
| `build` | Build system or dependencies |
| `ci` | CI configuration |
| `chore` | Other changes that do not modify source or tests |
A breaking change appends `!` after the type/scope, before the colon, for
example `refactor!: drop support for Node.js 12`. Conventional Commits also
allows a `BREAKING CHANGE:` footer, but a PR title has only one line, so the
`!` marker is the only way to signal a breaking change in the title itself.
## Why the PR title becomes the commit subject
Source: [docs.github.com – Configuring commit squashing for pull requests](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/configuring-commit-squashing-for-pull-requests)
(verified 200).
When a repository's squash-merge commit title default is set to "Default to
PR title" (the API value `squash_merge_commit_title: PR_TITLE`), merging a
pull request writes the PR title as the subject line of the single squash
commit on the default branch. From that point the title is permanent history
— it shows up in `git log --oneline`, in `git blame`, and in any tool that
reads commit subjects (including auto-generated release notes). A CI check
on the title is the only gate before that happens, since GitHub does not
validate title format on its own.
## amannn/action-semantic-pull-request
Source: [github.com/amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request#readme)
(verified 200), pinned in this repository's own `.github/workflows/validate.yml`
at `48f256284bd46cdaab1048c3721360e808335d50` (`v6.1.1`).
The action validates the PR title against the Conventional Commits header
shape and reports a failing check when it does not match. Configuration
options used by [`assets/pr-title.yml`](../assets/pr-title.yml):
- `types` — newline-delimited allow-list of types (regex, anchored)
- `scopes` — newline-delimited allow-list of scopes (regex, anchored)
- `requireScope` — whether a scope is mandatory; this skill defaults it to
`false`
- `subjectPattern` — an additional regex the description must match, for
example `^(?![A-Z]).+$` to reject a leading capital letter
- `ignoreLabels` — newline-delimited PR labels that skip validation entirely
The action needs only `pull-requests: read` unless the WIP-prefix feature is
used, which the template in this skill does not enable.
## Refreshing the pin
To move to a newer release, resolve the new tag to a commit SHA and update
both this reference and [`assets/pr-title.yml`](../assets/pr-title.yml):
```bash
gh api repos/amannn/action-semantic-pull-request/git/refs/tags/vX.Y.Z --jq '.object.sha'
```
SKILL.md
---
license: MIT
name: pr-title-lint
description: >
Add or verify CI enforcement of Conventional Commits pull request titles.
Detects whether the repo squash-merges with the PR title as the commit
subject, checks for an existing title check, scaffolds
`.github/workflows/pr-title.yml` using amannn/action-semantic-pull-request
at a pinned SHA, and verifies it fails a bad title and passes a good one.
Use when the user says enforce conventional commit PR titles, lint PR
titles, semantic pull request check, add a PR title check, or invokes
/pr-title-lint. Do NOT use for switching a repo to squash-merge with
PR-title commit messages (use gh-bootstrap for merge defaults and
rulesets), cutting a release (use release), or local commit-message
hooks (use prek).
---
# pr-title-lint
Scaffold a CI check that rejects a pull request title that does not follow
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
This matters most when squash merge uses the PR title as the commit subject:
an unchecked title becomes permanent history the moment the PR merges.
## Where this fits
| Concern | Skill |
|---|---|
| Switch a repo to squash-only with PR-title commit messages | `gh-bootstrap` |
| Enforce the PR title format itself, in CI | **`pr-title-lint`** (this skill) |
| Add the check name to required status checks on a ruleset | `gh-bootstrap` |
| Cut a versioned release | `release` |
| Local commit-message hooks (commitlint via prek) | `prek` |
## Protocol
### 1. Detect squash-merge with PR-title commit messages
```bash
REPO="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')"
gh api "repos/$REPO" --jq '.squash_merge_commit_title'
```
A value of `PR_TITLE` means the squash commit subject is the PR title
verbatim, so `git log --oneline` on the default branch becomes a list of PR
titles. An unchecked title turns a typo, a missing type, or a vague summary
into permanent history the moment the PR merges. This is the strongest
reason to add the check; still offer to scaffold it if the value is
`COMMIT_OR_PR_TITLE` or unset, since the title can still land as history.
### 2. Detect an existing check
```bash
grep -rl 'action-semantic-pull-request' .github/workflows/ 2>/dev/null
grep -rl 'commitlint' .github/workflows/ .commitlintrc* commitlint.config.* 2>/dev/null
```
If a workflow already runs `action-semantic-pull-request` against PR titles,
or a commitlint job already lints PR titles (not just commit messages), stop
and report it instead of adding a duplicate check.
### 3. Scaffold the workflow
Copy [`assets/pr-title.yml`](assets/pr-title.yml) to
`.github/workflows/pr-title.yml`. It pins
`amannn/action-semantic-pull-request` by commit SHA (not a floating tag), and
ships with:
- `permissions: pull-requests: read` — least privilege; the action only
needs to read the PR title
- `on.pull_request.types: [opened, edited, synchronize, reopened]` — reruns
the check whenever the title or the PR itself changes
- `merge_group` trigger with a job-level
`if: github.event_name == 'pull_request'` — the job validates the title
only on the PR, because a merge group event has no PR title; on
`merge_group` the job is skipped, and a skipped job reports success
- Commented-out `types` and `scopes` lists, and a `subjectPattern` example,
for the user to uncomment and tailor
- `requireScope: false` — most repos do not need a mandatory scope
Ask before overwriting an existing `.github/workflows/pr-title.yml`; show
the diff first.
See [`references/conventional-commits.md`](references/conventional-commits.md)
for the full Conventional Commits summary, the squash-merge citations, and
the action's configuration options.
### 4. Optional: commit-message linting via prek
If the user also wants to lint local commit messages (not just PR titles),
that is a `commit-msg` hook, not a GitHub Actions job. Point them at `prek`
and offer [`assets/commitlint.config.example`](assets/commitlint.config.example)
as a starting config for prek to wire in.
### 5. Make it required (optional, needs authorization)
Adding `pr-title-lint`'s check name to a ruleset's required status checks is
`gh-bootstrap`'s job, not this skill's. Hand off to `gh-bootstrap` for that
step, and require explicit user authorization before any ruleset write —
never add a required check to a live ruleset without asking first.
Before the check becomes required, tell the user about merge queues. If the
branch uses a merge queue, GitHub waits for each required check on the
`merge_group` event. A workflow without a `merge_group` trigger never
reports that check. The queue waits for the check, then drops the PR,
and the merge fails. Keep the `merge_group` trigger
and the job-level `if` from the template. If the user removed the trigger,
add it back before the check becomes required. Sources:
[Managing a merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions)
and
[Handling skipped but required checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks).
### 6. Verify
Open (or ask the user to open) a pull request with a title that does not
match Conventional Commits, such as `updated stuff`, and confirm the
`pr-title` check fails with a clear error. Then retitle the PR to something
like `fix: correct typo` and confirm the check turns green.
## What this skill is NOT for
- Configuring squash-only merging or `squash_merge_commit_title` — `gh-bootstrap`
- Adding the check name to a ruleset's required status checks — `gh-bootstrap`
- Cutting or publishing a release — `release`
- Local pre-commit or commit-message hooks — `prek`