Skip to content

🏗️✨:validate commit messages - #1810

Merged
DerekNonGeneric merged 8 commits into
livefrom
feat/validate-commit-messages
Aug 13, 2026
Merged

🏗️✨:validate commit messages#1810
DerekNonGeneric merged 8 commits into
livefrom
feat/validate-commit-messages

Conversation

@DerekNonGeneric

@DerekNonGeneric DerekNonGeneric commented Aug 13, 2026

Copy link
Copy Markdown
Member

Phase 1 of the landing plan, and closes #1539. Subject limit 50, and the #NNNN suffix is now rejected rather than expected.

Stacked on #1809#1804, since the rules get unit tests and the runner for those arrives in #1809.

What it checks

build/shared/commit-message.mts holds the format as rules; verify.commits applies them to every commit between the branch and its base.

subject category emoji, optional action emoji, (U+FF1A), non-empty description
≤ 50 characters, counted as grapheme clusters so an emoji counts as the one character it looks like
no trailing #NNNN, no trailing full stop, no space after the colon
body blank line under the subject, ≤ 72 characters per line
a line with no spaces is exempt, because reflowing a URL breaks it
trailers one block, at the end, every line a trailer, in the order below
Co-authored-by, Signed-off-by, Assisted-by, PR-URL, Fixes, Refs, Reviewed-by
spelt exactly, case included; no --- line; nothing stranded above the final paragraph

The emoji vocabulary is the one in .github/PULL_REQUEST_TEMPLATE.md — 12 categories, 8 actions — duplicated into the module rather than parsed out of a markdown comment, with a test that fails if the two drift.

Trailer order and spelling

The order is what nodejs/node actually lands, not invented. MetadataGenerator.getMetadata() in node-core-utils appends its metadata, so whatever the branch commit already carried — Co-authored-by, Signed-off-by — ends up above PR-URL. Confirmed against real commits in that repo.

The spelling is not Node's. Checked each source directly:

source writes
git, from --signoff Signed-off-by
git, interpret-trailers documentation Reviewed-by
Linux kernel submitting-patches.rst Reviewed-by, Acked-by, Co-developed-by
GitHub documentation Co-authored-by
nodejs metadata_gen.js Reviewed-By
this repository, already landed Reviewed-by

git, GitHub and the kernel agree on a lowercase by; nodejs is the only one that capitalises it. An earlier revision of this branch followed nodejs and so put the project at odds with its own history — corrected, and the handbook now records which authority each spelling comes from.

Case is checked. git and GitHub would match a token either way, so this buys nothing functional; it buys a history that reads the same throughout. Every open branch was rewritten to suit — see below.

The emoji spelling

Some of these have two spellings that look identical, because of an invisible character that may or may not be present. The rule is the obvious one: an emoji carries it when the character would otherwise be drawn as flat monochrome text, and does not carry it when the character is already drawn in colour. Nothing else is a valid spelling, in either direction.

A test holds the whole vocabulary to that, both ways, so a future addition cannot get it wrong. I also swept every tracked file for the same two mistakes — the pull request template included, where all 20 check out by code point. The only hits anywhere are two deliberately wrong fixtures in the test file, commented as such.

What is not here is any of that explanation. Contributing should not require reading about code points, so the handbook says to copy the emoji from the table, and the check names the one to copy and links to it:

“🏗” is not the emoji for infrastructure ∥ tooling ∥ builds ∥ CI/CD;
copy “🏗️” from https://open.inf.is/docs/handbook/style/commit-messages/

Disclosing an AI assistant

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> was wrong twice over: the wrong model, and the wrong trailer. Traced it to the source.

The Linux kernel's coding-assistants.rst defines the format, and it takes no address, because it names a tool rather than a person:

Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]

The optional tools are for specialized analysis tools; everyday ones — git, a compiler, an editor — are explicitly left out. Real nodejs/node commits land exactly that: Assisted-by: codex:gpt-5.6-sol, sitting after Signed-off-by and before PR-URL, which is where it now goes in our order. core-validate-commit has a rule for it, though that rule only checks position, not content — so the content check here is ours.

The policy behind it matters more than the format. Per the kernel and the OpenJS Foundation, an assistant must not carry a Signed-off-by: only a human can certify the DCO, and disclosure is not a transfer of responsibility. The handbook says so, and Co-authored-by is documented as being for people.

So the value is now Assisted-by: Claude-Code:claude-opus-5, and the old form is rejected by name:

“Assisted-by: Claude Opus 5 <noreply@anthropic.com>” names a tool, not a person:
write it as agent:model-version, as in “Assisted-by: Claude-Code:claude-opus-5”

All 30 commits across the eight open branches were rewritten to it.

Note

One thing to decide, from doc/contributing/ai-guidelines.md: nodejs recommends against naming for-profit brands in commit messages, since a commit message is part of the codebase and can be mined for marketing. Their suggestion is to anonymize — a closed-source coding agent — or to name the brand only in the pull request description. That conflicts with the kernel's agent:model precision, and it is your call which way this repository goes. I have used the precise form, since your landed history already names the model.

git has the final say on trailers

The rules describe git's behaviour, so they are cross-checked against it: for each commit the task also runs git interpret-trailers --parse and reports a disagreement over how many trailer lines are readable. If my reimplementation of the last-paragraph rule ever drifts from the tool it describes, that shows up as a failure rather than as a wrong verdict.

One consequence worth pinning down, and now a test: a bare URL as the final paragraph is a trailer as far as git is concerned — https://example.com/p parses as a trailer called https. So the task warns about it.

It caught its own author

The first version of the second commit here used Closes: for the issue. Not in the vocabulary — Fixes: is the one — and the task said so before it was pushed.

Documented, not just enforced

collections/_docs/handbook/style/commit-messages.md was a placeholder. It now carries the format and the reasoning: the subject rules, both emoji tables, the trailer order and where each spelling comes from, and the three ways a trailer block goes unread. It keeps the Classification anchor that colons.md links to.

The pull request template points at the page, notes that the title becomes the commit subject and so takes the same rules, and says to copy the emoji rather than type them.

Every open pull request was brought into line

Ran the rules over all eight open branches, then rewrote the messages that failed. Message-only rewrites — no tree changed, and both stack relationships were re-checked afterwards.

PR was now
all Co-authored-by: Claude … <…> Assisted-by: Claude-Code:claude-opus-5
#1804 format the files that were escaping the formatters (53) …that escaped the formatters (47)
#1805 wait a week before dependabot proposes a new version (55) hold dependabot to a week before proposing (45)
#1805 raise renovate's wait to a week & apply it beyond npm (56) hold renovate to a week, and beyond npm (42)
#1805 hold pnpm to the same week before installing a version (57) hold pnpm to the same week at install time (45)
#1807 stop dependabot proposing what renovate already proposes (59) stop dependabot duplicating renovate (39)
#1808 verify only the files meant to be run are executable (55) verify only programs are executable (38)
#1274 ♻️:kebab-case the two asset names the new task flags (51) 🏗️♻️:kebab-case two asset names (29)

That last one also had ♻️ where a category belongs — an action alone says what is happening without saying where — so it gained the 🏗️ category.

The merge-order constraint I flagged earlier is gone: every open pull request passes on its own, in any order.

Verification

  • nps test — 15/15 tasks, 40/40 tests
  • 30 tests on the rules, including the exact shape every recent commit has used, which is rejected for four separate reasons
  • Every tracked file swept for emoji spelling; the 20 in the template verified by code point
  • Ran against all eight open branches: 30 commits, none failing

@DerekNonGeneric
DerekNonGeneric force-pushed the test/unit-tests-for-glob branch from 3c2b074 to e7f33b6 Compare August 13, 2026 01:20
@DerekNonGeneric
DerekNonGeneric force-pushed the feat/validate-commit-messages branch 2 times, most recently from b1642c5 to a50fe17 Compare August 13, 2026 01:58
@DerekNonGeneric
DerekNonGeneric force-pushed the test/unit-tests-for-glob branch from e7f33b6 to 9487f49 Compare August 13, 2026 02:35
@DerekNonGeneric
DerekNonGeneric force-pushed the feat/validate-commit-messages branch from f5c5de6 to 9aab21a Compare August 13, 2026 02:35
@DerekNonGeneric
DerekNonGeneric force-pushed the test/unit-tests-for-glob branch from 9487f49 to bf48347 Compare August 13, 2026 03:10
@DerekNonGeneric
DerekNonGeneric force-pushed the feat/validate-commit-messages branch 2 times, most recently from 4eda330 to 5ae08a1 Compare August 13, 2026 03:12
@DerekNonGeneric
DerekNonGeneric force-pushed the test/unit-tests-for-glob branch from bf48347 to 1837a6e Compare August 13, 2026 03:26
@DerekNonGeneric
DerekNonGeneric changed the base branch from test/unit-tests-for-glob to live August 13, 2026 03:27
Issue #1539 asked for commit message validation and named the lengths.
This is the rest of the format written down as rules: the category and
action emoji from the pull request template, the ideographic colon, and
a trailer block git will actually read.

The vocabulary is duplicated from the template rather than parsed out of
it, with a test that fails if the two drift apart.

Assisted-by: Claude-Code:claude-opus-5
Refs: #1539
Compares the branch against its base, and cross-checks each verdict
against `git interpret-trailers --parse` -- git has the final say on
what a trailer is, so the rules are held against it rather than trusted
alone.

