Skip to content

ci: refuse pull request titles without a Conventional Commits type - #916

Merged
azimafroozeh merged 2 commits into
ModernRelay:mainfrom
azimafroozeh:pr-title-lint
Oct 9, 2026
Merged

azimafroozeh merged 2 commits into
ModernRelay:mainfrom
azimafroozeh:pr-title-lint

Conversation

@azimafroozeh

Copy link
Copy Markdown
Collaborator

What & why

This PR adds PR Title, a required check that holds every pull request title to type(scope)!: description. Every merge into main is a squash whose subject is the title, so the title is the one line git log and the release tooling keep; 22 of the 91 PRs between v0.11.0 and v0.12.0 carry no type, among them #861, #849, #829, #713 and #827, so a release summary built from subjects would drop exactly those.

  • The rule: type is one of feat fix perf refactor docs test ci build revert rfc release (no chore); (scope) is an optional lowercase comma list; ! marks a breaking change; the description starts lowercase (a backtick or digit also passes), has no trailing period, and the title stays within 100 characters with no whitespace but the plain space.
  • One spelling per history: docs(rfc): is refused in favour of rfc:, and GitHub's Revert "…" default in favour of revert: <what is undone>; the failure message names the accepted form.
  • scripts/check-pr-title.py is the check: stdlib only, runs locally with --title, carries its fixtures in --self-test, and on failure prints one line with the reason, the shape and two examples, plus a GitHub error annotation.
  • .github/workflows/pr-title.yml runs it on pull_request_target (opened, synchronize, reopened, edited) with contents: read, the same shape as Fix Regression Gate: the checker comes from main and the PR head is never fetched, so a PR cannot edit the rule it is checked by. A title edit re-runs the check.
  • On merge_group the same checker runs on merge_group.head_commit.message (first line, minus the (#N) suffix), the squash subject the queue is about to land.
  • PR Title joins .github/branch-protection.json and the two mirrored lists; the rule lives in CONTRIBUTING.md, Pull Requests, with a pointer from the PR template and AGENTS.md.
  • An in-repo script over commitlint or a third-party title action: no unpinned npm packages or external code with the job token, the rule is readable and runnable in the repo, and --self-test is the convention the other metadata gates follow.

Backing issue / RFC

  • none; motivated by untyped squash subjects on main making a title-based release summary drop user-facing changes

Checklist

  • Change is focused (one check: script, workflow, required context, and the docs that state the rule)
  • Tests added/updated for behavior changes (scripts/check-pr-title.py --self-test: 18 accepted, 54 refused, 6 squash subjects; the refused fixtures include 25 subjects that reached main before the check, and the workflow runs the self-test before every title check)
  • Public docs updated if user-facing surface changed (CONTRIBUTING.md, docs/dev/ci.md, docs/dev/branch-protection.md, .github/PULL_REQUEST_TEMPLATE.md, AGENTS.md; no user-facing surface)
  • Release note: N/A, contributor process only, no user-visible change
  • Reviewed against docs/dev/invariants.md: no Hard Invariant weakened, no deny-list item hit (no engine code; nearest invariant touched: none)

Local verification

  • python3 scripts/check-pr-title.py --self-test: ok (18 accepted, 54 refused, 6 squash subjects); not visible on this PR's checks because pull_request_target takes the workflow from main, where it does not exist yet
  • python3 scripts/check-pr-title.py --title "ci: refuse pull request titles without a Conventional Commits type": accepted (this PR's own title)
  • python3 scripts/check-pr-title.py --squash-subject "Update Readme (#881)": refused, exit 1, one failure line with the shape and two examples
  • PyYAML parse of .github/workflows/pr-title.yml: on is merge_group plus pull_request_target with opened, synchronize, reopened, edited; job pr_title is named PR Title
  • actionlint, shellcheck: not run: not installed here
  • Hosted merge-queue run: not run: that the landed commit's subject is the captured head_commit.message is inferred from the payload; one queue run after rollout confirms it

Notes for reviewers

  • This check does not run on this PR: pull_request_target executes main's copy of the workflow. Its first run is the first PR event after merge. After merge, apply the rule (scripts/apply-branch-protection.sh) so PR Title binds, and retitle the open PRs it refuses before they can queue.
  • The check assumes the repository's squash message setting is "Default to pull request title"; every squash-era subject on main reads <title> (#N), which is consistent with it but does not prove it. Under GitHub's "Default message", a one-commit PR's squash would use the commit subject instead. Verify on the rule page before applying.
  • Honest routes: every squash-era title shape has an accepted form, rfc: <subject> for RFCs, revert: <what is undone> for reverts, release: … or docs(release): … where chore(release): prepare notes for v0.13.1 #889 used chore(release). chore is deliberately absent: a type should say what kind of change landed, and 0 of 205 squash-era PRs before chore(release): prepare notes for v0.13.1 #889 used it.
  • What the check does not stop: a well-shaped wrong type (fix: on a feature) or an empty description (fix: stuff) passes; the check is shape only, review still decides whether the type describes the change. A breaking change without ! passes; the cross-check against changelog.d/*.breaking.md is the follow-up PR.
  • A title edited after the PR enters the queue: the merge-group run lints the captured head_commit.message, which the edit does not change, so the subject that lands was linted; the edit itself is not detected on the queue. The edited event re-runs the PR-side check, and whether a red result there removes the queued entry is undocumented by GitHub, the same gap docs/dev/branch-protection.md records for Fix Regression Gate.
  • The merge-group run executes the queued commit's copy of the checker, like every merge-group run; the policy copy is the pull_request_target run on the PR.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T13:57:10.219017Z cffe3e8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cffe3e8a7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/check-pr-title.py
Comment thread scripts/check-pr-title.py

@ragnorc ragnorc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendation: approve the code. I found no blocking defect in the reviewed head cffe3e8a7a284a0b728cdcbefff38d6356408cb2. Deployment still needs the rollout steps in the PR body.

This PR makes change history easier to classify. It requires a typed PR title, such as fix(server): reject an invalid token. A small Python checker validates the title. A base-branch workflow runs it after PR events, including title edits. The merge-queue path checks the captured commit subject. The branch-protection configuration adds the required context. Contributor documentation explains the same rule.

The general problem is admission of untyped history, rather than one badly named PR. The change addresses it at the shared admission boundary. It does not add exceptions for individual authors or past PR numbers. The validator owns the grammar. The workflow supplies event data through quoted environment variables. It does not interpolate the title into shell code. The PR path checks out the base version and never fetches the PR head. I checked the default ref/SHA selection in the exact pinned checkout implementation, not only its current README.

Tradeoffs and liability: a stdlib checker avoids an additional package manager, service, or title action. Its fixtures make the local rule easy to inspect. The cost is another required job and a repository-specific grammar to maintain. The documentation repeats parts of that grammar, so future rule changes must update those descriptions together. Five similar policy changes should retain small local owners and explicit event boundaries, rather than introduce a general policy framework. This addition reduces history-classification liability, while adding modest contributor and CI maintenance. Shape checks cannot prove that a selected type describes the change. They also do not establish breaking-change compatibility.

The operating boundary needs care. The live repository API currently reports squash_merge_commit_title=COMMIT_OR_PR_TITLE, not PR_TITLE. The PR body already identifies this rollout obligation. Set the intended title setting before enabling the required context if the contract is specifically that PR titles become squash subjects. The queue check still validates the actual captured subject rather than trusting that setting. A recent merge-group run exposed the typed subject of #914, and that SHA is now main. This is one observation, not proof for all queue configurations or title-edit races. I made no repository-setting changes.

Validation on the exact head:

  • python3 scripts/check-pr-title.py --self-test: passed, with 18 accepted titles, 54 refused titles and six squash subjects.
  • The PR's actual title passed --title. The captured #914 subject passed --squash-subject. Update Readme (#881) failed with exit 1 for the expected missing type prefix.
  • python3 scripts/check-merge-group-triggers.py --self-test: passed and found all 13 configured required contexts reporting on the queue.
  • Workflow action-pin checks passed for 77 external uses. CI-cell checks passed for 48 required test names. AGENTS link checks and git diff --check HEAD^ HEAD passed.
  • scripts/check-docs.py passed for 228 Markdown files using a temporary virtual environment with the pinned documentation requirements.

This is a new contributor-policy gate, not a repair to query behavior. GQT cannot express GitHub metadata events, token permissions or branch protection. The checker fixtures are the appropriate cheap test owner. Parent af83de9d2456c3f91469132ea3b296a2e9dec4fe has no checker. I do not count that missing program as a behavioral before/after regression proof. No Rust or Lance behavior changes, and no Lance implementation assumption is needed for this scope.

I checked the GitHub event contract, the merge-group payload schema and example, Conventional Commits, and the pinned checkout ref-selection source. Local tests do not execute a hosted title-edit event or qualify the new required context. The new workflow is absent from the base branch, so its lack of a check on this PR is expected. I do not claim full exact-head CI success or a hosted run of this new workflow. No optional code requests remain. The isolated checkout is clean. I rechecked open state, head, reviews and inline comments before posting.

@azimafroozeh
azimafroozeh enabled auto-merge October 9, 2026 14:18
@azimafroozeh
azimafroozeh disabled auto-merge October 9, 2026 14:27
@azimafroozeh
azimafroozeh added this pull request to the merge queue Oct 9, 2026
Merged via the queue into ModernRelay:main with commit 8d80442 Oct 9, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants