Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions @commitlint/cli/fixtures/no-config/helpurl-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// A shareable config that contributes no rules — used to verify that
// --default-config keeps user-supplied --extends configs when it falls back.
module.exports = {
helpUrl: "https://example.com/no-rules",
};
4 changes: 4 additions & 0 deletions @commitlint/cli/fixtures/no-config/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "no-config-fixture",
"private": true
}
1 change: 1 addition & 0 deletions @commitlint/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"pkg": "pkg-check"
},
"dependencies": {
"@commitlint/config-conventional": "workspace:^",
"@commitlint/format": "workspace:^",
"@commitlint/lint": "workspace:^",
"@commitlint/load": "workspace:^",
Expand Down
114 changes: 92 additions & 22 deletions @commitlint/cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,66 @@ test("should succeed for input from stdin with rules", async () => {
expect(result.exitCode).toBe(ExitCode.CommitlintDefault);
});

test("should fail without config file and without --default-config flag", async () => {
const cwd = await gitBootstrap("fixtures/no-config");
const result = cli([], { cwd })("feat: this should not work");
const output = await result;
expect(output.stdout.trim()).toContain("Please add rules");
expect(result.exitCode).toBe(ExitCode.CommitlintInvalidArgument);
});

test("should succeed for conventional input from stdin without config file with --default-config flag", async () => {
const cwd = await gitBootstrap("fixtures/no-config");
const result = cli(["--default-config"], { cwd })("feat: this should work");
const output = await result;
expect(output.stderr).toEqual("");
expect(result.exitCode).toBe(ExitCode.CommitlintDefault);
});

test("should fail for non-conventional input from stdin without config file with --default-config flag", async () => {
const cwd = await gitBootstrap("fixtures/no-config");
const result = cli(["--default-config"], { cwd })("this is not a conventional commit");
const output = await result;
expect(output.stdout.trim()).toContain("type may not be empty");
expect(output.stdout.trim()).toContain("subject may not be empty");
expect(result.exitCode).toBe(ExitCode.CommitlintErrorDefault);
});

test("should prefer config file over --default-config flag", async () => {
const cwd = await gitBootstrap("fixtures/default");
// "type: bar" passes the fixture config (type-enum never [foo]) but would
// fail @commitlint/config-conventional (type "type" is not allowed there)
const result = cli(["--default-config"], { cwd })("type: bar");
await result;
expect(result.exitCode).toBe(ExitCode.CommitlintDefault);
});

test("should fall back to default config with --default-config flag for a config file without rules", async () => {
const cwd = await gitBootstrap("fixtures/empty");
const result = cli(["--default-config"], { cwd })("feat: this should work");
await result;
expect(result.exitCode).toBe(ExitCode.CommitlintDefault);
});

test("should keep --extends configs when the --default-config fallback applies", async () => {
const cwd = await gitBootstrap("fixtures/no-config");
// ./helpurl-only contributes a helpUrl but no rules, so the fallback
// still applies and must not drop the user-supplied extends
const result = cli(["--default-config", "--extends", "./helpurl-only"], { cwd })("foo bar");
const output = await result;
expect(output.stdout.trim()).toContain("type may not be empty");
expect(output.stdout.trim()).toContain("https://example.com/no-rules");
expect(result.exitCode).toBe(ExitCode.CommitlintErrorDefault);
});

test("should point to the --default-config flag when no rules are found", async () => {
const cwd = await gitBootstrap("fixtures/empty");
const result = cli([], { cwd })("foo: bar");
const output = await result;
expect(output.stdout.trim()).toContain("--default-config");
expect(result.exitCode).toBe(ExitCode.CommitlintInvalidArgument);
});

test("should fail for input from stdin with rule from rc", async () => {
const cwd = await gitBootstrap("fixtures/simple");
const result = cli([], { cwd })("foo: bar");
Expand Down Expand Up @@ -609,28 +669,29 @@ test("should print help", async () => {
[input] reads from stdin if --edit, --env, --from and --to are omitted

Options:
-c, --color toggle colored output [boolean] [default: true]
-g, --config path to the config file; result code 9 if config is missing [string]
--print-config print resolved config [string] [choices: "", "text", "json"]
-d, --cwd directory to execute in [string] [default: (Working Directory)]
-e, --edit read last commit message from the specified file or fallbacks to ./.git/COMMIT_EDITMSG [string]
-E, --env check message in the file at path given by environment variable value [string]
-x, --extends array of shareable configurations to extend [array]
-H, --help-url help url in error message [string]
-f, --from lower end of the commit range to lint; applies if edit=false [string]
--from-last-tag uses the last tag as the lower end of the commit range to lint; applies if edit=false and from is not set [boolean]
--git-log-args additional git log arguments as space separated string, example '--first-parent --cherry-pick' [string]
-l, --last just analyze the last commit; applies if edit=false [boolean]
-o, --format output format of the results [string]
-p, --parser-preset configuration preset to use for conventional-commits-parser [string]
-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]
-h, --help Show help [boolean]"
-c, --color toggle colored output [boolean] [default: true]
-g, --config path to the config file; result code 9 if config is missing [string]
--default-config use built-in default config (@commitlint/config-conventional) when no rules are found [boolean]
--print-config print resolved config [string] [choices: "", "text", "json"]
-d, --cwd directory to execute in [string] [default: (Working Directory)]
-e, --edit read last commit message from the specified file or fallbacks to ./.git/COMMIT_EDITMSG [string]
-E, --env check message in the file at path given by environment variable value [string]
-x, --extends array of shareable configurations to extend [array]
-H, --help-url help url in error message [string]
-f, --from lower end of the commit range to lint; applies if edit=false [string]
--from-last-tag uses the last tag as the lower end of the commit range to lint; applies if edit=false and from is not set [boolean]
--git-log-args additional git log arguments as space separated string, example '--first-parent --cherry-pick' [string]
-l, --last just analyze the last commit; applies if edit=false [boolean]
-o, --format output format of the results [string]
-p, --parser-preset configuration preset to use for conventional-commits-parser [string]
-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]
-h, --help Show help [boolean]"
`);
});

Expand Down Expand Up @@ -698,6 +759,15 @@ describe("should print config", () => {
`"{"extends":[],"formatter":"@commitlint/format","plugins":{},"rules":{"type-enum":[2,"never",["foo"]]},"helpUrl":"https://github.com/conventional-changelog/commitlint/#what-is-commitlint","prompt":{}}"`,
);
});

