From 3fce4e6f6cc4ebb2970406324b28fa6a03369489 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 3 Apr 2026 11:09:11 -0700 Subject: [PATCH 1/6] Add Signed-off-by and Assisted-By rules (#141) --- lib/rules/assisted-by-is-trailer.js | 49 ++ lib/rules/signed-off-by.js | 217 +++++++ lib/validator.js | 4 + test/cli-test.js | 8 +- test/fixtures/commit.json | 2 +- test/fixtures/pr.json | 2 +- test/rules/assisted-by-is-trailer.js | 190 +++++++ test/rules/signed-off-by.js | 819 +++++++++++++++++++++++++++ test/validator.js | 20 +- 9 files changed, 1302 insertions(+), 9 deletions(-) create mode 100644 lib/rules/assisted-by-is-trailer.js create mode 100644 lib/rules/signed-off-by.js create mode 100644 test/rules/assisted-by-is-trailer.js create mode 100644 test/rules/signed-off-by.js diff --git a/lib/rules/assisted-by-is-trailer.js b/lib/rules/assisted-by-is-trailer.js new file mode 100644 index 0000000..1864304 --- /dev/null +++ b/lib/rules/assisted-by-is-trailer.js @@ -0,0 +1,49 @@ +const id = 'assisted-by-is-trailer' + +export default { + id, + meta: { + description: 'enforce that "Assisted-by:" lines are trailers', + recommended: true + }, + defaults: {}, + options: {}, + validate: (context, rule) => { + const parsed = context.toJSON() + const lines = parsed.body.map((line, i) => [line, i]) + const re = /^Assisted-by:/gi + const assisted = lines.filter(([line]) => re.test(line)) + if (assisted.length !== 0) { + const firstAssisted = assisted[0] + const emptyLines = lines.filter(([text]) => text.trim().length === 0) + // There must be at least one empty line, and the last empty line must be + // above the first Assisted-by line. + const isTrailer = (emptyLines.length !== 0) && + emptyLines.at(-1)[1] < firstAssisted[1] + if (isTrailer) { + context.report({ + id, + message: 'Assisted-by is a trailer', + string: '', + level: 'pass' + }) + } else { + context.report({ + id, + message: 'Assisted-by must be a trailer', + string: firstAssisted[0], + line: firstAssisted[1], + column: 0, + level: 'fail' + }) + } + } else { + context.report({ + id, + message: 'no Assisted-by metadata', + string: '', + level: 'pass' + }) + } + } +} diff --git a/lib/rules/signed-off-by.js b/lib/rules/signed-off-by.js new file mode 100644 index 0000000..b92d91d --- /dev/null +++ b/lib/rules/signed-off-by.js @@ -0,0 +1,217 @@ +const id = 'signed-off-by' + +// Matches the name and email from a Signed-off-by line +const signoffParts = /^Signed-off-by: (.*) <([^>]+)>/i + +// Bot/AI patterns: name ending in [bot], or GitHub bot noreply emails +// This is an imperfect heuristic, but there's no standard way to identify +// bot commits in a reliable, generic way. +const botNamePattern = /\[bot\]$/i +const botEmailPattern = /(?:\[bot\]@users\.noreply\.github\.com|github-bot@iojs\.org)$/i + +// Matches "Name " author strings +const authorPattern = /^(.*?)\s*<([^>]+)>/ +const signoffPattern = /^Signed-off-by: /i +const validSignoff = /^Signed-off-by: .+ <[^@]+@[^@]+\.[^@]+>/i +const backportPattern = /^Backport-PR-URL:/i + +// Parse name and email from an author string like "Name " +function parseAuthor (authorStr) { + if (!authorStr) return null + const match = authorStr.match(authorPattern) + if (!match) return null // Purely defensive check here + return { name: match[1].trim(), email: match[2].toLowerCase() } +} + +// Check if an author string looks like a bot +function isBotAuthor (authorStr) { + const author = parseAuthor(authorStr) + if (!author) return false + return botNamePattern.test(author.name) || botEmailPattern.test(author.email) +} + +export default { + id, + meta: { + description: 'enforce DCO sign-off', + recommended: true + }, + defaults: {}, + options: {}, + validate: (context, rule) => { + const parsed = context.toJSON() + + // Release commits generally won't have sign-offs + if (parsed.release) { + context.report({ + id, + message: 'skipping sign-off for release commit', + string: '', + level: 'skip' + }) + return + } + + // Deps commits are cherry-picks/backports/updates from upstream projects + // (V8, etc.) and are not expected to have a Signed-off-by. When deps + // is mixed with other subsystems, a sign-off might be required but we'll + // downgrade to a warn instead of fail in this case. + const hasDeps = parsed.subsystems.includes('deps') + if (hasDeps && parsed.subsystems.every((s) => s === 'deps')) { + context.report({ + id, + message: 'skipping sign-off for deps commit', + string: '', + level: 'skip' + }) + return + } + + // Backport commits (identified by a Backport-PR-URL trailer) are + // cherry-picks of existing commits into release branches. The + // original commit was already validated. + if (parsed.body.some((line) => backportPattern.test(line))) { + context.report({ + id, + message: 'skipping sign-off for backport commit', + string: '', + level: 'skip' + }) + return + } + + const signoffs = parsed.body + .map((line, i) => [line, i]) + .filter(([line]) => signoffPattern.test(line)) + + // Bot-authored commits don't need a sign-off. + // If they have one, warn; otherwise pass. + if (isBotAuthor(parsed.author)) { + if (signoffs.length === 0) { + context.report({ + id, + message: 'skipping sign-off for bot commit', + string: '', + level: 'pass' + }) + } else { + for (const [line, lineNum] of signoffs) { + context.report({ + id, + message: 'bot commit should not have a "Signed-off-by" trailer', + string: line, + line: lineNum, + column: 0, + level: 'warn' + }) + } + } + return + } + + // Assume it's not a bot commit... a Signed-off-by trailer is required. + // For mixed deps commits (deps + other subsystems), downgrade to warn + // since the deps portion may legitimately lack a sign-off. + if (signoffs.length === 0) { + context.report({ + id, + message: hasDeps + ? 'Commit with non-deps changes should have a "Signed-off-by" trailer' + : 'Commit must have a "Signed-off-by" trailer', + string: '', + level: hasDeps ? 'warn' : 'fail' + }) + return + } + + // Flag every sign-off that has an invalid email format. + // Collect valid sign-offs for further checks. + const valid = [] + for (const [line, lineNum] of signoffs) { + if (validSignoff.test(line)) { + valid.push([line, lineNum]) + } else { + context.report({ + id, + message: '"Signed-off-by" trailer has invalid email', + string: line, + line: lineNum, + column: 0, + level: 'fail' + }) + } + } + + if (valid.length === 0) { + // All sign-offs had invalid emails; already reported above. + return + } + + // Flag any sign-off that appears to be from a bot or AI agent. + // Bots and AI agents are not permitted to sign off on commits. + // Collect non-bot sign-offs for further checks. If the commit + // itself appears to be from a bot, the case is handled above. + const human = [] + for (const [line, lineNum] of valid) { + const { 1: name, 2: email } = line.match(signoffParts) + if (botNamePattern.test(name) || botEmailPattern.test(email)) { + context.report({ + id, + message: '"Signed-off-by" must be from a human author, ' + + 'not a bot or AI agent', + string: line, + line: lineNum, + column: 0, + level: 'warn' + }) + } else { + human.push([line, lineNum]) + } + } + + // All sign-offs appear to be from bots; already reported above. + // If there are no human sign-offs, fail (or warn for mixed deps). + if (human.length === 0) { + context.report({ + id, + message: hasDeps + ? 'Commit with non-deps changes should have a "Signed-off-by" ' + + 'trailer from a human author' + : 'Commit must have a "Signed-off-by" trailer from a human author', + string: '', + level: hasDeps ? 'warn' : 'fail' + }) + return + } + + // When author info is available, warn if none of the human sign-off + // emails match the commit author email. This may indicate an automated + // tool signed off on behalf of the author. + const authorEmail = parseAuthor(parsed.author)?.email + if (authorEmail) { + const authorMatch = human.some(([line]) => { + const { 2: email } = line.match(signoffParts) + return email.toLowerCase() === authorEmail + }) + if (!authorMatch) { + context.report({ + id, + message: '"Signed-off-by" email does not match the ' + + 'commit author email', + string: human[0][0], + line: human[0][1], + column: 0, + level: 'warn' + }) + return + } + } + + context.report({ + id, + message: 'has valid Signed-off-by', + string: '', + level: 'pass' + }) + } +} diff --git a/lib/validator.js b/lib/validator.js index bbd26bf..dfa2a94 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -3,6 +3,7 @@ import Parser from 'gitlint-parser-node' import BaseRule from './rule.js' // Rules +import assistedByIsTrailer from './rules/assisted-by-is-trailer.js' import coAuthoredByIsTrailer from './rules/co-authored-by-is-trailer.js' import fixesUrl from './rules/fixes-url.js' import lineAfterTitle from './rules/line-after-title.js' @@ -11,10 +12,12 @@ import metadataEnd from './rules/metadata-end.js' import prUrl from './rules/pr-url.js' import reviewers from './rules/reviewers.js' import subsystem from './rules/subsystem.js' +import signedOffBy from './rules/signed-off-by.js' import titleFormat from './rules/title-format.js' import titleLength from './rules/title-length.js' const RULES = { + 'assisted-by-is-trailer': assistedByIsTrailer, 'co-authored-by-is-trailer': coAuthoredByIsTrailer, 'fixes-url': fixesUrl, 'line-after-title': lineAfterTitle, @@ -22,6 +25,7 @@ const RULES = { 'metadata-end': metadataEnd, 'pr-url': prUrl, reviewers, + 'signed-off-by': signedOffBy, subsystem, 'title-format': titleFormat, 'title-length': titleLength diff --git a/test/cli-test.js b/test/cli-test.js index 9fc19fd..bcaea29 100644 --- a/test/cli-test.js +++ b/test/cli-test.js @@ -155,7 +155,7 @@ test('Test cli flags', (t) => { t.test('test stdin with valid JSON', (tt) => { const validCommit = { id: '2b98d02b52', - message: 'stream: make null an invalid chunk to write in object mode\n\nthis harmonizes behavior between readable, writable, and transform\nstreams so that they all handle nulls in object mode the same way by\nconsidering them invalid chunks.\n\nPR-URL: https://github.com/nodejs/node/pull/6170\nReviewed-By: James M Snell \nReviewed-By: Matteo Collina ' + message: 'stream: make null an invalid chunk to write in object mode\n\nthis harmonizes behavior between readable, writable, and transform\nstreams so that they all handle nulls in object mode the same way by\nconsidering them invalid chunks.\n\nSigned-off-by: Calvin Metcalf \nPR-URL: https://github.com/nodejs/node/pull/6170\nReviewed-By: James M Snell \nReviewed-By: Matteo Collina ' } const input = JSON.stringify([validCommit]) @@ -211,11 +211,11 @@ test('Test cli flags', (t) => { const commits = [ { id: 'commit1', - message: 'doc: update README\n\nPR-URL: https://github.com/nodejs/node/pull/1111\nReviewed-By: Someone ' + message: 'doc: update README\n\nSigned-off-by: Someone \nPR-URL: https://github.com/nodejs/node/pull/1111\nReviewed-By: Someone ' }, { id: 'commit2', - message: 'test: add new test case\n\nPR-URL: https://github.com/nodejs/node/pull/2222\nReviewed-By: Someone ' + message: 'test: add new test case\n\nSigned-off-by: Someone \nPR-URL: https://github.com/nodejs/node/pull/2222\nReviewed-By: Someone ' } ] const input = JSON.stringify(commits) @@ -337,7 +337,7 @@ test('Test cli flags', (t) => { t.test('test stdin with --no-validate-metadata', (tt) => { const commit = { id: 'novalidate', - message: 'doc: update README\n\nThis commit has no PR-URL or reviewers' + message: 'doc: update README\n\nThis commit has no PR-URL or reviewers\n\nSigned-off-by: Someone ' } const input = JSON.stringify([commit]) diff --git a/test/fixtures/commit.json b/test/fixtures/commit.json index 08bec52..a45f90a 100644 --- a/test/fixtures/commit.json +++ b/test/fixtures/commit.json @@ -16,7 +16,7 @@ "sha": "d3f20ccfaa7b0919a7c5a472e344b7de8829b30c", "url": "https://api.github.com/repos/nodejs/node/git/trees/d3f20ccfaa7b0919a7c5a472e344b7de8829b30c" }, - "message": "stream: make null an invalid chunk to write in object mode\n\nthis harmonizes behavior between readable, writable, and transform\nstreams so that they all handle nulls in object mode the same way by\nconsidering them invalid chunks.\n\nPR-URL: https://github.com/nodejs/node/pull/6170\nReviewed-By: James M Snell \nReviewed-By: Matteo Collina ", + "message": "stream: make null an invalid chunk to write in object mode\n\nthis harmonizes behavior between readable, writable, and transform\nstreams so that they all handle nulls in object mode the same way by\nconsidering them invalid chunks.\n\nSigned-off-by: Calvin Metcalf \nPR-URL: https://github.com/nodejs/node/pull/6170\nReviewed-By: James M Snell \nReviewed-By: Matteo Collina ", "parents": [ { "sha": "ec2822adaad76b126b5cccdeaa1addf2376c9aa6", diff --git a/test/fixtures/pr.json b/test/fixtures/pr.json index 3ad5078..5af96c6 100644 --- a/test/fixtures/pr.json +++ b/test/fixtures/pr.json @@ -12,7 +12,7 @@ "email": "cmetcalf@appgeo.com", "date": "2016-04-13T16:33:55Z" }, - "message": "stream: make null an invalid chunk to write in object mode\n\nthis harmonizes behavior between readable, writable, and transform\nstreams so that they all handle nulls in object mode the same way by\nconsidering them invalid chunks.\n\nPR-URL: https://github.com/nodejs/node/pull/6170\nReviewed-By: James M Snell \nReviewed-By: Matteo Collina ", + "message": "stream: make null an invalid chunk to write in object mode\n\nthis harmonizes behavior between readable, writable, and transform\nstreams so that they all handle nulls in object mode the same way by\nconsidering them invalid chunks.\n\nSigned-off-by: Calvin Metcalf \nPR-URL: https://github.com/nodejs/node/pull/6170\nReviewed-By: James M Snell \nReviewed-By: Matteo Collina ", "tree": { "sha": "e4f9381fdd77d1fd38fe27a80dc43486ac732d48", "url": "https://api.github.com/repos/nodejs/node/git/trees/e4f9381fdd77d1fd38fe27a80dc43486ac732d48" diff --git a/test/rules/assisted-by-is-trailer.js b/test/rules/assisted-by-is-trailer.js new file mode 100644 index 0000000..01d2a2d --- /dev/null +++ b/test/rules/assisted-by-is-trailer.js @@ -0,0 +1,190 @@ +import { test } from 'tap' +import Rule from '../../lib/rules/assisted-by-is-trailer.js' +import Commit from 'gitlint-parser-node' +import Validator from '../../index.js' + +test('rule: assisted-by-is-trailer', (t) => { + t.test('no assisted-by', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'fhqwhgads' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'assisted-by-is-trailer', 'id') + tt.equal(opts.message, 'no Assisted-by metadata', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('no empty lines above', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + 'Assisted-by: Whatever' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'assisted-by-is-trailer', 'id') + tt.equal(opts.message, + 'Assisted-by must be a trailer', 'message') + tt.equal(opts.string, + 'Assisted-by: Whatever', 'string') + tt.equal(opts.line, 0, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('not trailer - in body before metadata', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Assisted-by: Whatever\n' + + '\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'assisted-by-is-trailer', 'id') + tt.equal(opts.message, + 'Assisted-by must be a trailer', 'message') + tt.equal(opts.string, + 'Assisted-by: Whatever', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('is trailer', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Assisted-by: Whatever\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'assisted-by-is-trailer', 'id') + tt.equal(opts.message, + 'Assisted-by is a trailer', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('multiple assisted-by as trailers', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Assisted-by: Whatever\n' + + 'Assisted-by: Something else\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'assisted-by-is-trailer', 'id') + tt.equal(opts.message, + 'Assisted-by is a trailer', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('not all are trailers', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Assisted-by: Whatever\n' + + '\n' + + 'Assisted-by: Something else\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'assisted-by-is-trailer', 'id') + tt.equal(opts.message, + 'Assisted-by must be a trailer', 'message') + tt.equal(opts.string, + 'Assisted-by: Whatever', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.end() +}) diff --git a/test/rules/signed-off-by.js b/test/rules/signed-off-by.js new file mode 100644 index 0000000..acff632 --- /dev/null +++ b/test/rules/signed-off-by.js @@ -0,0 +1,819 @@ +import { test } from 'tap' +import Rule from '../../lib/rules/signed-off-by.js' +import Commit from 'gitlint-parser-node' +import Validator from '../../index.js' + +test('rule: signed-off-by', (t) => { + t.test('valid sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Foo \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, 'has valid Signed-off-by', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('missing sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'Commit must have a "Signed-off-by" trailer', 'message') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('invalid email - no angle brackets', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Foo foo@example.com\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + '"Signed-off-by" trailer has invalid email', 'message') + tt.equal(opts.string, + 'Signed-off-by: Foo foo@example.com', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('invalid email - no domain', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Foo \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + '"Signed-off-by" trailer has invalid email', 'message') + tt.equal(opts.string, + 'Signed-off-by: Foo ', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('missing name', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + '"Signed-off-by" trailer has invalid email', 'message') + tt.equal(opts.string, + 'Signed-off-by: ', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('release commit', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: '2024-01-01, Version 22.0.0 (Current)\n' + + '\n' + + 'Notable changes:\n' + + '\n' + + 'Some changes here.' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'skipping sign-off for release commit', 'message') + tt.equal(opts.level, 'skip', 'level') + } + + Rule.validate(context) + }) + + t.test('deps commit without sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Richard Lau', + email: 'richard.lau@ibm.com', + date: '2026-03-28T22:57:44Z' + }, + message: 'deps: V8: cherry-pick cf1bce40a5ef\n' + + '\n' + + 'Original commit message:\n' + + '\n' + + ' [wasm] Fix S128Const on big endian\n' + + '\n' + + 'Refs: https://github.com/v8/v8/commit/cf1bce40a5ef\n' + + 'PR-URL: https://github.com/nodejs/node/pull/62449\n' + + 'Reviewed-By: Guy Bedford ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'skipping sign-off for deps commit', 'message') + tt.equal(opts.level, 'skip', 'level') + } + + Rule.validate(context) + }) + + t.test('deps commit with sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Richard Lau', + email: 'richard.lau@ibm.com', + date: '2026-03-28T22:57:44Z' + }, + message: 'deps: V8: cherry-pick cf1bce40a5ef\n' + + '\n' + + 'Original commit message:\n' + + '\n' + + ' [wasm] Fix S128Const on big endian\n' + + '\n' + + 'Signed-off-by: Richard Lau \n' + + 'Refs: https://github.com/v8/v8/commit/cf1bce40a5ef\n' + + 'PR-URL: https://github.com/nodejs/node/pull/62449\n' + + 'Reviewed-By: Guy Bedford ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'skipping sign-off for deps commit', 'message') + tt.equal(opts.level, 'skip', 'level') + } + + Rule.validate(context) + }) + + t.test('mixed deps commit without sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2026-03-28T22:57:44Z' + }, + message: 'deps,src: update V8 and fix build\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'Commit with non-deps changes should have a ' + + '"Signed-off-by" trailer', 'message') + tt.equal(opts.level, 'warn', 'level') + } + + Rule.validate(context) + }) + + t.test('mixed deps commit with valid sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2026-03-28T22:57:44Z' + }, + message: 'deps,src: update V8 and fix build\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Foo \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, 'has valid Signed-off-by', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('backport commit without sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Anna Henningsen', + email: 'anna@addaleax.net', + date: '2026-03-03T23:12:18Z' + }, + message: 'src: convert context_frame field in AsyncWrap\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'PR-URL: https://github.com/nodejs/node/pull/62103\n' + + 'Backport-PR-URL: https://github.com/nodejs/node/pull/62357\n' + + 'Reviewed-By: Anna Henningsen ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'skipping sign-off for backport commit', 'message') + tt.equal(opts.level, 'skip', 'level') + } + + Rule.validate(context) + }) + + t.test('backport commit with sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Anna Henningsen', + email: 'anna@addaleax.net', + date: '2026-03-03T23:12:18Z' + }, + message: 'src: convert context_frame field in AsyncWrap\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Anna Henningsen \n' + + 'PR-URL: https://github.com/nodejs/node/pull/62103\n' + + 'Backport-PR-URL: https://github.com/nodejs/node/pull/62357\n' + + 'Reviewed-By: Anna Henningsen ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'skipping sign-off for backport commit', 'message') + tt.equal(opts.level, 'skip', 'level') + } + + Rule.validate(context) + }) + + t.test('multiple valid sign-offs', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Foo \n' + + 'Signed-off-by: Bar \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Baz ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, 'has valid Signed-off-by', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('bot author with sign-off - name with [bot] suffix', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'dependabot[bot]', + email: '49699333+dependabot[bot]@users.noreply.github.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: dependabot[bot] ' + + '<49699333+dependabot[bot]@users.noreply.github.com>\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'bot commit should not have a "Signed-off-by" trailer', 'message') + tt.match(opts.string, /dependabot\[bot\]/, 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'warn', 'level') + } + + Rule.validate(context) + }) + + t.test('bot author with sign-off - GitHub bot noreply email', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'some-tool[bot]', + email: 'some-tool[bot]@users.noreply.github.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: some-tool[bot] ' + + '\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'bot commit should not have a "Signed-off-by" trailer', 'message') + tt.match(opts.string, /some-tool\[bot\]/, 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'warn', 'level') + } + + Rule.validate(context) + }) + + t.test('bot author without sign-off', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'dependabot[bot]', + email: '49699333+dependabot[bot]@users.noreply.github.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'build(deps): bump some-package from 1.0.0 to 2.0.0\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'skipping sign-off for bot commit', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('bot author - Node.js GitHub Bot', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Node.js GitHub Bot', + email: 'github-bot@iojs.org', + date: '2026-03-22T00:28:21Z' + }, + message: 'test: update WPT for url to fc3e651593\n' + + '\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'skipping sign-off for bot commit', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('bot author with human sign-off', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'dependabot[bot]', + email: '49699333+dependabot[bot]@users.noreply.github.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'build(deps): bump some-package from 1.0.0 to 2.0.0\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Human \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + 'bot commit should not have a "Signed-off-by" trailer', 'message') + tt.equal(opts.string, + 'Signed-off-by: Human ', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'warn', 'level') + } + + Rule.validate(context) + }) + + t.test('author email mismatch', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Alice', + email: 'alice@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Bob \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, + '"Signed-off-by" email does not match the ' + + 'commit author email', 'message') + tt.equal(opts.string, + 'Signed-off-by: Bob ', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'warn', 'level') + } + + Rule.validate(context) + }) + + t.test('author email matches one of multiple sign-offs', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Alice', + email: 'alice@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Bob \n' + + 'Signed-off-by: Alice \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, 'has valid Signed-off-by', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('no author info available - skips mismatch check', (tt) => { + tt.plan(4) + const v = new Validator() + // Simulate stdin JSON input which has no author info. + // Use a plain object context with no author field. + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Someone \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, 'has valid Signed-off-by', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('author email mismatch is case-insensitive', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'Foo@Example.COM', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Foo \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'signed-off-by', 'id') + tt.equal(opts.message, 'has valid Signed-off-by', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('multiple sign-offs with some invalid emails', (tt) => { + tt.plan(9) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Bad One bad@\n' + + 'Signed-off-by: Foo \n' + + 'Signed-off-by: Bad Two noangles@example.com\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + const reports = [] + context.report = (opts) => { + reports.push(opts) + } + + Rule.validate(context) + + // Should get 3 reports: 2 fails for invalid emails + 1 pass + tt.equal(reports.length, 3, 'total reports') + + tt.equal(reports[0].level, 'fail', 'first invalid is fail') + tt.equal(reports[0].message, + '"Signed-off-by" trailer has invalid email', 'first message') + tt.equal(reports[0].string, + 'Signed-off-by: Bad One bad@', 'first string') + + tt.equal(reports[1].level, 'fail', 'second invalid is fail') + tt.equal(reports[1].message, + '"Signed-off-by" trailer has invalid email', 'second message') + tt.equal(reports[1].string, + 'Signed-off-by: Bad Two noangles@example.com', 'second string') + + tt.equal(reports[2].level, 'pass', 'valid one passes') + tt.equal(reports[2].message, + 'has valid Signed-off-by', 'pass message') + }) + + t.test('human author with only bot sign-offs', (tt) => { + tt.plan(5) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Alice', + email: 'alice@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: dependabot[bot] ' + + '<49699333+dependabot[bot]@users.noreply.github.com>\n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + const reports = [] + context.report = (opts) => { + reports.push(opts) + } + + Rule.validate(context) + + // Should get 2 reports: warn for bot sign-off + fail for no human sign-off + tt.equal(reports.length, 2, 'total reports') + + tt.equal(reports[0].level, 'warn', 'bot sign-off is warn') + tt.match(reports[0].string, /dependabot\[bot\]/, 'bot string') + + tt.equal(reports[1].level, 'fail', 'no human sign-off is fail') + tt.equal(reports[1].message, + 'Commit must have a "Signed-off-by" trailer from a human author', + 'fail message') + }) + + t.test('multiple sign-offs with human and bot', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Signed-off-by: Foo \n' + + 'Signed-off-by: dependabot[bot] \n' + + 'PR-URL: https://github.com/nodejs/node/pull/1234\n' + + 'Reviewed-By: Bar ' + }, v) + + const reports = [] + context.report = (opts) => { + reports.push(opts) + } + + Rule.validate(context) + + // Should get 2 reports: warn for bot + pass for human + tt.equal(reports.length, 2, 'total reports') + + tt.equal(reports[0].level, 'warn', 'bot is warn') + tt.equal(reports[0].message, + '"Signed-off-by" must be from a human author, ' + + 'not a bot or AI agent', 'bot message') + tt.match(reports[0].string, + /dependabot\[bot\]/, 'bot string') + + tt.equal(reports[1].level, 'pass', 'human passes') + tt.equal(reports[1].message, + 'has valid Signed-off-by', 'pass message') + tt.equal(reports[1].string, '', 'pass string') + }) + + t.end() +}) diff --git a/test/validator.js b/test/validator.js index d1c16d2..27dcf5d 100644 --- a/test/validator.js +++ b/test/validator.js @@ -15,6 +15,7 @@ CommitDate: Wed Apr 20 13:28:35 2016 -0700 streams so that they all handle nulls in object mode the same way by considering them invalid chunks. + Signed-off-by: Calvin Metcalf PR-URL: https://github.com/nodejs/node/pull/6170 Reviewed-By: James M Snell Reviewed-By: Matteo Collina @@ -31,6 +32,7 @@ Date: Tue Mar 29 08:09:37 2016 -0500 The offending commit broke certain usages of piping from stdin. Fixes: https://github.com/nodejs/node/issues/5927 + Signed-off-by: Evan Lucas PR-URL: https://github.com/nodejs/node/pull/5947 Reviewed-By: Matteo Collina Reviewed-By: Alexis Campailla @@ -50,6 +52,7 @@ Date: Fri Apr 15 13:32:36 2016 +0200 [1] https://github.com/nodejs/node/pull/5172/commits/ae18bbef48d87d9c641df85369f62cfd5ed8c250 Fixes: https://github.com/nodejs/node/issues/6214 + Signed-off-by: Michaël Zasso PR-URL: https://github.com/nodejs/node/pull/6215 Reviewed-By: James M Snell Reviewed-By: Brian White ` @@ -63,6 +66,7 @@ Date: Thu Mar 3 10:10:46 2016 -0600 The properties on memoryUsage were not checked before, this commit checks them. + Signed-off-by: Wyatt Preul PR-URL: #5546 Reviewed-By: Colin Ihrig ` @@ -73,7 +77,9 @@ Date: Thu Mar 3 10:10:46 2016 -0600 test: check memoryUsage properties The properties on memoryUsage were not checked before, - this commit checks them.` + this commit checks them. + + Signed-off-by: Wyatt Preul ` /* eslint-disable */ const str6 = { @@ -94,7 +100,7 @@ const str6 = { "sha": "b505c0ffa0555730e9f4cdb391d1ebeb48bb2f59", "url": "https://api.github.com/repos/nodejs/node/git/trees/b505c0ffa0555730e9f4cdb391d1ebeb48bb2f59" }, - "message": "fs: fix handling of `uv_stat_t` fields\n\n`FChown` and `Chown` test that the `uid` and `gid` parameters\nthey receive are unsigned integers, but `Stat()` and `FStat()`\nwould return the corresponding fields of `uv_stat_t` as signed\nintegers. Applications which pass those these values directly\nto `Chown` may fail\n(e.g. for `nobody` on OS X, who has an `uid` of `-2`, see e.g.\nhttps://github.com/nodejs/node-v0.x-archive/issues/5890).\n\nThis patch changes the `Integer::New()` call for `uid` and `gid`\nto `Integer::NewFromUnsigned()`.\n\nAll other fields are kept as they are, for performance, but\nstrictly speaking the respective sizes of those\nfields aren’t specified, either.\n\nRef: https://github.com/npm/npm/issues/13918\nPR-URL: https://github.com/nodejs/node/pull/8515\nReviewed-By: Ben Noordhuis \nReviewed-By: Sakthipriyan Vairamani \nReviewed-By: James M Snell \n\nundo accidental change to other fields of uv_fs_stat", + "message": "fs: fix handling of `uv_stat_t` fields\n\n`FChown` and `Chown` test that the `uid` and `gid` parameters\nthey receive are unsigned integers, but `Stat()` and `FStat()`\nwould return the corresponding fields of `uv_stat_t` as signed\nintegers. Applications which pass those these values directly\nto `Chown` may fail\n(e.g. for `nobody` on OS X, who has an `uid` of `-2`, see e.g.\nhttps://github.com/nodejs/node-v0.x-archive/issues/5890).\n\nThis patch changes the `Integer::New()` call for `uid` and `gid`\nto `Integer::NewFromUnsigned()`.\n\nAll other fields are kept as they are, for performance, but\nstrictly speaking the respective sizes of those\nfields aren’t specified, either.\n\nSigned-off-by: Anna Henningsen \nRef: https://github.com/npm/npm/issues/13918\nPR-URL: https://github.com/nodejs/node/pull/8515\nReviewed-By: Ben Noordhuis \nReviewed-By: Sakthipriyan Vairamani \nReviewed-By: James M Snell \n\nundo accidental change to other fields of uv_fs_stat", "parents": [ { "sha": "4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4", @@ -110,6 +116,8 @@ Author: Wyatt Preul Date: Thu Mar 3 10:10:46 2016 -0600 test: check memoryUsage properties. + + Signed-off-by: Wyatt Preul ` const str8 = `commit 7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 @@ -117,6 +125,8 @@ Author: Wyatt Preul Date: Thu Mar 3 10:10:46 2016 -0600 test: Check memoryUsage properties + + Signed-off-by: Wyatt Preul ` const str9 = `commit 7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 @@ -124,6 +134,8 @@ Author: Wyatt Preul Date: Thu Mar 3 10:10:46 2016 -0600 test: Check memoryUsage properties. + + Signed-off-by: Wyatt Preul ` const str10 = `commit b04fe688d5859f707cf1a5e0206967268118bf7a @@ -169,6 +181,8 @@ Date: Sat Oct 22 10:22:43 2022 +0200 Revert "deps: V8: forward declaration of \`Rtl*FunctionTable\`" This reverts commit 01bc8e6fd81314e76c7fb0d09e5310f609e48bee. + + Signed-off-by: Michaël Zasso ` test('Validator - misc', (t) => { @@ -395,7 +409,7 @@ test('Validator - real commits', (t) => { const item = filtered[0] tt.equal(item.id, 'metadata-end', 'id') tt.equal(item.message, 'commit metadata at end of message', 'message') - tt.equal(item.line, 22, 'line') + tt.equal(item.line, 23, 'line') tt.equal(item.column, 0, 'column') tt.end() }) From 58c48dcead19eb9c868a7d67bc42bbc5a418725b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Mon, 13 Apr 2026 13:12:27 +0100 Subject: [PATCH 2/6] fix(rules): add line-length exemptions for DCO sign-offs (#142) Refs: https://github.com/nodejs/core-validate-commit/pull/141 --- lib/rules/line-length.js | 4 ++++ test/rules/line-length.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lib/rules/line-length.js b/lib/rules/line-length.js index 3a568a9..9a3be31 100644 --- a/lib/rules/line-length.js +++ b/lib/rules/line-length.js @@ -36,6 +36,10 @@ export default { if (/https?:\/\//.test(line)) { continue } // Skip co-authorship. if (/^co-authored-by:/i.test(line)) { continue } + // Skip DCO sign-offs. + if (/^signed-off-by:/i.test(line)) { continue } + // Skip agentic assistants. + if (/^assisted-by:/i.test(line)) { continue } if (line.length > len) { failed = true diff --git a/test/rules/line-length.js b/test/rules/line-length.js index 1eb781d..78e8f33 100644 --- a/test/rules/line-length.js +++ b/test/rules/line-length.js @@ -161,5 +161,37 @@ https://${'very-'.repeat(80)}-long-url.org/ tt.end() }) + t.test('Signed-off-by and Assisted-by lines', (tt) => { + const v = new Validator() + + const good = new Commit({ + sha: '016b3921626b58d9b595c90141e65c6fbe0c78e2', + author: { + name: 'John Connor', + email: '9092381+JConnor1985@users.noreply.github.com', + date: '2026-04-10T16:38:01Z' + }, + message: [ + 'Signed-off-by: John Connor <9092381+JConnor1985@users.noreply.github.com>', + 'Assisted-by: The Longest-Named Code Agent In The World ' + ].join('\n') + }, v) + + good.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'line-length', 'id') + tt.equal(opts.string, '', 'string') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(good, { + options: { + length: 72 + } + }) + + tt.end() + }) + t.end() }) From cd1a6a88c8b6d8fcdbc20355456934c2be37264f Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 22 Apr 2026 17:55:04 +0100 Subject: [PATCH 3/6] feat!: parse trailers using `git` if available, allow longer lines (#144) --- lib/gitlint-parser.js | 171 +++++++++++++ lib/rules/line-length.js | 35 ++- lib/rules/signed-off-by.js | 10 +- lib/validator.js | 2 +- npm-shrinkwrap.json | 36 +-- package.json | 2 +- test/gitlint-parser.js | 321 ++++++++++++++++++++++++ test/rules/assisted-by-is-trailer.js | 2 +- test/rules/co-authored-by-is-trailer.js | 2 +- test/rules/fixes-url.js | 2 +- test/rules/line-after-title.js | 2 +- test/rules/line-length.js | 137 +++++++++- test/rules/reviewers.js | 2 +- test/rules/signed-off-by.js | 2 +- test/rules/subsystem.js | 2 +- test/rules/title-format.js | 2 +- test/validator.js | 54 +--- 17 files changed, 662 insertions(+), 122 deletions(-) create mode 100644 lib/gitlint-parser.js create mode 100644 test/gitlint-parser.js diff --git a/lib/gitlint-parser.js b/lib/gitlint-parser.js new file mode 100644 index 0000000..7cfb775 --- /dev/null +++ b/lib/gitlint-parser.js @@ -0,0 +1,171 @@ +import { spawnSync } from 'node:child_process' +import Base from 'gitlint-parser-base' + +const revertRE = /Revert "(.*)"$/ +const workingRE = /Working on v([\d]+)\.([\d]+).([\d]+)$/ +const releaseRE = /([\d]{4})-([\d]{2})-([\d]{2}),? Version/ +const reviewedByRE = /^Reviewed-By: (.*)$/ +const fixesRE = /^Fixes: (.*)$/ +const prUrlRE = /^PR-URL: (.*)$/ +const refsRE = /^Refs?: (.*)$/ + +export default class Parser extends Base { + constructor (str, validator) { + super(str, validator) + this.subsystems = [] + this.fixes = [] + this.prUrl = null + this.refs = [] + this.reviewers = [] + this._metaStart = 0 + this._metaEnd = 0 + this._parse() + } + + _setMetaStart (n) { + if (this._metaStart) return + this._metaStart = n + } + + _setMetaEnd (n) { + if (n < this._metaEnd) return + this._metaEnd = n + } + + _parseTrailers (body) { + const interpretTrailers = commitMessage => spawnSync('git', [ + 'interpret-trailers', '--only-trailers', '--only-input', '--no-divider' + ], { + encoding: 'utf-8', + input: `'dummy subject\n\n${commitMessage.join('\n')}\n` + }).stdout + + let originalTrailers + try { + originalTrailers = interpretTrailers(body).trim() + } catch (err) { + console.warn('git is not available, trailers detection might be a bit ' + + 'off which is acceptable in most cases', err) + return body + } + const trailerFreeBody = body.slice(1) // clone, and remove the first empty line + const stillInTrailers = () => { + const result = interpretTrailers(trailerFreeBody) + return result.length && originalTrailers.startsWith(result.trim()) + } + for (let i = trailerFreeBody.length - 1; stillInTrailers(); i--) { + // Remove last line until git no longer detects any trailers + trailerFreeBody.pop() + } + this._metaStart = trailerFreeBody.length + 1 // the subject line needs to be counted + for (let i = trailerFreeBody.length - 1; trailerFreeBody[i] === ''; i--) { + // Remove additional empty line(s) + trailerFreeBody.pop() + } + this._metaEnd = body.length - 1 + this.trailerFreeBody = trailerFreeBody + return (this.trailers = originalTrailers.split('\n')) + } + + _parse () { + const revert = this.isRevert() + if (!revert) { + this.subsystems = getSubsystems(this.title || '') + } else { + const matches = this.title.match(revertRE) + if (matches) { + const title = matches[1] + this.subsystems = getSubsystems(title) + } + } + + const trailers = this._parseTrailers(this.body) + + for (let i = 0; i < trailers.length; i++) { + const line = trailers[i] + const reviewedBy = reviewedByRE.exec(line) + if (reviewedBy) { + this._setMetaStart(i) + this._setMetaEnd(i) + this.reviewers.push(reviewedBy[1]) + continue + } + + const fixes = fixesRE.exec(line) + if (fixes) { + this._setMetaStart(i) + this._setMetaEnd(i) + this.fixes.push(fixes[1]) + continue + } + + const prUrl = prUrlRE.exec(line) + if (prUrl) { + this._setMetaStart(i) + this._setMetaEnd(i) + this.prUrl = prUrl[1] + continue + } + + const refs = refsRE.exec(line) + if (refs) { + this._setMetaStart(i) + this._setMetaEnd(i) + this.refs.push(refs[1]) + continue + } + + if (this._metaStart && !this._metaEnd) { this._setMetaEnd(i) } + } + } + + isRevert () { + return revertRE.test(this.title) + } + + isWorkingCommit () { + return workingRE.test(this.title) + } + + isReleaseCommit () { + return releaseRE.test(this.title) + } + + toJSON () { + return { + sha: this.sha, + title: this.title, + subsystems: this.subsystems, + author: this.author, + date: this.date, + fixes: this.fixes, + refs: this.refs, + prUrl: this.prUrl, + reviewers: this.reviewers, + body: this.body, + trailers: this.trailers, + trailerFreeBody: this.trailerFreeBody, + revert: this.isRevert(), + release: this.isReleaseCommit(), + working: this.isWorkingCommit(), + metadata: { + start: this._metaStart, + end: this._metaEnd + } + } + } +} + +function getSubsystems (str) { + str = str || '' + const colon = str.indexOf(':') + if (colon === -1) { + return [] + } + + const subStr = str.slice(0, colon) + const subs = subStr.split(',') + return subs.map((item) => { + return item.trim() + }) +} diff --git a/lib/rules/line-length.js b/lib/rules/line-length.js index 9a3be31..dfa573a 100644 --- a/lib/rules/line-length.js +++ b/lib/rules/line-length.js @@ -7,10 +7,12 @@ export default { recommended: true }, defaults: { - length: 72 + length: 72, + trailerLength: 120 }, options: { - length: 72 + length: 72, + trailerLength: 120 }, validate: (context, rule) => { const len = rule.options.length @@ -27,19 +29,14 @@ export default { return } let failed = false - for (let i = 0; i < parsed.body.length; i++) { - const line = parsed.body[i] + const body = parsed.trailerFreeBody ?? parsed.body + for (let i = 0; i < body.length; i++) { + const line = body[i] // Skip quoted lines, e.g. for original commit messages of V8 backports. if (line.startsWith(' ')) { continue } // Skip lines with URLs. if (/https?:\/\//.test(line)) { continue } - // Skip co-authorship. - if (/^co-authored-by:/i.test(line)) { continue } - // Skip DCO sign-offs. - if (/^signed-off-by:/i.test(line)) { continue } - // Skip agentic assistants. - if (/^assisted-by:/i.test(line)) { continue } if (line.length > len) { failed = true @@ -48,7 +45,23 @@ export default { message: `Line should be <= ${len} columns.`, string: line, maxLength: len, - line: i, + line: i + parsed.body.length - body.length, + column: len, + level: 'fail' + }) + } + } + for (let i = 0; i < (parsed.trailers?.length ?? 0); i++) { + const line = parsed.trailers[i] + const len = rule.options.trailerLength + if (line.length > len) { + failed = true + context.report({ + id, + message: `Trailer should be <= ${len} columns.`, + string: line, + maxLength: len, + line: i + parsed.body.length - parsed.trailers.length, column: len, level: 'fail' }) diff --git a/lib/rules/signed-off-by.js b/lib/rules/signed-off-by.js index b92d91d..3bdf6ce 100644 --- a/lib/rules/signed-off-by.js +++ b/lib/rules/signed-off-by.js @@ -67,10 +67,12 @@ export default { return } + const body = parsed.trailers ?? parsed.body + // Backport commits (identified by a Backport-PR-URL trailer) are // cherry-picks of existing commits into release branches. The // original commit was already validated. - if (parsed.body.some((line) => backportPattern.test(line))) { + if (body.some((line) => backportPattern.test(line))) { context.report({ id, message: 'skipping sign-off for backport commit', @@ -80,9 +82,9 @@ export default { return } - const signoffs = parsed.body - .map((line, i) => [line, i]) - .filter(([line]) => signoffPattern.test(line)) + const signoffs = body + .filter(line => signoffPattern.test(line)) + .map((line, i) => [line, i + parsed.body.length - body.length]) // Bot-authored commits don't need a sign-off. // If they have one, warn; otherwise pass. diff --git a/lib/validator.js b/lib/validator.js index dfa2a94..c28cc94 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -1,5 +1,5 @@ import EE from 'node:events' -import Parser from 'gitlint-parser-node' +import Parser from './gitlint-parser.js' import BaseRule from './rule.js' // Rules diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9a25b80..87e41c9 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -9,7 +9,7 @@ "version": "5.0.1", "license": "MIT", "dependencies": { - "gitlint-parser-node": "^1.1.0" + "gitlint-parser-base": "^2.0.0" }, "bin": { "core-validate-commit": "bin/cmd.js" @@ -55,7 +55,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1031,7 +1030,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1223,19 +1221,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "extraneous": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/check-pkg": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-pkg/-/check-pkg-2.1.1.tgz", @@ -1918,7 +1903,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2126,7 +2110,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -2194,7 +2177,6 @@ "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", @@ -2221,7 +2203,6 @@ "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2238,7 +2219,6 @@ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -2407,7 +2387,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2883,15 +2862,6 @@ "integrity": "sha512-kWkbGGH55AigPQgTz9ZEomv8uQn2GRZiFgAg+SbTIV+8ineU8f3QDeAQHzS7h9yKVuE7cjcgQm0ENmHPM0bxMQ==", "license": "MIT" }, - "node_modules/gitlint-parser-node": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gitlint-parser-node/-/gitlint-parser-node-1.1.0.tgz", - "integrity": "sha512-h99xAvajhfkTtwVo8q9oBlL8+QrljJltguykpszQWyu0D9SVE5+5CqfEn9qn5IJ254Q2mr1ByjrFu9Rs2niSyA==", - "license": "MIT", - "dependencies": { - "gitlint-parser-base": "^2.0.0" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6008,7 +5978,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -6465,7 +6434,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6594,7 +6562,6 @@ ], "inBundle": true, "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001565", "electron-to-chromium": "^1.4.601", @@ -7365,7 +7332,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" diff --git a/package.json b/package.json index a63502e..dbe4224 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "test-ci": "npm run test && c8 report --reporter=lcov" }, "dependencies": { - "gitlint-parser-node": "^1.1.0" + "gitlint-parser-base": "^2.0.0" }, "devDependencies": { "c8": "^7.13.0", diff --git a/test/gitlint-parser.js b/test/gitlint-parser.js new file mode 100644 index 0000000..e00d5ab --- /dev/null +++ b/test/gitlint-parser.js @@ -0,0 +1,321 @@ +import { test } from 'tap' +import Parser from '../lib/gitlint-parser.js' + +test('Parser', (t) => { + t.test('basic commit with PR-URL and reviewers', (tt) => { + tt.plan(10) + const input = `commit e7c077c610afa371430180fbd447bfef60ebc5ea +Author: Calvin Metcalf +Date: Tue Apr 12 15:42:23 2016 -0400 + + stream: make null an invalid chunk to write in object mode + + this harmonizes behavior between readable, writable, and transform + streams so that they all handle nulls in object mode the same way by + considering them invalid chunks. + + PR-URL: https://github.com/nodejs/node/pull/6170 + Reviewed-By: James M Snell + Reviewed-By: Matteo Collina ` + + const data = { name: 'biscuits' } + + const v = { + report: (obj) => { + tt.pass('called report') + tt.equal(obj.data, data, 'obj') + } + } + const p = new Parser(input, v) + const c = p.toJSON() + tt.equal(c.sha, 'e7c077c610afa371430180fbd447bfef60ebc5ea', 'sha') + tt.equal(c.author, 'Calvin Metcalf ', 'author') + tt.equal(c.date, 'Tue Apr 12 15:42:23 2016 -0400', 'date') + tt.deepEqual(c.subsystems, ['stream'], 'subsystems') + tt.deepEqual(c.fixes, [], 'fixes') + tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/6170', 'prUrl') + tt.deepEqual(c.reviewers, [ + 'James M Snell ', + 'Matteo Collina ' + ], 'reviewers') + tt.deepEqual(c.metadata, { + start: 5, + end: 7 + }, 'metadata') + p.report(data) + }) + + t.test('basic commit using format=fuller', (tt) => { + tt.plan(9) + const input = `commit e7c077c610afa371430180fbd447bfef60ebc5ea +Author: Calvin Metcalf +AuthorDate: Tue Apr 12 15:42:23 2016 -0400 +Commit: James M Snell +CommitDate: Tue Apr 12 15:42:23 2016 -0400 + + stream: make null an invalid chunk to write in object mode + + this harmonizes behavior between readable, writable, and transform + streams so that they all handle nulls in object mode the same way by + considering them invalid chunks. + + PR-URL: https://github.com/nodejs/node/pull/6170 + Reviewed-By: James M Snell + Reviewed-By: Matteo Collina ` + + const data = { name: 'biscuits' } + + const v = { + report: (obj) => { + tt.pass('called report') + tt.equal(obj.data, data, 'obj') + } + } + const p = new Parser(input, v) + const c = p.toJSON() + tt.equal(c.sha, 'e7c077c610afa371430180fbd447bfef60ebc5ea', 'sha') + tt.equal(c.author, 'Calvin Metcalf ', 'author') + tt.equal(c.date, 'Tue Apr 12 15:42:23 2016 -0400', 'date') + tt.deepEqual(c.subsystems, ['stream'], 'subsystems') + tt.deepEqual(c.fixes, [], 'fixes') + tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/6170', 'prUrl') + tt.deepEqual(c.reviewers, [ + 'James M Snell ', + 'Matteo Collina ' + ], 'reviewers') + p.report(data) + }) + + t.test('revert commit', (tt) => { + tt.plan(13) + const input = `commit 1d4c7993a9c6dcacaca5074a80b1043e977c43fb +Author: Rod Vagg +Date: Wed Jun 8 16:32:10 2016 +1000 + + Revert "test: change duration_ms to duration" + + This reverts commit d413378e513c47a952f7979c74f4a4d9f144ca7d. + + PR-URL: https://github.com/nodejs/node/pull/7216 + Reviewed-By: Colin Ihrig + Reviewed-By: James M Snell + Reviewed-By: Michaël Zasso + Reviewed-By: Johan Bergström ` + const data = { name: 'biscuits' } + + const v = { + report: (obj) => { + tt.pass('called report') + tt.equal(obj.data, data, 'obj') + } + } + const p = new Parser(input, v) + const c = p.toJSON() + tt.equal(c.sha, '1d4c7993a9c6dcacaca5074a80b1043e977c43fb', 'sha') + tt.equal(c.author, 'Rod Vagg ', 'author') + tt.equal(c.date, 'Wed Jun 8 16:32:10 2016 +1000', 'date') + tt.deepEqual(c.subsystems, ['test'], 'subsystems') + tt.deepEqual(c.fixes, [], 'fixes') + tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/7216', 'prUrl') + tt.deepEqual(c.reviewers, [ + 'Colin Ihrig ', + 'James M Snell ', + 'Michaël Zasso ', + 'Johan Bergström ' + ], 'reviewers') + + tt.equal(c.revert, true, 'revert') + tt.equal(c.release, false, 'release') + tt.equal(c.working, false, 'working') + tt.deepEqual(c.metadata, { + start: 3, + end: 7 + }, 'metadata') + p.report(data) + }) + + t.test('backport commit', (tt) => { + const input = `commit 5cdbbdf94d6f656230293ddd2a71dedf11cab17d +Author: Ben Noordhuis +Date: Thu Jul 7 14:29:32 2016 -0700 + + deps: cherry-pick 1f53e42 from v8 upstream + + Original commit message: + + Handle symbols in FrameMirror#invocationText(). + + Fix a TypeError when putting together the invocationText for a + symbol method's stack frame. + + See https://github.com/nodejs/node/issues/7536. + + Review-Url: https://codereview.chromium.org/2122793003 + Cr-Commit-Position: refs/heads/master@{#37597} + + Fixes: https://github.com/nodejs/node/issues/7536 + PR-URL: https://github.com/nodejs/node/pull/7612 + Reviewed-By: Colin Ihrig + Reviewed-By: Michaël Zasso ` + const data = { name: 'biscuits' } + + const v = { + report: (obj) => { + tt.pass('called report') + tt.equal(obj.data, data, 'obj') + } + } + const p = new Parser(input, v) + const c = p.toJSON() + tt.equal(c.sha, '5cdbbdf94d6f656230293ddd2a71dedf11cab17d', 'sha') + tt.equal(c.author, 'Ben Noordhuis ', 'author') + tt.equal(c.date, 'Thu Jul 7 14:29:32 2016 -0700', 'date') + tt.deepEqual(c.subsystems, ['deps'], 'subsystems') + tt.deepEqual(c.fixes, [ + 'https://github.com/nodejs/node/issues/7536' + ], 'fixes') + tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/7612', 'prUrl') + tt.deepEqual(c.reviewers, [ + 'Colin Ihrig ', + 'Michaël Zasso ' + ], 'reviewers') + + tt.equal(c.revert, false, 'revert') + tt.equal(c.release, false, 'release') + tt.equal(c.working, false, 'working') + tt.deepEqual(c.metadata, { + start: 13, + end: 16 + }, 'metadata') + p.report(data) + tt.end() + }) + + t.test('release commit', (tt) => { + /* eslint-disable */ + const input = `commit 6a9438343bb63e2c1fc028f2e9387e6ae41b9fc8 +Author: Evan Lucas +Date: Thu Jun 23 07:27:44 2016 -0500 + + 2016-06-23, Version 5.12.0 (Stable) + + Notable changes: + + This is a security release. All Node.js users should consult the security + release summary at https://nodejs.org/en/blog/vulnerability/june-2016-security-releases + for details on patched vulnerabilities. + + * **buffer** + * backport allocUnsafeSlow (Сковорода Никита Андреевич) [#7169](https://github.com/nodejs/node/pull/7169) + * ignore negative allocation lengths (Anna Henningsen) [#7221](https://github.com/nodejs/node/pull/7221) + * **deps**: backport 3a9bfec from v8 upstream (Ben Noordhuis) [nodejs/node-private#40](https://github.com/nodejs/node-private/pull/40) + * Fixes a Buffer overflow vulnerability discovered in v8. More details + can be found in the CVE (CVE-2016-1699). + + PR-URL: https://github.com/nodejs/node-private/pull/51` + /* eslint-enable */ + const v = { + report: () => {} + } + const p = new Parser(input, v) + const c = p.toJSON() + tt.equal(c.sha, '6a9438343bb63e2c1fc028f2e9387e6ae41b9fc8', 'sha') + tt.equal(c.author, 'Evan Lucas ', 'author') + tt.equal(c.date, 'Thu Jun 23 07:27:44 2016 -0500', 'date') + tt.deepEqual(c.subsystems, [], 'subsystems') + tt.deepEqual(c.fixes, [], 'fixes') + tt.equal(c.prUrl, 'https://github.com/nodejs/node-private/pull/51', 'prUrl') + tt.deepEqual(c.reviewers, [], 'reviewers') + tt.deepEqual(c.metadata, { + start: 14, + end: 14 + }, 'metadata') + + tt.equal(c.revert, false, 'revert') + tt.equal(c.release, true, 'release') + tt.equal(c.working, false, 'working') + tt.end() + }) + + t.test('extra meta', (tt) => { + /* eslint-disable */ + const input = { + "sha": "c5545f2c63fe30b0cfcdafab18c26df8286881d0", + "url": "https://api.github.com/repos/nodejs/node/git/commits/c5545f2c63fe30b0cfcdafab18c26df8286881d0", + "html_url": "https://github.com/nodejs/node/commit/c5545f2c63fe30b0cfcdafab18c26df8286881d0", + "author": { + "name": "Anna Henningsen", + "email": "anna@addaleax.net", + "date": "2016-09-13T10:57:49Z" + }, + "committer": { + "name": "Anna Henningsen", + "email": "anna@addaleax.net", + "date": "2016-09-19T12:50:57Z" + }, + "tree": { + "sha": "b505c0ffa0555730e9f4cdb391d1ebeb48bb2f59", + "url": "https://api.github.com/repos/nodejs/node/git/trees/b505c0ffa0555730e9f4cdb391d1ebeb48bb2f59" + }, + "message": "fs: fix handling of `uv_stat_t` fields\n\n`FChown` and `Chown` test that the `uid` and `gid` parameters\nthey receive are unsigned integers, but `Stat()` and `FStat()`\nwould return the corresponding fields of `uv_stat_t` as signed\nintegers. Applications which pass those these values directly\nto `Chown` may fail\n(e.g. for `nobody` on OS X, who has an `uid` of `-2`, see e.g.\nhttps://github.com/nodejs/node-v0.x-archive/issues/5890).\n\nThis patch changes the `Integer::New()` call for `uid` and `gid`\nto `Integer::NewFromUnsigned()`.\n\nAll other fields are kept as they are, for performance, but\nstrictly speaking the respective sizes of those\nfields aren’t specified, either.\n\nRef: https://github.com/npm/npm/issues/13918\nPR-URL: https://github.com/nodejs/node/pull/8515\nReviewed-By: Ben Noordhuis \nReviewed-By: Sakthipriyan Vairamani \nReviewed-By: James M Snell ", + "parents": [ + { + "sha": "4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4", + "url": "https://api.github.com/repos/nodejs/node/git/commits/4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4", + "html_url": "https://github.com/nodejs/node/commit/4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4" + } + ] + } + /* eslint-enable */ + const v = { + report: () => {} + } + const p = new Parser(input, v) + const c = p.toJSON() + tt.equal(c.sha, 'c5545f2c63fe30b0cfcdafab18c26df8286881d0', 'sha') + tt.equal(c.author, 'Anna Henningsen ', 'author') + tt.equal(c.date, '2016-09-13T10:57:49Z', 'date') + tt.deepEqual(c.subsystems, ['fs'], 'subsystems') + tt.deepEqual(c.fixes, [], 'fixes') + tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/8515', 'prUrl') + tt.deepEqual(c.reviewers, [ + 'Ben Noordhuis ', + 'Sakthipriyan Vairamani ', + 'James M Snell ' + ], 'reviewers') + tt.deepEqual(c.metadata, { + start: 16, + end: 20 + }, 'metadata') + tt.deepEqual(c.trailers, [ + 'Ref: https://github.com/npm/npm/issues/13918', + 'PR-URL: https://github.com/nodejs/node/pull/8515', + 'Reviewed-By: Ben Noordhuis ', + 'Reviewed-By: Sakthipriyan Vairamani ', + 'Reviewed-By: James M Snell ' + ], 'c.trailers') + tt.deepEqual(c.trailerFreeBody, [ + '`FChown` and `Chown` test that the `uid` and `gid` parameters', + 'they receive are unsigned integers, but `Stat()` and `FStat()`', + 'would return the corresponding fields of `uv_stat_t` as signed', + 'integers. Applications which pass those these values directly', + 'to `Chown` may fail', + '(e.g. for `nobody` on OS X, who has an `uid` of `-2`, see e.g.', + 'https://github.com/nodejs/node-v0.x-archive/issues/5890).', + '', + 'This patch changes the `Integer::New()` call for `uid` and `gid`', + 'to `Integer::NewFromUnsigned()`.', + '', + 'All other fields are kept as they are, for performance, but', + 'strictly speaking the respective sizes of those', + 'fields aren’t specified, either.' + ], 'c.trailerFreeBody') + + tt.equal(c.revert, false, 'revert') + tt.equal(c.release, false, 'release') + tt.equal(c.working, false, 'working') + tt.end() + }) + + t.end() +}) diff --git a/test/rules/assisted-by-is-trailer.js b/test/rules/assisted-by-is-trailer.js index 01d2a2d..c4dd6b0 100644 --- a/test/rules/assisted-by-is-trailer.js +++ b/test/rules/assisted-by-is-trailer.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/assisted-by-is-trailer.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' test('rule: assisted-by-is-trailer', (t) => { diff --git a/test/rules/co-authored-by-is-trailer.js b/test/rules/co-authored-by-is-trailer.js index 3dc3b8d..c7a9cd9 100644 --- a/test/rules/co-authored-by-is-trailer.js +++ b/test/rules/co-authored-by-is-trailer.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/co-authored-by-is-trailer.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' test('rule: co-authored-by-is-trailer', (t) => { diff --git a/test/rules/fixes-url.js b/test/rules/fixes-url.js index 1ed7744..4f468b6 100644 --- a/test/rules/fixes-url.js +++ b/test/rules/fixes-url.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/fixes-url.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' const INVALID_PRURL = 'Pull request URL must reference a comment or discussion.' diff --git a/test/rules/line-after-title.js b/test/rules/line-after-title.js index a7e7256..ab9fabb 100644 --- a/test/rules/line-after-title.js +++ b/test/rules/line-after-title.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/line-after-title.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' test('rule: line-after-title', (t) => { diff --git a/test/rules/line-length.js b/test/rules/line-length.js index 78e8f33..061741e 100644 --- a/test/rules/line-length.js +++ b/test/rules/line-length.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/line-length.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' test('rule: line-length', (t) => { @@ -31,7 +31,8 @@ ${'aaa'.repeat(30)}` Rule.validate(context, { options: { - length: 72 + length: 72, + trailerLength: 120 } }) }) @@ -59,7 +60,8 @@ ${'aaa'.repeat(30)}` Rule.validate(context, { options: { - length: 72 + length: 72, + trailerLength: 120 } }) tt.end() @@ -93,7 +95,8 @@ That was the original code. Rule.validate(context, { options: { - length: 72 + length: 72, + trailerLength: 120 } }) tt.end() @@ -111,6 +114,8 @@ That was the original code. message: `src: make foo mor foo-ey https://${'very-'.repeat(80)}-long-url.org/ + +Trailer: value ` }, v) @@ -123,13 +128,14 @@ https://${'very-'.repeat(80)}-long-url.org/ Rule.validate(context, { options: { - length: 72 + length: 72, + trailerLength: 120 } }) tt.end() }) - t.test('Co-author lines', (tt) => { + t.test('Co-author trailers', (tt) => { const v = new Validator() const good = new Commit({ @@ -141,6 +147,7 @@ https://${'very-'.repeat(80)}-long-url.org/ }, message: [ 'fixup!: apply case-insensitive suggestion', + '', 'Co-authored-by: Michaël Zasso <37011812+targos@users.noreply.github.com>' ].join('\n') }, v) @@ -154,14 +161,81 @@ https://${'very-'.repeat(80)}-long-url.org/ Rule.validate(good, { options: { - length: 72 + length: 72, + trailerLength: 120 } }) tt.end() }) - t.test('Signed-off-by and Assisted-by lines', (tt) => { + t.test('Multi-line trailers', (tt) => { + const v = new Validator() + + const good = new Commit({ + sha: 'f1496de5a7d5474e39eafaafe6f79befe5883a5b', + author: { + name: 'Jacob Smith', + email: '3012099+JakobJingleheimer@users.noreply.github.com', + date: '2025-12-22T09:40:42Z' + }, + message: [ + 'subsystem: add support for foobar', + '', + 'Lorem-Ipsum: dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna', + ' aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + ' Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint', + ' occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + ].join('\n') + }, v) + const tooLong = ' Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + const bad = new Commit({ + sha: 'f1496de5a7d5474e39eafaafe6f79befe5883a5b', + author: { + name: 'Jacob Smith', + email: '3012099+JakobJingleheimer@users.noreply.github.com', + date: '2025-12-22T09:40:42Z' + }, + message: [ + 'subsystem: add support for foobar', + '', + 'Lorem-Ipsum: dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna', + ' aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + tooLong + ].join('\n') + }, v) + + good.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'line-length', 'id') + tt.equal(opts.string, '', 'string') + tt.equal(opts.level, 'pass', 'level') + } + bad.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'line-length', 'id') + tt.equal(opts.message, 'Trailer should be <= 120 columns.', 'message') + tt.equal(opts.string, tooLong, 'string') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(good, { + options: { + length: 72, + trailerLength: 120 + } + }) + Rule.validate(bad, { + options: { + length: 72, + trailerLength: 120 + } + }) + + tt.end() + }) + + t.test('Signed-off-by and Assisted-by trailers', (tt) => { const v = new Validator() const good = new Commit({ @@ -172,6 +246,8 @@ https://${'very-'.repeat(80)}-long-url.org/ date: '2026-04-10T16:38:01Z' }, message: [ + 'subsystem: foobar', + '', 'Signed-off-by: John Connor <9092381+JConnor1985@users.noreply.github.com>', 'Assisted-by: The Longest-Named Code Agent In The World ' ].join('\n') @@ -186,7 +262,50 @@ https://${'very-'.repeat(80)}-long-url.org/ Rule.validate(good, { options: { - length: 72 + length: 72, + trailerLength: 120 + } + }) + + tt.end() + }) + + t.test('Signed-off-by and Assisted-by non-trailers', (tt) => { + tt.plan(8) + const v = new Validator() + + const context = new Commit({ + sha: '016b3921626b58d9b595c90141e65c6fbe0c78e2', + author: { + name: 'John Connor', + email: '9092381+JConnor1985@users.noreply.github.com', + date: '2026-04-10T16:38:01Z' + }, + message: [ + 'subsystem: foobar', + '', + 'Signed-off-by: John Connor <9092381+JConnor1985@users.noreply.github.com>', + 'Assisted-by: The Longest-Named Code Agent In The World ', + '', + 'Actual-trailer: Value' + ].join('\n') + }, v) + + let called = 0 + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'line-length', 'id') + tt.equal(opts.string, + called++ + ? 'Assisted-by: The Longest-Named Code Agent In The World ' + : 'Signed-off-by: John Connor <9092381+JConnor1985@users.noreply.github.com>', 'string') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context, { + options: { + length: 72, + trailerLength: 120 } }) diff --git a/test/rules/reviewers.js b/test/rules/reviewers.js index d76865a..3639936 100644 --- a/test/rules/reviewers.js +++ b/test/rules/reviewers.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/reviewers.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' const MSG = 'Commit must have at least 1 reviewer.' diff --git a/test/rules/signed-off-by.js b/test/rules/signed-off-by.js index acff632..859d9e5 100644 --- a/test/rules/signed-off-by.js +++ b/test/rules/signed-off-by.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/signed-off-by.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' test('rule: signed-off-by', (t) => { diff --git a/test/rules/subsystem.js b/test/rules/subsystem.js index 5e47c01..ccb9bb4 100644 --- a/test/rules/subsystem.js +++ b/test/rules/subsystem.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/subsystem.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' test('rule: subsystem', (t) => { diff --git a/test/rules/title-format.js b/test/rules/title-format.js index 405f37b..c22384b 100644 --- a/test/rules/title-format.js +++ b/test/rules/title-format.js @@ -1,6 +1,6 @@ import { test } from 'tap' import Rule from '../../lib/rules/title-format.js' -import Commit from 'gitlint-parser-node' +import Commit from '../../lib/gitlint-parser.js' import Validator from '../../index.js' function makeCommit (title) { diff --git a/test/validator.js b/test/validator.js index 27dcf5d..2dee88b 100644 --- a/test/validator.js +++ b/test/validator.js @@ -81,34 +81,6 @@ Date: Thu Mar 3 10:10:46 2016 -0600 Signed-off-by: Wyatt Preul ` -/* eslint-disable */ -const str6 = { - "sha": "c5545f2c63fe30b0cfcdafab18c26df8286881d0", - "url": "https://api.github.com/repos/nodejs/node/git/commits/c5545f2c63fe30b0cfcdafab18c26df8286881d0", - "html_url": "https://github.com/nodejs/node/commit/c5545f2c63fe30b0cfcdafab18c26df8286881d0", - "author": { - "name": "Anna Henningsen", - "email": "anna@addaleax.net", - "date": "2016-09-13T10:57:49Z" - }, - "committer": { - "name": "Anna Henningsen", - "email": "anna@addaleax.net", - "date": "2016-09-19T12:50:57Z" - }, - "tree": { - "sha": "b505c0ffa0555730e9f4cdb391d1ebeb48bb2f59", - "url": "https://api.github.com/repos/nodejs/node/git/trees/b505c0ffa0555730e9f4cdb391d1ebeb48bb2f59" - }, - "message": "fs: fix handling of `uv_stat_t` fields\n\n`FChown` and `Chown` test that the `uid` and `gid` parameters\nthey receive are unsigned integers, but `Stat()` and `FStat()`\nwould return the corresponding fields of `uv_stat_t` as signed\nintegers. Applications which pass those these values directly\nto `Chown` may fail\n(e.g. for `nobody` on OS X, who has an `uid` of `-2`, see e.g.\nhttps://github.com/nodejs/node-v0.x-archive/issues/5890).\n\nThis patch changes the `Integer::New()` call for `uid` and `gid`\nto `Integer::NewFromUnsigned()`.\n\nAll other fields are kept as they are, for performance, but\nstrictly speaking the respective sizes of those\nfields aren’t specified, either.\n\nSigned-off-by: Anna Henningsen \nRef: https://github.com/npm/npm/issues/13918\nPR-URL: https://github.com/nodejs/node/pull/8515\nReviewed-By: Ben Noordhuis \nReviewed-By: Sakthipriyan Vairamani \nReviewed-By: James M Snell \n\nundo accidental change to other fields of uv_fs_stat", - "parents": [ - { - "sha": "4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4", - "url": "https://api.github.com/repos/nodejs/node/git/commits/4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4", - "html_url": "https://github.com/nodejs/node/commit/4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4" - } - ] -} /* eslint-enable */ const str7 = `commit 7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 @@ -289,7 +261,7 @@ test('Validator - real commits', (t) => { const filtered = msgs.filter((item) => { return item.level === 'fail' }) - tt.equal(filtered.length, 0, 'messages.length') + tt.same(filtered, [], 'messages.length') tt.end() }) }) @@ -391,30 +363,6 @@ test('Validator - real commits', (t) => { }) }) - t.test('non empty lines after metadata', (tt) => { - const v = new Validator() - v.lint(str6) - v.on('commit', (data) => { - const c = data.commit.toJSON() - tt.equal(c.sha, 'c5545f2c63fe30b0cfcdafab18c26df8286881d0', 'sha') - tt.equal(c.date, '2016-09-13T10:57:49Z', 'date') - tt.same(c.subsystems, ['fs'], 'subsystems') - tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/8515', 'pr') - tt.equal(c.revert, false, 'revert') - const msgs = data.messages - const filtered = msgs.filter((item) => { - return item.level === 'fail' - }) - tt.equal(filtered.length, 1, 'messages.length') - const item = filtered[0] - tt.equal(item.id, 'metadata-end', 'id') - tt.equal(item.message, 'commit metadata at end of message', 'message') - tt.equal(item.line, 23, 'line') - tt.equal(item.column, 0, 'column') - tt.end() - }) - }) - t.test('trailing punctuation in title line', (tt) => { const v = new Validator({ 'validate-metadata': false From 2481226956a0e9c6fb4175c6407b0c68ee1605a9 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 22 Apr 2026 22:47:27 +0100 Subject: [PATCH 4/6] feat!: drop support for Node.js 20.x and 25.x (#145) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dbe4224..5d4622c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "core-validate-commit": "./bin/cmd.js" }, "engines": { - "node": "^20.19.6 || ^22.21.1 || >=24.12.0" + "node": "^22.21.1 || ^24.12.0 || >=26.0.0" }, "author": "Evan Lucas ", "repository": { From 284263ffafaacb1affde9c5abb42986980d3bc16 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 27 Apr 2026 10:01:35 +0200 Subject: [PATCH 5/6] fix(rules): add `ffi` subsystem (#146) --- lib/rules/subsystem.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rules/subsystem.js b/lib/rules/subsystem.js index 611765d..c32533f 100644 --- a/lib/rules/subsystem.js +++ b/lib/rules/subsystem.js @@ -46,6 +46,7 @@ const validSubsystems = [ 'dns', 'domain', 'events', + 'ffi', 'fs', 'http', 'http2', From 41b8777f780e941b97771e0ee42b51bf853c0edf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:09:45 +0200 Subject: [PATCH 6/6] chore(main): release 6.0.0 (#143) --- CHANGELOG.md | 20 ++++++++++++++++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2125bd..7cb5fe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [6.0.0](https://github.com/nodejs/core-validate-commit/compare/v5.0.1...v6.0.0) (2026-04-27) + + +### ⚠ BREAKING CHANGES + +* drop support for Node.js 20.x and 25.x ([#145](https://github.com/nodejs/core-validate-commit/issues/145)) +* parse trailers using `git` if available, allow longer lines ([#144](https://github.com/nodejs/core-validate-commit/issues/144)) + +### Features + +* add Signed-off-by and Assisted-By rules ([3fce4e6](https://github.com/nodejs/core-validate-commit/commit/3fce4e6f6cc4ebb2970406324b28fa6a03369489)) +* drop support for Node.js 20.x and 25.x ([#145](https://github.com/nodejs/core-validate-commit/issues/145)) ([2481226](https://github.com/nodejs/core-validate-commit/commit/2481226956a0e9c6fb4175c6407b0c68ee1605a9)) +* parse trailers using `git` if available, allow longer lines ([#144](https://github.com/nodejs/core-validate-commit/issues/144)) ([cd1a6a8](https://github.com/nodejs/core-validate-commit/commit/cd1a6a88c8b6d8fcdbc20355456934c2be37264f)) + + +### Bug Fixes + +* **rules:** add `ffi` subsystem ([#146](https://github.com/nodejs/core-validate-commit/issues/146)) ([284263f](https://github.com/nodejs/core-validate-commit/commit/284263ffafaacb1affde9c5abb42986980d3bc16)) +* **rules:** add line-length exemptions for DCO sign-offs ([#142](https://github.com/nodejs/core-validate-commit/issues/142)) ([58c48dc](https://github.com/nodejs/core-validate-commit/commit/58c48dcead19eb9c868a7d67bc42bbc5a418725b)) + ## [5.0.1](https://github.com/nodejs/core-validate-commit/compare/v5.0.0...v5.0.1) (2026-03-18) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 87e41c9..bee2c3a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "core-validate-commit", - "version": "5.0.1", + "version": "6.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "core-validate-commit", - "version": "5.0.1", + "version": "6.0.0", "license": "MIT", "dependencies": { "gitlint-parser-base": "^2.0.0" diff --git a/package.json b/package.json index 5d4622c..9fd3438 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "core-validate-commit", - "version": "5.0.1", + "version": "6.0.0", "description": "Validate the commit message for a particular commit in node core", "main": "index.js", "type": "module",