Runs unconditionally in CI, since every pull request has commit messages
whatever files it touches, and the checkout needs full history to see
the base branch at all.

Assisted-by: Claude-Code:claude-opus-5
Fixes: #1539
Trailer tokens are now matched case-sensitively. git and GitHub would
take them either way, so this buys a history that reads the same
throughout rather than anything functional.

The emoji complaint was the wrong one. 🏗🔧 says infrastructure fix as
plainly as 🏗️🔧 does, and rejecting it as an unknown category was
misleading: it is the same character without the U+FE0F that asks for
the emoji rendering, so the message now says that. A selector on an
emoji that does not need one is reported the same way, since either
mistake leaves two spellings of one symbol.

Assisted-by: Claude-Code:claude-opus-5
The handbook page for this was a placeholder. It now documents what
`nps verify.commits` enforces and why, including the three ways a
trailer block goes unread, and keeps the Classification anchor that
colons.md links to.

The pull request template points at it, and says outright that the
emoji want copying rather than typing.

Assisted-by: Claude-Code:claude-opus-5
The rule stands: an emoji carries the invisible selector when it would
otherwise be drawn as flat text, and does not when it would not. A test
holds the whole vocabulary to that, both ways.

What is gone is the explaining. Contributing should not require reading
about code points, so the handbook says to copy the emoji from the table
and the check names the one to copy and links to it. The reasoning lives
in this repository's history, not in a contributor's way.

Assisted-by: Claude-Code:claude-opus-5
git generates Signed-off-by, GitHub documents Co-authored-by, and the
kernel established the form for all of them: the `by` is lowercase.
nodejs is the one place that writes Reviewed-By, and following it here
put this repository at odds with its own landed history, which had the
lowercase form all along.

Assisted-by: Claude-Code:claude-opus-5
`Co-authored-by` says a person wrote part of the change. An assistant is
not a person, and the trailer for it is `Assisted-by`, which the Linux
kernel defines as agent-name:model-version and nodejs/node lands in that
form -- no address, since there is nobody to address.

The check enforces that shape, so the form this repository had been
using is now rejected by name. Also documented: an assistant never gets
a `Signed-off-by`, because only a human can certify the DCO.

Assisted-by: Claude-Code:claude-opus-5
Three false positives and one false negative, all found in review before
this landed, all now covered by tests that check the rules against
`git interpret-trailers --parse` on the same input.

A last paragraph that is not all trailers is prose, and git reads no
trailers in it -- so a commit closing on “Note: only staging.” is no
longer refused. A trailer folded onto an indented line is git's own
syntax and is now read as one trailer. A carriage return no longer makes
the blank line look non-blank, which had been hiding every trailer
problem behind it.

The cross-check in the task compared git's count against a different
notion of a trailer than the rules used, so it could fail a commit that
had nothing wrong with it. Both sides now read the same block.

Assisted-by: Claude-Code:claude-opus-5
@DerekNonGeneric
DerekNonGeneric force-pushed the feat/validate-commit-messages branch from 5ae08a1 to ab70f37 Compare August 13, 2026 03:29
@netlify

netlify Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploy Preview for gh-pages-openinf ready!

Name Link
🔨 Latest commit ab70f37
🔍 Latest deploy log https://app.netlify.com/projects/gh-pages-openinf/deploys/6a7d3a0cd9b37f0009a47942
😎 Deploy Preview https://deploy-preview-1810--gh-pages-openinf.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

* @returns {string[]} The trailer lines, one per trailer, empty if there is no block.
*/
export function readTrailers(message: string) {
const [, ...rest] = message.replace(/[\r\n]+$/, '').split(/\r?\n/);
const last = paragraphsOf(rest).at(-1) ?? [];
const isBlock =
last.length > 0 &&
TRAILER_LINE.test(last[0] ?? '') &&
last.length > 0 &&
TRAILER_LINE.test(last[0] ?? '') &&
last.every(
(line) => TRAILER_LINE.test(line) || CONTINUATION_LINE.test(line)
// meant as one of ours, since it is going unread.
if (block.length === 0) {
for (const line of last) {
const token = line.match(TRAILER_LINE)?.groups?.token ?? '';
}
} else {
for (const line of block) {
const found = line.match(TRAILER_LINE)?.groups;

if (
token.toLowerCase() === 'assisted-by' &&
!ASSISTED_BY_VALUE.test(found?.value ?? '')
// about the message itself. Carriage returns say nothing either: git reads
// trailers through them, so a message written on Windows must not be judged
// differently from the same message written anywhere else.
const lines = message.replace(/[\r\n]+$/, '').split(/\r?\n/);
@DerekNonGeneric
DerekNonGeneric merged commit 7c92342 into live Aug 13, 2026
12 of 13 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.

🏷️ commit message validation

2 participants