test("should print default config with --default-config flag when no config file is found", async () => {
const cwd = await gitBootstrap("fixtures/no-config");
const result = cli(["--print-config=json", "--no-color", "--default-config"], { cwd })();
const output = await result;
const printed = JSON.parse(output.stdout.trim());
expect(printed.rules).toHaveProperty("type-enum");
expect(printed.rules["type-enum"][2]).toContain("feat");
});
});

async function writePkg(payload: unknown, options: TestOptions) {
Expand Down
59 changes: 47 additions & 12 deletions @commitlint/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const pkg: typeof import("../package.json") = require("../package.json");

const gitDefaultCommentChar = "#";

const defaultConfig = "@commitlint/config-conventional";

const cli = yargs(process.argv.slice(2))
.options({
color: {
Expand All @@ -49,6 +51,11 @@ const cli = yargs(process.argv.slice(2))
description: "path to the config file; result code 9 if config is missing",
type: "string",
},
"default-config": {
description:
"use built-in default config (@commitlint/config-conventional) when no rules are found",
type: "boolean",
},
"print-config": {
choices: ["", "text", "json"],
description: "print resolved config",
Expand Down Expand Up @@ -221,10 +228,7 @@ async function main(args: MainArgs): Promise<void> {
}

if (typeof options["print-config"] === "string") {
const loaded = await load(getSeed(flags), {
cwd: flags.cwd,
file: flags.config,
});
const loaded = await loadConfig(flags);

switch (options["print-config"]) {
case "json":
Expand Down Expand Up @@ -291,10 +295,7 @@ async function main(args: MainArgs): Promise<void> {
throw err;
}

const loaded = await load(getSeed(flags), {
cwd: flags.cwd,
file: flags.config,
});
const loaded = await loadConfig(flags);
const parserOpts = selectParserOpts(loaded.parserPreset);
const opts: LintOptions & { parserOpts: Options } = {
parserOpts: {},
Expand Down Expand Up @@ -351,6 +352,7 @@ async function main(args: MainArgs): Promise<void> {
"Please add rules to your `commitlint.config.js`",
" - Getting started guide: https://commitlint.js.org/guides/getting-started",
" - Example config: https://github.com/conventional-changelog/commitlint/blob/master/%40commitlint/config-conventional/src/index.ts",
" - Or run commitlint with the built-in default config: commitlint --default-config",
].join("\n"),
},
],
Expand Down Expand Up @@ -484,6 +486,42 @@ function getEditValue(flags: CliFlags) {
return edit;
}

async function loadConfig(flags: CliFlags): Promise<QualifiedConfig> {
const loaded = await load(getSeed(flags), {
cwd: flags.cwd,
file: flags.config,
});

// `--default-config` falls back to the built-in default config when
// config resolution yields no rules (e.g. no config file was found).
// The default config is prepended so user-supplied --extends configs
// keep precedence over it.
if (flags["default-config"] && Object.keys(loaded.rules).length === 0) {
const extendsWithDefault = [resolveDefaultConfig(flags), ...(flags.extends || [])];
return load(getSeed({ ...flags, extends: extendsWithDefault }), {
cwd: flags.cwd,
file: flags.config,
});
}
Comment on lines +495 to +505

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. --default-config gates default fallback 📎 Requirement gap ≡ Correctness

The CLI only loads the built-in default config when --default-config is explicitly provided, so
running commitlint with no config file still resolves to empty rules and errors. This violates the
requirement to automatically use the built-in default configuration when no commitlint config file
is found.
Agent Prompt
## Issue description
The built-in default config is only applied when the user passes `--default-config`, but the compliance requirement expects commitlint to automatically fall back to the built-in default configuration when no commitlint config file is present.

## Issue Context
`loadConfig()` currently checks `flags["default-config"]` before prepending `@commitlint/config-conventional`, so config-less invocations still end up with `loaded.rules` empty and hit the empty-rules error path.

## Fix Focus Areas
- @commitlint/cli/src/cli.ts[495-505]
- @commitlint/cli/src/cli.test.ts[217-231]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


return loaded;
}

function resolveDefaultConfig(flags: CliFlags): string {
// Resolve from the cli package itself first so the fallback works without
// @commitlint/config-conventional being installed in the linted project
// (e.g. `npx commitlint --default-config` or strictly isolated node_modules).
return resolveModulePath(defaultConfig, flags) || defaultConfig;
}

function resolveModulePath(moduleName: string, flags: CliFlags): string | undefined {
return (
resolveFromSilent(moduleName, __dirname) ||
resolveFromSilent(moduleName, flags.cwd) ||
resolveGlobalSilent(moduleName)
);
}

function getSeed(flags: CliFlags): UserConfig {
const n = (flags.extends || []).filter((i): i is string => typeof i === "string");
return n.length > 0
Expand All @@ -505,10 +543,7 @@ function selectParserOpts(parserPreset: ParserPreset | undefined) {

function loadFormatter(config: QualifiedConfig, flags: CliFlags): Promise<Formatter> {
const moduleName = flags.format || config.formatter || "@commitlint/format";
const modulePath =
resolveFromSilent(moduleName, __dirname) ||
resolveFromSilent(moduleName, flags.cwd) ||
resolveGlobalSilent(moduleName);
const modulePath = resolveModulePath(moduleName, flags);

if (modulePath) {
return dynamicImport<Formatter>(modulePath);
Expand Down
1 change: 1 addition & 0 deletions @commitlint/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface CliFlags {
color: boolean;
config?: string;
cwd: string;
"default-config"?: boolean;
edit?: string | boolean;
env?: string;
extends?: (string | number)[];
Expand Down
9 changes: 9 additions & 0 deletions docs/guides/local-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,15 @@ deno task --eval commitlint --from HEAD~1 --to HEAD --verbose

This will check your last commit and return an error if invalid or a positive output if valid.

::: tip
To try commitlint without creating a configuration file first, pipe a message to it and use the built-in default config ([@commitlint/config-conventional](https://www.npmjs.com/package/@commitlint/config-conventional)):

```sh
echo "feat: add new feature" | npx commitlint --default-config
```

:::

### Test the hook

You can test the hook by simply committing. You should see something like this if everything works.
Expand Down
86 changes: 55 additions & 31 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,63 @@
[input] reads from stdin if --edit, --env, --from and --to are omitted

Options:
-c, --color toggle colored output [boolean] [default: true]
-g, --config path to the config file; result code 9 if config is
missing [string]
--print-config print resolved config
-c, --color toggle colored output [boolean] [default: true]
-g, --config path to the config file; result code 9 if config is
missing [string]
--default-config use built-in default config
(@commitlint/config-conventional) when no rules are
found [boolean]
--print-config print resolved config
[string] [choices: "", "text", "json"]
-d, --cwd directory to execute in
-d, --cwd directory to execute in
[string] [default: (Working Directory)]
-e, --edit read last commit message from the specified file or
fallbacks to ./.git/COMMIT_EDITMSG [string]
-E, --env check message in the file at path given by environment
variable value [string]
-x, --extends array of shareable configurations to extend [array]
-H, --help-url help url in error message [string]
-f, --from lower end of the commit range to lint; applies if
edit=false [string]
--from-last-tag uses the last tag as the lower end of the commit range to
lint; applies if edit=false and from is not set [boolean]
--git-log-args additional git log arguments as space separated string,
example '--first-parent --cherry-pick' [string]
-l, --last just analyze the last commit; applies if edit=false
-e, --edit read last commit message from the specified file or
fallbacks to ./.git/COMMIT_EDITMSG [string]
-E, --env check message in the file at path given by environment
variable value [string]
-x, --extends array of shareable configurations to extend [array]
-H, --help-url help url in error message [string]
-f, --from lower end of the commit range to lint; applies if
edit=false [string]
--from-last-tag uses the last tag as the lower end of the commit range
to lint; applies if edit=false and from is not set
[boolean]
-o, --format output format of the results [string]
-p, --parser-preset configuration preset to use for
conventional-commits-parser [string]
-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
--git-log-args additional git log arguments as space separated string,
example '--first-parent --cherry-pick' [string]
-l, --last just analyze the last commit; applies if edit=false
[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]
-h, --help Show help [boolean]
-o, --format output format of the results [string]
-p, --parser-preset configuration preset to use for
conventional-commits-parser [string]
-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]
-h, --help Show help [boolean]
```

## Lint without a config file

By default commitlint requires a configuration with rules to run — without one
it exits with an error (result code 9). Pass `--default-config` to fall back to
the built-in default config
([@commitlint/config-conventional](https://www.npmjs.com/package/@commitlint/config-conventional))
when no rules are found, so commitlint can run without any setup:

```sh
echo "feat: add new feature" | npx commitlint --default-config
```

This is useful for one-off checks or server-side hooks (e.g. `pre-receive`)
where creating a `commitlint.config.js` and installing a shareable config is
not practical. A configuration file with rules always takes precedence over
`--default-config`, and configs passed via `--extends` are kept and override
the default config when the fallback applies.
Loading
Loading