From 33405f65b82a9eb3ccbd2b8428362ef3b757c2ce Mon Sep 17 00:00:00 2001 From: Anatoli Babenia Date: Wed, 22 Apr 2026 16:30:29 +0800 Subject: [PATCH 1/6] feat!: show input: from a new line This will be less confusing to read, and also easier to test and copy/paste. --- @commitlint/format/src/format.test.ts | 2 +- @commitlint/format/src/format.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/@commitlint/format/src/format.test.ts b/@commitlint/format/src/format.test.ts index 3388e96afb..286549e384 100644 --- a/@commitlint/format/src/format.test.ts +++ b/@commitlint/format/src/format.test.ts @@ -54,7 +54,7 @@ test("returns empty summary with full commit message if verbose", () => { ); expect(actual).toStrictEqual( - "⧗ input: feat(cli): this is a valid header\n\nThis is a valid body\n\nSigned-off-by: tester\n✔ found 0 problems, 0 warnings", + "⧗ --- inptu ---\n feat(cli): this is a valid header\n\nThis is a valid body\n\nSigned-off-by: tester\n✔ found 0 problems, 0 warnings", ); }); diff --git a/@commitlint/format/src/format.ts b/@commitlint/format/src/format.ts index 2496d7272e..340281d520 100644 --- a/@commitlint/format/src/format.ts +++ b/@commitlint/format/src/format.ts @@ -51,7 +51,7 @@ function formatInput( const hasProblems = errors.length > 0 || warnings.length > 0; return options.verbose || hasProblems - ? [`${decoration} input: ${decoratedInput}`] + ? [`${decoration} --- input ---\n${decoratedInput}`] : []; } From d6e3199c50093398ca19d21d999cd860508c27bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20G=2E=20Aragoneses?= Date: Wed, 22 Apr 2026 15:24:03 +0800 Subject: [PATCH 2/6] docs: address Copilot review (new format) Previous commit adtoped a new output format by default, so we need to reflect it in the docs like it's done here. --- @commitlint/config-lerna-scopes/readme.md | 9 ++++++--- @commitlint/config-nx-scopes/readme.md | 9 ++++++--- @commitlint/config-pnpm-scopes/readme.md | 9 ++++++--- @commitlint/config-rush-scopes/readme.md | 9 ++++++--- docs/guides/local-setup.md | 3 ++- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/@commitlint/config-lerna-scopes/readme.md b/@commitlint/config-lerna-scopes/readme.md index 3d9d9ee3ee..e9393b65bf 100644 --- a/@commitlint/config-lerna-scopes/readme.md +++ b/@commitlint/config-lerna-scopes/readme.md @@ -31,16 +31,19 @@ packages └── web ❯ echo "build(api): change something in api's build" | commitlint -⧗ input: build(api): change something in api's build +⧗ --- input --- +build(api): change something in api's build ✔ found 0 problems, 0 warnings ❯ echo "test(foo): this won't pass" | commitlint -⧗ input: test(foo): this won't pass +⧗ --- input --- +test(foo): this won't pass ✖ scope must be one of [api, app, web] [scope-enum] ✖ found 1 problems, 0 warnings ❯ echo "ci: do some general maintenance" | commitlint -⧗ input: ci: do some general maintenance +⧗ --- input --- +ci: do some general maintenance ✔ found 0 problems, 0 warnings ``` diff --git a/@commitlint/config-nx-scopes/readme.md b/@commitlint/config-nx-scopes/readme.md index 2890f85872..15d4a3645c 100644 --- a/@commitlint/config-nx-scopes/readme.md +++ b/@commitlint/config-nx-scopes/readme.md @@ -97,16 +97,19 @@ packages └── web ❯ echo "build(api): change something in api's build" | commitlint -⧗ input: build(api): change something in api's build +⧗ --- input --- +build(api): change something in api's build ✔ found 0 problems, 0 warnings ❯ echo "test(foo): this won't pass" | commitlint -⧗ input: test(foo): this won't pass +⧗ --- input --- +test(foo): this won't pass ✖ scope must be one of [api, app, web] [scope-enum] ✖ found 1 problems, 0 warnings ❯ echo "ci: do some general maintenance" | commitlint -⧗ input: ci: do some general maintenance +⧗ --- input --- +ci: do some general maintenance ✔ found 0 problems, 0 warnings ``` diff --git a/@commitlint/config-pnpm-scopes/readme.md b/@commitlint/config-pnpm-scopes/readme.md index 45d65af9dc..e393c4d1aa 100644 --- a/@commitlint/config-pnpm-scopes/readme.md +++ b/@commitlint/config-pnpm-scopes/readme.md @@ -28,16 +28,19 @@ packages └── web ❯ echo "build(api): change something in api's build" | commitlint -⧗ input: build(api): change something in api's build +⧗ --- input --- +build(api): change something in api's build ✔ found 0 problems, 0 warnings ❯ echo "test(foo): this won't pass" | commitlint -⧗ input: test(foo): this won't pass +⧗ --- input --- +test(foo): this won't pass ✖ scope must be one of [api, app, web] [scope-enum] ✖ found 1 problems, 0 warnings ❯ echo "ci: do some general maintenance" | commitlint -⧗ input: ci: do some general maintenance +⧗ --- input --- +ci: do some general maintenance ✔ found 0 problems, 0 warnings ``` diff --git a/@commitlint/config-rush-scopes/readme.md b/@commitlint/config-rush-scopes/readme.md index 505b681720..a3145e6cae 100644 --- a/@commitlint/config-rush-scopes/readme.md +++ b/@commitlint/config-rush-scopes/readme.md @@ -28,16 +28,19 @@ packages └── web ❯ echo "build(api): change something in api's build" | commitlint -⧗ input: build(api): change something in api's build +⧗ --- input --- +build(api): change something in api's build ✔ found 0 problems, 0 warnings ❯ echo "test(foo): this won't pass" | commitlint -⧗ input: test(foo): this won't pass +⧗ --- input --- +test(foo): this won't pass ✖ scope must be one of [api, app, web] [scope-enum] ✖ found 1 problems, 0 warnings ❯ echo "ci: do some general maintenance" | commitlint -⧗ input: ci: do some general maintenance +⧗ --- input --- +ci: do some general maintenance ✔ found 0 problems, 0 warnings ``` diff --git a/docs/guides/local-setup.md b/docs/guides/local-setup.md index 8da8d26353..f5bb2672da 100644 --- a/docs/guides/local-setup.md +++ b/docs/guides/local-setup.md @@ -271,7 +271,8 @@ You can test the hook by simply committing. You should see something like this i git commit -m "foo: this will fail" # husky > commit-msg No staged files match any of provided globs. -⧗ input: foo: this will fail +⧗ --- input --- +foo: this will fail ✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum] ✖ found 1 problems, 0 warnings From 1518484075e6e2443e24716f38618fae28e3fe77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20G=2E=20Aragoneses?= Date: Wed, 22 Apr 2026 16:32:21 +0800 Subject: [PATCH 3/6] test: new multi-line input error case As instructed by GitHub Copilot, we add a new testcase. --- @commitlint/format/src/format.test.ts | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/@commitlint/format/src/format.test.ts b/@commitlint/format/src/format.test.ts index 286549e384..9e9ebd8c99 100644 --- a/@commitlint/format/src/format.test.ts +++ b/@commitlint/format/src/format.test.ts @@ -58,6 +58,34 @@ test("returns empty summary with full commit message if verbose", () => { ); }); +test('returns input banner with errors and multi-line input', () => { + const actual = format( + { + results: [ + { + errors: [ + { + level: 2, + name: 'type-enum', + message: + 'type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]', + }, + ], + warnings: [], + input: 'foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester', + }, + ], + }, + { + color: false, + } + ); + + expect(actual).toStrictEqual( + '⧗ --- input ---\nfoo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester\n✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]\n\n✖ found 1 problems, 0 warnings' + ); +}); + test("returns a correct summary of empty .errors and .warnings", () => { const actualError = format({ results: [ From fd1a7b2f619512ec4c7899f44019d0618fea34f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20G=2E=20Aragoneses?= Date: Wed, 22 Apr 2026 16:48:58 +0800 Subject: [PATCH 4/6] feat(format): add --legacy-output flag To restore old single-line input output. --- @commitlint/cli/src/cli.ts | 5 +++ @commitlint/cli/src/types.ts | 1 + @commitlint/format/src/format.test.ts | 55 ++++++++++++++++++++++++++- @commitlint/format/src/format.ts | 15 ++++++-- @commitlint/types/src/format.ts | 1 + 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/@commitlint/cli/src/cli.ts b/@commitlint/cli/src/cli.ts index dabedae66f..ec687c7755 100644 --- a/@commitlint/cli/src/cli.ts +++ b/@commitlint/cli/src/cli.ts @@ -135,6 +135,10 @@ const cli = yargs(process.argv.slice(2)) type: "boolean", description: "enable verbose output for reports without problems", }, + "legacy-output": { + description: "use the legacy input output format (single-line 'input: ...')", + type: "boolean", + }, strict: { alias: "s", type: "boolean", @@ -398,6 +402,7 @@ async function main(args: MainArgs): Promise { color: flags.color, verbose: flags.verbose, helpUrl, + legacyOutput: flags["legacy-output"], }); if (!flags.quiet && output !== "") { diff --git a/@commitlint/cli/src/types.ts b/@commitlint/cli/src/types.ts index cbc9a8956a..818772ab9e 100644 --- a/@commitlint/cli/src/types.ts +++ b/@commitlint/cli/src/types.ts @@ -20,6 +20,7 @@ export interface CliFlags { /** @type {'' | 'text' | 'json'} */ "print-config"?: string; strict?: boolean; + "legacy-output"?: boolean; _: (string | number)[]; $0: string; } diff --git a/@commitlint/format/src/format.test.ts b/@commitlint/format/src/format.test.ts index 9e9ebd8c99..2de84b1ce5 100644 --- a/@commitlint/format/src/format.test.ts +++ b/@commitlint/format/src/format.test.ts @@ -54,7 +54,31 @@ test("returns empty summary with full commit message if verbose", () => { ); expect(actual).toStrictEqual( - "⧗ --- inptu ---\n feat(cli): this is a valid header\n\nThis is a valid body\n\nSigned-off-by: tester\n✔ found 0 problems, 0 warnings", + "⧗ --- input ---\nfeat(cli): this is a valid header\n\nThis is a valid body\n\nSigned-off-by: tester\n✔ found 0 problems, 0 warnings", + ); +}); + +test("returns legacy output with full commit message if verbose and legacyOutput", () => { + const actual = format( + { + results: [ + { + errors: [], + warnings: [], + input: + "feat(cli): this is a valid header\n\nThis is a valid body\n\nSigned-off-by: tester", + }, + ], + }, + { + verbose: true, + color: false, + legacyOutput: true, + }, + ); + + expect(actual).toStrictEqual( + "⧗ input: feat(cli): this is a valid header\n\nThis is a valid body\n\nSigned-off-by: tester\n✔ found 0 problems, 0 warnings", ); }); @@ -86,6 +110,35 @@ test('returns input banner with errors and multi-line input', () => { ); }); +test('returns legacy input banner with errors and multi-line input', () => { + const actual = format( + { + results: [ + { + errors: [ + { + level: 2, + name: 'type-enum', + message: + 'type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]', + }, + ], + warnings: [], + input: 'foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester', + }, + ], + }, + { + color: false, + legacyOutput: true, + } + ); + + expect(actual).toStrictEqual( + '⧗ input: foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester\n✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]\n\n✖ found 1 problems, 0 warnings' + ); +}); + test("returns a correct summary of empty .errors and .warnings", () => { const actualError = format({ results: [ diff --git a/@commitlint/format/src/format.ts b/@commitlint/format/src/format.ts index 340281d520..998cfc9b2b 100644 --- a/@commitlint/format/src/format.ts +++ b/@commitlint/format/src/format.ts @@ -37,7 +37,7 @@ function formatInput( result: FormattableResult & WithInput, options: FormatOptions = {}, ): string[] { - const { color: enabled = true } = options; + const { color: enabled = true, legacyOutput = false } = options; const { errors = [], warnings = [], input = "" } = result; if (!input) { @@ -50,9 +50,16 @@ function formatInput( const decoratedInput = enabled ? pc.bold(input) : input; const hasProblems = errors.length > 0 || warnings.length > 0; - return options.verbose || hasProblems - ? [`${decoration} --- input ---\n${decoratedInput}`] - : []; + if (!(options.verbose || hasProblems)) { + return []; + } + + if (legacyOutput) { + // legacy: single line with 'input: ' prefix, no extra newlines or dashes + return [`${decoration} input: ${decoratedInput}`]; + } + + return [`${decoration} --- input ---\n${decoratedInput}`]; } export function formatResult( diff --git a/@commitlint/types/src/format.ts b/@commitlint/types/src/format.ts index ca9f87495a..31742e649e 100644 --- a/@commitlint/types/src/format.ts +++ b/@commitlint/types/src/format.ts @@ -41,4 +41,5 @@ export interface FormatOptions { colors?: readonly [PicocolorsColor, PicocolorsColor, PicocolorsColor]; verbose?: boolean; helpUrl?: string; + legacyOutput?: boolean; } From b68535554ce99c3419bed7efa00735b1e8ee4a55 Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Thu, 23 Apr 2026 15:21:57 +0200 Subject: [PATCH 5/6] chore: fix tests --- @commitlint/cli/src/cli.test.ts | 1 + @commitlint/cli/src/cli.ts | 3 ++- @commitlint/format/src/format.test.ts | 26 ++++++++++++++------------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/@commitlint/cli/src/cli.test.ts b/@commitlint/cli/src/cli.test.ts index 7182a12761..f3107a6d5e 100644 --- a/@commitlint/cli/src/cli.test.ts +++ b/@commitlint/cli/src/cli.test.ts @@ -644,6 +644,7 @@ test("should print help", async () => { -q, --quiet toggle console output [boolean] [default: false] -t, --to upper end of the commit range to lint; applies if edit=false [string] -V, --verbose enable verbose output for reports without problems [boolean] + --legacy-output use the legacy input output format (single-line 'input: ...') [boolean] -s, --strict enable strict mode; result code 2 for warnings, 3 for errors [boolean] --options path to a JSON file or Common.js module containing CLI options -v, --version display version information [boolean] diff --git a/@commitlint/cli/src/cli.ts b/@commitlint/cli/src/cli.ts index ec687c7755..89bcdbc041 100644 --- a/@commitlint/cli/src/cli.ts +++ b/@commitlint/cli/src/cli.ts @@ -136,7 +136,8 @@ const cli = yargs(process.argv.slice(2)) description: "enable verbose output for reports without problems", }, "legacy-output": { - description: "use the legacy input output format (single-line 'input: ...')", + description: + "use the legacy input output format (single-line 'input: ...')", type: "boolean", }, strict: { diff --git a/@commitlint/format/src/format.test.ts b/@commitlint/format/src/format.test.ts index 2de84b1ce5..b856853574 100644 --- a/@commitlint/format/src/format.test.ts +++ b/@commitlint/format/src/format.test.ts @@ -82,7 +82,7 @@ test("returns legacy output with full commit message if verbose and legacyOutput ); }); -test('returns input banner with errors and multi-line input', () => { +test("returns input banner with errors and multi-line input", () => { const actual = format( { results: [ @@ -90,27 +90,28 @@ test('returns input banner with errors and multi-line input', () => { errors: [ { level: 2, - name: 'type-enum', + name: "type-enum", message: - 'type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]', + "type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]", }, ], warnings: [], - input: 'foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester', + input: + "foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester", }, ], }, { color: false, - } + }, ); expect(actual).toStrictEqual( - '⧗ --- input ---\nfoo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester\n✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]\n\n✖ found 1 problems, 0 warnings' + "⧗ --- input ---\nfoo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester\n✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]\n\n✖ found 1 problems, 0 warnings\n", ); }); -test('returns legacy input banner with errors and multi-line input', () => { +test("returns legacy input banner with errors and multi-line input", () => { const actual = format( { results: [ @@ -118,24 +119,25 @@ test('returns legacy input banner with errors and multi-line input', () => { errors: [ { level: 2, - name: 'type-enum', + name: "type-enum", message: - 'type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]', + "type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]", }, ], warnings: [], - input: 'foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester', + input: + "foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester", }, ], }, { color: false, legacyOutput: true, - } + }, ); expect(actual).toStrictEqual( - '⧗ input: foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester\n✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]\n\n✖ found 1 problems, 0 warnings' + "⧗ input: foo: this is an invalid header\n\nThis is a body\n\nSigned-off-by: tester\n✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]\n\n✖ found 1 problems, 0 warnings\n", ); }); From 3f1c644f111e368706c09b48012b2870303ebe0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20G=2E=20Aragoneses?= Date: Sun, 26 Apr 2026 11:26:26 +0800 Subject: [PATCH 6/6] docs: warn about BREAKING CHANGES --- docs/guides/local-setup.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/guides/local-setup.md b/docs/guides/local-setup.md index f5bb2672da..b74bb32521 100644 --- a/docs/guides/local-setup.md +++ b/docs/guides/local-setup.md @@ -291,4 +291,7 @@ No staged files match any of provided globs. # husky > commit-msg ``` +Since [v22.0.0](https://github.com/conventional-changelog/commitlint/releases/tag/v22.0.0) `commitlint` will output the commit message after a new line (EOL) instead of after a colon.\ +(You can use the `--legacy-output` flag to get the previous output format used in older versions) + Local linting is fine for fast feedback but can easily be tinkered with. To ensure all commits are linted you'll want to check commits on an automated CI Server too. Learn how to in the [CI Setup guide](/guides/ci-setup).