diff --git a/AGENTS.md b/AGENTS.md index 2e08e188a..2e310e581 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,63 +2,29 @@ ## Purpose -This file provides **project-specific guidance for AI agents** (and other automated tools) working on the `commitizen` repository. -Follow these instructions in addition to any higher-level system or tool rules. +This file is the auto-loaded entry point for AI agents working on the `commitizen` repository. It holds the rules an agent needs in **every** session. Deeper guidance lives in: -## Project Overview +- [Contributing](docs/contributing/contributing.md) — setup, dev workflow, PR lifecycle. +- [Contributing TL;DR](docs/contributing/contributing_tldr.md) — poe command cheat sheet. +- [Pull Request Guidelines](docs/contributing/pull_request.md) — PR etiquette and AI-assisted policy. +- [Architecture Overview](docs/contributing/architecture.md) — codebase topology and extension points. +- [For AI Agents](docs/contributing/agents/index.md) — agent-shaped recipes, validation map, playbooks. -- **Project**: `commitizen` - a tool to help enforce and automate conventional commits, version bumps, and changelog generation. -- **Primary language**: Python (library + CLI). -- **Cross-platform**: Tests run on Linux, macOS, and Windows. Avoid POSIX-only assumptions in code (paths, subprocesses, line endings). -- **Key entrypoints**: - - `commitizen/cli.py` - main CLI implementation. - - `commitizen/commands/` - subcommands such as `bump`, `commit`, `changelog`, `check`, etc. - - `commitizen/config/` - configuration discovery and loading. - - `commitizen/providers/` - version providers (e.g., `pep621`, `poetry`, `npm`, `uv`). -- **Config sources**: `pyproject.toml` (project config, poe tasks, ruff, mypy), `.pre-commit-config.yaml` (hooks), `.github/workflows/` (CI). - -## General Expectations - -- **Preserve public behavior and CLI UX** — no breaking changes to APIs, CLI flags, or exit codes unless explicitly requested. -- **Update or add tests/docs** when you change user-facing behavior. -- **Commit messages** must follow [Conventional Commits](https://www.conventionalcommits.org/) (enforced by commitizen itself). -- **Pull requests** must follow the [Pull Request Guidelines](docs/contributing/pull_request.md) and the template in `.github/pull_request_template.md`. - -## Setup and Validation - -> Full contributor guidelines (prerequisites, workflow, PR process): [`docs/contributing/contributing.md`](docs/contributing/contributing.md). - -### Bootstrap - -```bash -uv sync --frozen --group base --group test --group linters -uv run poe setup-pre-commit # install git hooks (uses prek, a pre-commit runner) -``` - -### Local commands - -- **Format**: `uv run poe format` (runs `ruff check --fix` then `ruff format`) -- **Lint**: `uv run poe lint` (runs `ruff check` then `mypy`) -- **Test**: `uv run poe test` (runs `pytest -n auto`) -- **CI-equivalent**: `uv run poe ci` (commit check + pre-commit hooks via `prek` + test with coverage) -- **Full local check**: `uv run poe all` (format + lint + check-commit + coverage) +Follow these instructions in addition to any higher-level tool or system rules. -Always run at least `uv run ruff check --fix . && uv run ruff format .` before pushing. CI will fail if the formatter modifies any files. +## Project at a glance -### CI pipeline - -- CI runs `poe ci` on a matrix of Python 3.10–3.14 × ubuntu/macos/windows. -- Pre-commit hooks are defined in `.pre-commit-config.yaml` and run via [`prek`](https://github.com/j178/prek) (a `pre-commit` compatible runner). -- The matrix is **fail-fast**: inspect the earliest failing job that completed; others are cancelled. - -### Common CI failure patterns - -- **"Format Python code...Failed"**: Run `uv run poe format` and commit the result. -- **mypy `[arg-type]` on TypedDict**: Dynamically-constructed dicts (e.g., from `pytest.mark.parametrize`) passed to TypedDict-typed params need `# type: ignore[arg-type]`. -- **"pathspec 'vX.Y.Z' did not match"**: `.pre-commit-config.yaml` pins a tag of this repo. Rebase onto master to pick up the tag. -- **`VersionProtocol` + `issubclass`**: This Protocol has non-method members (properties), so `issubclass()` raises `TypeError`. Use `hasattr` checks for runtime validation. +- **Project**: `commitizen` — Python CLI for enforcing Conventional Commits, automating version bumps, and generating changelogs. +- **Library + CLI**: code is reachable both via `cz` and `import commitizen`. +- **Cross-platform**: tests run on Linux/macOS/Windows × Python 3.10–3.14. Avoid POSIX-only assumptions (paths, subprocesses, line endings). +- **Key entrypoints**: + - `commitizen/cli.py` — CLI definition (decli + argparse). + - `commitizen/commands/` — one module per `cz` subcommand. + - `commitizen/config/` — configuration discovery and parsing. + - `commitizen/providers/` — version providers. + - `commitizen/changelog_formats/` — changelog file formats. -## What to Read Before Changing +## Read before changing | Changing... | Read first | |---|---| @@ -66,18 +32,31 @@ Always run at least `uv run ruff check --fix . && uv run ruff format .` before p | Bump logic | `commitizen/bump.py`, `commitizen/commands/bump.py`, `docs/commands/bump.md` | | Changelog generation | `commitizen/changelog.py`, `commitizen/changelog_formats/`, `docs/commands/changelog.md` | | Version schemes | `commitizen/version_schemes.py`, `tests/test_version_schemes.py` | -| Version providers | `commitizen/providers/`, `tests/test_providers.py`, `docs/config/version_provider.md` | +| Version providers | `commitizen/providers/`, `tests/providers/`, `docs/config/version_provider.md` | | Config resolution | `commitizen/config/`, `tests/test_conf.py`, `docs/config/` | | Tag handling | `commitizen/tags.py`, `tests/test_tags.py` | | Pre-commit / CI | `.pre-commit-config.yaml`, `.github/workflows/`, `pyproject.toml` (poe tasks) | -## Coding Guidelines +For recurring task types (add a provider, deprecate an API, regenerate snapshots, ...), use the matching playbook in [For AI Agents § Playbooks](docs/contributing/agents/index.md#playbooks) instead of reinventing the workflow. + +## Do not touch + +These files are regenerated by Commitizen-specific tooling, so hand-edits get reverted on the next release or doc rebuild: + +- `CHANGELOG.md` — produced by `cz changelog`. Hand-edits will be overwritten on the next release. +- `commitizen/__version__.py` — bumped by `cz bump` via the configured version provider. +- `.pre-commit-config.yaml:rev:` lines under the `Commitizen` repo — bumped by `cz bump` (`version_files` in `pyproject.toml`). +- `docs/images/cli_help/*.svg` and `docs/images/cli_interactive/*.gif` — regenerated by `uv run poe doc:screenshots`. See the [update-snapshots playbook](docs/contributing/agents/playbooks/update-snapshots.md). +- `tests/**/*` snapshot files used by `pytest-regressions` — regenerated via `uv run poe test:regen`. + +## Mandatory PR reminders + +These are easy to miss when working from an agent and are required by the PR template: -- **Types**: Preserve or improve existing type hints. -- **Errors**: Prefer `commitizen/exceptions.py` error types; keep messages clear for CLI users. -- **Output**: Use `commitizen/out.py`; do not add noisy logging. +1. **Complete the AI disclosure**. Check "Was generative AI tooling used to co-author this PR?" and fill in the `Generated-by:` trailer with the tool name. Details: [Pull Request Guidelines § AI-Assisted Contributions](docs/contributing/pull_request.md#ai-assisted-contributions). +2. **Run `uv run poe all` before pushing**. This is the command named in the PR template; it auto-formats then runs the same lint/check/test pipeline as CI. To mirror CI exactly afterwards, run `uv run poe ci` (uses `prek`, does not auto-format). +3. **Fill in "Steps to Test This Pull Request"** with the exact commands you ran locally — the maintainers re-run them. -## When Unsure +## When unsure -- Prefer **reading tests and documentation first** to understand the expected behavior. -- When behavior is ambiguous, **assume backward compatibility** with current tests and docs is required. +When behavior is ambiguous, assume **backward compatibility with current tests and docs** is required. Add a deprecation window instead of breaking it; see the [deprecate-public-api playbook](docs/contributing/agents/playbooks/deprecate-public-api.md). diff --git a/docs/contributing/agents/index.md b/docs/contributing/agents/index.md new file mode 100644 index 000000000..277631c5b --- /dev/null +++ b/docs/contributing/agents/index.md @@ -0,0 +1,38 @@ +# For AI Agents + +These pages are written for AI agents contributing to Commitizen. Human contributors may also find them useful as a quick reference. They **complement** the existing human-facing contributor docs rather than replace them — anything covered by the human docs is linked, not restated. + +> If you are an AI agent looking to **use** Commitizen as a tool (validate +> commit messages, bump versions in a downstream project), see the skill +> definition at `.agents/skills/commitizen/SKILL.md` in the repo root. +> The pages here are for working **on** Commitizen itself. + +## When to read what + +| You want to... | Read | +|---|---| +| Set up a local dev environment | [Contributing](../contributing.md#prerequisites-setup) | +| Look up a poe command | [Contributing TL;DR](../contributing_tldr.md#command-cheat-sheet) | +| Understand the codebase layout and extension points | [Architecture Overview](../architecture.md) | +| Open a pull request | [Pull Request Guidelines](../pull_request.md) and the [PR template](https://github.com/commitizen-tools/commitizen/blob/master/.github/pull_request_template.md) | +| Pick the right test selector for a change | [Validation Guide](validation.md#targeted-test-map) | +| Recover from a CI failure | [Validation Guide](validation.md#ci-failure-recipes) | +| Implement a recurring task type | [Playbooks](#playbooks) | + +The repo-root [`AGENTS.md`](https://github.com/commitizen-tools/commitizen/blob/master/AGENTS.md) is the auto-loaded entry point for most agent tools. It holds the rules an agent needs in every session; this page is the deeper reference. + +## Playbooks + +Recipes for recurring task types. Each playbook is self-contained: trigger, files to read first, ordered steps, verification commands, and known pitfalls. They link out to the human-facing concept docs rather than restating concepts. + +- [Add a version provider](playbooks/add-version-provider.md) +- [Add a changelog format](playbooks/add-changelog-format.md) +- [Add or modify a CLI flag](playbooks/add-cli-flag.md) +- [Deprecate a public API](playbooks/deprecate-public-api.md) +- [Update generated snapshots and screenshots](playbooks/update-snapshots.md) + +If no playbook matches, read the [Architecture Overview](../architecture.md) for the relevant subsystem and follow 1-2 existing examples in the same directory before changing code. + +## Updating these pages + +Treat these pages like any other code change: open a PR, follow the template, run `uv run poe doc:build` to verify the mkdocs build, and check internal links. If you find yourself restating something that already lives in a human-facing doc, link to it instead and shorten the agent doc. diff --git a/docs/contributing/agents/playbooks/add-changelog-format.md b/docs/contributing/agents/playbooks/add-changelog-format.md new file mode 100644 index 000000000..60c630548 --- /dev/null +++ b/docs/contributing/agents/playbooks/add-changelog-format.md @@ -0,0 +1,69 @@ +# Playbook: Add a Changelog Format + +A changelog format handles parsing and rendering a `CHANGELOG.` file in a specific markup language. End-user documentation: [Changelog command](../../../commands/changelog.md). Built-ins are `markdown` (default), `asciidoc`, `textile`, `restructuredtext`. + +Architectural context: [Architecture § Extension points](../../architecture.md#extension-points). + +## Trigger + +- "Support `` changelogs." +- A user wants `cz changelog` to emit something other than Markdown. +- An incremental-changelog use case fails because the user's existing `CHANGELOG` file is not Markdown. + +## Read first + +- `commitizen/changelog_formats/__init__.py` — `ChangelogFormat` Protocol, entry-point group `commitizen.changelog_format`, `KNOWN_CHANGELOG_FORMATS` registry, `_guess_changelog_format` extension-based fallback. +- `commitizen/changelog_formats/base.py:BaseFormat` — provides a default `get_metadata_from_file` that walks the file once for **line-anchored** titles. You override `parse_version_from_title` and `parse_title_level` to fit your markup. +- A close-match existing format: + - Line-anchored heading prefix (`#`, `=`, `h1.`): `commitizen/changelog_formats/markdown.py`, `asciidoc.py`, or `textile.py` — each overrides only the two `parse_*` methods. + - Multi-line titles (overline/underline, as in reStructuredText): `commitizen/changelog_formats/restructuredtext.py` — overrides `get_metadata_from_file` directly because the base class assumes one-line titles. +- `commitizen/templates/` — Jinja2 templates named `CHANGELOG..j2` (e.g. `CHANGELOG.md.j2`, `CHANGELOG.rst.j2`, `CHANGELOG.adoc.j2`); the file extension comes from the `extension` class attribute, not the format name. +- `tests/test_changelog_format_.py` — every format has parity tests; copy the closest one. + +## Steps + +1. **Create the format module** at `commitizen/changelog_formats/.py`. Subclass `BaseFormat`. Set the class attributes: + - `extension: ClassVar[str]` — primary file extension (no dot). + - `alternative_extensions: ClassVar[set[str]]` — other accepted extensions for the same format. +2. **Implement two methods** for line-anchored formats: + - `parse_version_from_title(line: str) -> VersionTag | None` — given one line, return a `VersionTag` if the line is a release heading. + - `parse_title_level(line: str) -> int | None` — return the heading level (1, 2, 3, ...) if the line is a heading. The base class `BaseFormat.get_metadata_from_file` walks the file once and calls both methods per line. + + If your format's titles span multiple lines (overline/title/underline, as in reStructuredText), override `get_metadata_from_file` directly instead — see `RestructuredText` for the algorithm. +3. **Add the Jinja2 template** at `commitizen/templates/CHANGELOG..j2`. Mirror the structure of `CHANGELOG.md.j2` — same blocks, different markup. Make sure the loops over `tree`, `entries`, and `change_type` match. +4. **Register the built-in** in `pyproject.toml` under `[project.entry-points."commitizen.changelog_format"]`: + + ```toml + = "commitizen.changelog_formats.:" + ``` + +5. **Add tests** at `tests/test_changelog_format_.py`. Copy the closest existing test file and adapt the fixtures. +6. **Update the cross-format suite** `tests/test_changelog_formats.py` if it parametrizes over all formats — add the new one to its lists. +7. **Update user docs** at `docs/commands/changelog.md` and `docs/customization/changelog_template.md` — list the new format and show how to opt in via `changelog_format`. +8. **Re-run the install** so the entry point registers: + + ```bash + uv sync --frozen --group base --group test --group linters + ``` + +## Validate + +```bash +uv run pytest tests/test_changelog_format_.py tests/test_changelog_formats.py tests/test_changelog.py tests/test_incremental_build.py -n auto +uv run poe lint +uv run poe doc:build # if docs changed +uv run poe all # final pre-push +``` + +## Pitfalls + +- **`KNOWN_CHANGELOG_FORMATS` is populated at import time** from entry points, so you must re-run `uv sync` after editing `pyproject.toml` before tests can see your new format. +- **Forgetting `alternative_extensions`** — `_guess_changelog_format` uses both `extension` and `alternative_extensions` when the user does not set `changelog_format` explicitly. If a user has `CHANGELOG.`, your format will not auto-detect without it. +- **Template encoding** — Jinja2 reads templates with the active encoding; keep them ASCII-safe or test with non-UTF-8 `encoding` settings. +- **Heading regex anchoring** — match the whole line (`^...$`) when the markup is line-anchored (Markdown headings); a substring match will pick up non-heading lines that mention `unreleased`. +- **Snapshot updates** — many changelog tests use `pytest-regressions`. See the [update-snapshots playbook](update-snapshots.md) when output intentionally changes. + +## Stop and ask if + +- The target format requires structured metadata that does not fit the `parse_title_*` Protocol (e.g., front-matter in YAML). +- The format implies a fundamentally different rendering tree (e.g., one file per release) — that is a bigger change than a format addition. diff --git a/docs/contributing/agents/playbooks/add-cli-flag.md b/docs/contributing/agents/playbooks/add-cli-flag.md new file mode 100644 index 000000000..58e5f9758 --- /dev/null +++ b/docs/contributing/agents/playbooks/add-cli-flag.md @@ -0,0 +1,68 @@ +# Playbook: Add or Modify a CLI Flag + +Commitizen's CLI is built declaratively with [decli](https://github.com/woile/decli) and `argparse` in `commitizen/cli.py`. Flags are dicts inside a `data["subcommands"]` list. End-user documentation: [Commands](../../../commands/init.md). + +## Trigger + +- "Add a `--` flag to `cz `." +- "Make `--` configurable via the config file." +- "Change the default of `--`." + +## Read first + +- `commitizen/cli.py` — the entire CLI schema. Search for the subcommand name in the `subcommands` block to find where its `arguments` list lives. +- `commitizen/commands/.py` — the command class that receives the parsed arguments via `self.arguments`. +- `commitizen/defaults.py:Settings` — TypedDict of all settings; required if your flag should also be config-file-readable. +- `tests/test_cli.py` and `tests/test_cli/` — flag-parsing tests. +- `tests/commands/test__command.py` — behavior tests. +- `docs/commands/.md` — user-facing reference for the subcommand. +- `scripts/gen_cli_help_screenshots.py` — regenerates `--help` SVGs. + +## Steps + +1. **Add the flag** in `commitizen/cli.py` inside the relevant subcommand's `arguments` list. Follow the existing dict shape: + + ```python + { + "name": ["--my-flag", "-m"], # or just "--my-flag" if no short + "action": "store_true", # or "store", "store_false", ParseKwargs, ... + "default": False, # only when not store_true + "help": "", + } + ``` + + Look at neighboring flags in the same subcommand to match style (option grouping, help-text tone). +2. **Consume the flag** in `commitizen/commands/.py`. It will arrive as `self.arguments["my_flag"]` (dashes become underscores). +3. **Config-file support (optional)**. If the flag should also be settable in the user's config file: + - Add the key to `commitizen/defaults.py:Settings` (and to `DEFAULT_SETTINGS` if there is a non-`None` default). + - In the command, fall back to `self.config.settings["my_flag"]` when the CLI value is `None`. + - Document the setting in the relevant `docs/config/.md` page. +4. **Add tests**: + - CLI parsing: extend `tests/test_cli/` or `tests/test_cli.py` with a case that invokes `cz --my-flag` and asserts the parsed namespace. + - Behavior: extend `tests/commands/test__command.py`. +5. **Update user docs** at `docs/commands/.md`. If the flag has a corresponding config setting, also update `docs/config/.md`. +6. **Regenerate the help SVGs** so the new flag appears in the rendered docs. See the [update-snapshots playbook](update-snapshots.md) for the `poe doc:screenshots` workflow. + +## Validate + +```bash +uv run pytest tests/test_cli/ tests/test_cli.py tests/commands/test__command.py -n auto +uv run poe lint +uv run poe doc:build +uv run poe all # final pre-push +``` + +## Pitfalls + +- **Underscores vs dashes** — argparse converts `--my-flag` to `my_flag` in the namespace, but `decli` accepts both. Be consistent with neighboring flags. +- **`store_true` with explicit `default`** — argparse uses `False` as the implicit default for `store_true`; do not set `default` unless you need `None` to detect "user did not pass the flag" (which matters for config-file fallback). +- **Mutually exclusive flags** — argparse does not enforce mutual exclusion through the `decli` dict schema; validate in the command class and raise `commitizen.exceptions.InvalidCommandArgumentError` with a clear message. +- **Forgetting the `Settings` TypedDict** when adding a config-file key — `read_cfg` will accept the value but `mypy` will flag every read of `self.config.settings["my_flag"]`. +- **Breaking flag removals** — see the [deprecate-public-api playbook](deprecate-public-api.md). A flag is user-facing surface; do not remove it without a deprecation window. +- **Stale `--help` screenshots** — CI does not regenerate them. Run `uv run poe doc:screenshots` after any flag change and commit the result. + +## Stop and ask if + +- The flag would change the **exit code** of an existing success path — that breaks scripts that depend on exit codes. See [Exit Codes](../../../exit_codes.md). +- The flag's behavior overlaps with an existing flag with subtly different semantics — propose a deprecation plan first. +- The flag controls something that is currently determined by config precedence rules (CLI > env > config); make the precedence explicit in the issue. diff --git a/docs/contributing/agents/playbooks/add-version-provider.md b/docs/contributing/agents/playbooks/add-version-provider.md new file mode 100644 index 000000000..ce9bf0d67 --- /dev/null +++ b/docs/contributing/agents/playbooks/add-version-provider.md @@ -0,0 +1,69 @@ +# Playbook: Add a Version Provider + +A version provider tells Commitizen where to read and write the project's version (e.g., `pyproject.toml` for PEP 621, `Cargo.toml` for Cargo, `package.json` for npm). End-user documentation: [Version Provider](../../../config/version_provider.md). + +Architectural context: [Architecture § Extension points](../../architecture.md#extension-points). + +## Trigger + +- "Add support for `` version files." +- "Read the version from `` instead of asking the user." +- Issue or feature request mentions a packaging system that is not in the list of built-ins (`cargo`, `commitizen`, `composer`, `npm`, `pep621`, `poetry`, `scm`, `uv`). + +## Read first + +- `commitizen/providers/__init__.py` — registration helper `get_provider`, entry-point group `commitizen.provider`, default provider name. +- `commitizen/providers/base_provider.py` — `VersionProvider`, `FileProvider`, `JsonProvider`, `TomlProvider` base classes. +- An existing provider that resembles your target: + - Simplest JSON case (single file, top-level `version` key): `commitizen/providers/composer_provider.py` — only sets `filename` and `indent`. + - JSON with multi-file updates (package + lockfile + shrinkwrap): `commitizen/providers/npm_provider.py` — overrides `get_version`/`set_version`. + - TOML with multi-file updates (`pyproject.toml` + `uv.lock`): `commitizen/providers/uv_provider.py`. + - SCM tag-based, no file write: `commitizen/providers/scm_provider.py`. +- Test for the closest existing provider: `tests/providers/test__provider.py`. +- `commitizen/config/base_config.py:BaseConfig` — what your provider's `__init__(config)` will receive. + +## Steps + +1. **Create the provider module** at `commitizen/providers/_provider.py`. Subclass the closest base: + - `TomlProvider` if your file is TOML and `[project].version` is sufficient — override only `get`/`set` if the version lives at a different path. + - `JsonProvider` for JSON files; same override pattern. + - `FileProvider` directly when the format is neither TOML nor JSON. + - `VersionProvider` when there is no file (e.g., SCM-tag-based). +2. **Honor the configured encoding** for every file read and write — call `self._get_encoding()` (provided by `FileProvider`) rather than relying on system defaults. See `commitizen/providers/base_provider.py` for examples. +3. **Register the built-in** by adding one line to `pyproject.toml` under `[project.entry-points."commitizen.provider"]`: + + ```toml + = "commitizen.providers:" + ``` + +4. **Export the class** from `commitizen/providers/__init__.py`: import it and add it to `__all__`. +5. **Add tests** at `tests/providers/test__provider.py`. The existing tests demonstrate the patterns — most use `pytest-regressions` for file snapshots and `pytest-mock` to substitute the working directory. +6. **Update user docs** at `docs/config/version_provider.md` — add a row to the providers table and an example block if the configuration is non-trivial. +7. **Re-run the editable install** so the new entry point is picked up: + + ```bash + uv sync --frozen --group base --group test --group linters + ``` + +## Validate + +```bash +uv run pytest tests/providers/test__provider.py tests/test_factory.py -n auto +uv run poe lint +uv run poe doc:build # if docs/config/version_provider.md changed +uv run poe all # final pre-push (PR template requirement) +``` + +## Pitfalls + +- **Forgetting `pyproject.toml` registration** — the provider class will exist but `cz bump` will raise `VersionProviderUnknown` because `get_provider` looks it up by entry point, not by import path. +- **Hardcoded `open(path)`** — drops the user's configured encoding. Use `self._get_encoding()` and `Path.read_text(encoding=...)`. +- **Mutating files outside `set_version`** — providers should be idempotent and side-effect-free on `get_version`. Multi-file updates (like `UvProvider` updating both `pyproject.toml` and `uv.lock`) belong inside `set_version`. +- **Not testing the missing-file path** — `cz bump --get-next` runs `get_version` on a fresh checkout. Make sure your provider returns a reasonable default or raises a clear exception when its file is absent. +- **Cross-platform line endings** — write with `Path.write_text(...)` and add a trailing newline; do not assume `\n`. + +## Stop and ask if + +- The packaging ecosystem requires authentication to discover the version (e.g., reading from a registry). Network-dependent providers are out of scope for built-ins; suggest packaging it as a third-party plugin. +- The version is split across two unrelated files with no clear "primary" source. +- The setting would require a new key in the `Settings` TypedDict (`commitizen/defaults.py`) — that is a config schema change, surface it in the issue. diff --git a/docs/contributing/agents/playbooks/deprecate-public-api.md b/docs/contributing/agents/playbooks/deprecate-public-api.md new file mode 100644 index 000000000..5885b497e --- /dev/null +++ b/docs/contributing/agents/playbooks/deprecate-public-api.md @@ -0,0 +1,88 @@ +# Playbook: Deprecate a Public API + +Commitizen ships a stable Python API on top of the CLI. Removing or renaming anything importable from `commitizen.*` is a breaking change. Use this playbook to add a deprecation window before removal in the next major version. + +## Trigger + +- "Rename `` to ``." +- "Remove the old ``." +- "Change the signature of ``." +- A refactor PR proposes removing a class, function, attribute, or module-level constant that is reachable via `import commitizen.`. + +## Read first + +- `commitizen/changelog_formats/__init__.py` — example of a module-level `__getattr__` that warns and forwards (look at the `guess_changelog_format` → `_guess_changelog_format` deprecation). +- Any existing `warnings.warn(..., DeprecationWarning, stacklevel=2)` call site in the codebase — `grep -rn DeprecationWarning commitizen/`. +- `tests/test_deprecated.py` — pattern for asserting the warning is raised and the old path still works. +- `pyproject.toml:filterwarnings` — examples of deprecations that the test suite explicitly silences (currently `get_smart_tag_range`). + +## Deprecation message convention + +Match the phrasing used elsewhere in the codebase: + +``` + is deprecated and will be removed in v. Use instead. +``` + +Example (from `commitizen/changelog_formats/__init__.py`): + +```python +warnings.warn( + "guess_changelog_format is deprecated and will be removed in v5. " + "Use _guess_changelog_format instead.", + DeprecationWarning, + stacklevel=2, +) +``` + +## Steps + +1. **Pick the deprecation shape** based on what you are changing: + - **Renamed module-level symbol** → add a module `__getattr__` that returns the new symbol after issuing a `DeprecationWarning`. See `commitizen/changelog_formats/__init__.py` for the template. + - **Renamed function/method** → keep the old name as a thin wrapper that warns and delegates. + - **Changed function signature** → add a `typing.overload` for the old signature; internally route old usage to the new path and warn. + - **Renamed class** → keep the old class as a subclass of the new one and emit a warning from `__init_subclass__` or `__init__`. +2. **Issue the warning** with `warnings.warn(, DeprecationWarning, stacklevel=2)`. `stacklevel=2` points the warning at the caller, not at the wrapper. +3. **Decide the removal version**. Use the next major (current version is in `commitizen/__version__.py`). Put the version in the warning message and in the changelog entry. +4. **Add tests** in `tests/test_deprecated.py`: + + ```python + def test_old_name_is_deprecated() -> None: + with pytest.warns(DeprecationWarning, match="will be removed in v"): + result = commitizen.old_name(...) + assert result == expected + ``` + +5. **Silence the warning in the test suite** if the deprecated path is still exercised by unrelated tests. Add to `pyproject.toml:tool.pytest.filterwarnings`: + + ```toml + "ignore: is deprecated:DeprecationWarning", + ``` + +6. **Update all internal callers** to use the new name. Run `git grep -n ` to find them all — search **docs**, **tests**, and `.github/` too, not just source. +7. **Update user docs** if the symbol is documented. For module-private symbols (leading underscore), this step is usually unnecessary. +8. **Note the removal target** in the PR description so the maintainers can track it. + +## Validate + +```bash +uv run pytest tests/test_deprecated.py -n auto +uv run pytest # confirm new and old paths both work +uv run poe lint # mypy will warn if you import deprecated names internally +uv run poe all # final pre-push +git grep -n # zero hits expected outside the deprecation shim +``` + +## Pitfalls + +- **Hard removal in a non-major release** — refuse. The deprecation must ship in version N, and the removal in N+1 (major). +- **Wrong `stacklevel`** — `stacklevel=1` points at the warning call itself and is unhelpful. Almost always use `2`. +- **Missing `filterwarnings` entry** — internal callers that still use the old name will turn the suite noisy (or break `-W error::DeprecationWarning` invocations). +- **Forgetting to grep docs/tests/CI** — the [PR scope rule](https://github.com/commitizen-tools/commitizen/blob/master/AGENTS.md) applies. A deprecation PR that leaves the old name referenced in `docs/` is incomplete. +- **`DeprecationWarning` is hidden by default** — Python suppresses it outside `__main__`. The PR description should mention how a downstream user will see it (test runners surface it, `-W default` shows it). + +## Stop and ask if + +- The symbol is documented as part of the public API and has no obvious successor. Propose a migration path in the issue first. +- Multiple symbols would deprecate at once with cross-dependencies — sequence them carefully so users can migrate in steps. +- The removal target would be the **same major** as the deprecation — that defeats the purpose of the window. diff --git a/docs/contributing/agents/playbooks/update-snapshots.md b/docs/contributing/agents/playbooks/update-snapshots.md new file mode 100644 index 000000000..709b61c5b --- /dev/null +++ b/docs/contributing/agents/playbooks/update-snapshots.md @@ -0,0 +1,93 @@ +# Playbook: Update Generated Snapshots and Screenshots + +Commitizen has two kinds of generated artifacts that look like regular files in `git status` but are produced by poe tasks: + +1. **Test regression snapshots** — used by `pytest-regressions`. Stored under `tests/` next to the tests that produce them. +2. **CLI `--help` screenshots and demo GIFs** — used in the rendered docs. Stored under `docs/images/cli_help/` and `docs/images/cli_interactive/`. + +Both need to be regenerated whenever the underlying behavior changes — test snapshots when output intentionally changes, and CLI screenshots when flags or help text change. + +## Trigger + +- A test fails with `pytest-regressions` complaining about a snapshot mismatch, **and** the new output is the intended output. +- A CLI flag was added or renamed (see [add-cli-flag playbook](add-cli-flag.md)). +- A subcommand's `--help` text changed. + +## Read first + +- `pyproject.toml:tool.poe.tasks` — see the `test:regen`, `doc:screenshots`, and `test:all` task definitions. +- `scripts/gen_cli_help_screenshots.py` and `scripts/gen_cli_interactive_gifs.py` — the screenshot generators. +- `docs/images/*.tape` — `vhs` tape files that drive the GIF generation. +- The failing test, to confirm the snapshot file path and that the diff is intentional. + +## Regenerating test snapshots + +The `test:regen` task runs both single-Python and all-Python regenerations because some snapshots are Python-version-specific (their names start with `py_`): + +```bash +uv run poe test:regen +``` + +Under the hood: + +```toml +"test:regen".parallel = [ + { ref = "test -k 'not py_' --regen-all" }, # version-agnostic + { ref = "test:all -- -k py_ --regen-all" }, # per-Python-version via tox +] +``` + +The `test:all` half requires `tox` and the supported Python interpreters to be installed locally. If only the version-agnostic snapshots changed, run just the first half: + +```bash +uv run pytest -k 'not py_' --regen-all -n auto +``` + +After regeneration: + +1. Inspect every changed file with `git --no-pager diff`. The new content must be what the code was **supposed** to produce — do not blanket-commit snapshot diffs without reading them. +2. Stage and commit the snapshots in the same commit as the code change that motivated them. Reviewers need both halves to validate intentionality. + +## Regenerating CLI help screenshots + +After any flag, help-text, or subcommand change, regenerate the SVGs and GIFs displayed in the docs: + +```bash +uv run poe doc:screenshots +``` + +This runs two scripts in parallel: + +- `scripts/gen_cli_help_screenshots.py` → updates SVGs under `docs/images/cli_help/`. +- `scripts/gen_cli_interactive_gifs.py` → updates GIFs under `docs/images/cli_interactive/` using [`vhs`](https://github.com/charmbracelet/vhs) and the `.tape` files under `docs/images/`. + +GIF generation requires `vhs` to be installed on `PATH`. If it is not available locally, run only the SVG half: + +```bash +uv run python -m scripts.gen_cli_help_screenshots +``` + +and skip the GIFs — surface in the PR description that the GIFs need to be regenerated by a maintainer. + +## Validate + +```bash +git --no-pager diff # read every snapshot diff before committing +uv run pytest -n auto # snapshots now match +uv run poe doc:build # mkdocs renders new images without broken links +uv run poe all # final pre-push +``` + +## Pitfalls + +- **Regenerating without reading the diff.** `--regen-all` overwrites every snapshot the test touches. If a bug in your code changed the output, the snapshot will faithfully record the bug. +- **Committing snapshots separately from the code change.** Reviewers cannot tell intent from a snapshot-only commit. +- **`vhs` not installed.** The GIF script silently fails or produces an empty file. Inspect `docs/images/cli_interactive/*.gif` after running and check file sizes are non-zero. +- **Python-version-specific snapshots.** Tests whose names start with `py_` (e.g., `tests/test_x.py::test_py_3_12_specific`) need `test:all` to regenerate across all interpreters. Skipping that step passes locally but breaks CI on the other matrix rows. +- **Encoding-dependent snapshots.** On Windows, `pytest-regressions` may write CRLF line endings. Configure git or your editor to normalize, or the diff will be all-lines-changed. + +## Stop and ask if + +- A snapshot regenerates with a diff that does not match what your code change should produce — there is a bug. Do not commit the snapshot. +- The CI failure asks for snapshot regeneration but you cannot reproduce the diff locally — Python-version mismatch is the most likely cause; flag it in the PR. +- `doc:screenshots` requires installing `vhs` and you are running in a sandboxed environment that cannot install system packages. diff --git a/docs/contributing/agents/validation.md b/docs/contributing/agents/validation.md new file mode 100644 index 000000000..a6a457b01 --- /dev/null +++ b/docs/contributing/agents/validation.md @@ -0,0 +1,93 @@ +# Validation Guide + +How to verify a change to Commitizen without running the full CI matrix every time. This page is the agent-facing counterpart to the human [Contributing TL;DR](../contributing_tldr.md), focused on **which** selector to run for a given change and **how** to recognize CI failures. + +For the full poe command reference, see [Contributing TL;DR](../contributing_tldr.md#command-cheat-sheet). + +## Targeted test map + +During iteration, prefer running only the tests that cover what you changed. The full suite is for the final pre-push run. Tests mirror the source tree (see [Architecture Overview § Tests mirror the source tree](../architecture.md#tests-mirror-the-source-tree)); the table below picks the most useful selectors. + +| Changing... | Targeted selector | +|---|---| +| A version provider in `commitizen/providers/_provider.py` | `uv run pytest tests/providers/test__provider.py -n auto` | +| The provider lookup or registration | `uv run pytest tests/providers/ tests/test_factory.py -n auto` | +| A changelog format in `commitizen/changelog_formats/.py` | `uv run pytest tests/test_changelog_format_.py tests/test_changelog_formats.py -n auto` | +| The changelog generation engine | `uv run pytest tests/test_changelog.py tests/test_incremental_build.py -n auto` | +| A version scheme | `uv run pytest tests/test_version_scheme_.py tests/test_version_schemes.py -n auto` | +| Tag parsing / format | `uv run pytest tests/test_tags.py -n auto` | +| Bump logic | `uv run pytest "tests/test_bump_*.py" tests/commands/test_bump_command.py -n auto` | +| A CLI subcommand `commitizen/commands/.py` | `uv run pytest tests/commands/test__command.py tests/test_cli/ -n auto` | +| CLI argument parsing (`commitizen/cli.py`) | `uv run pytest tests/test_cli/ tests/test_cli.py -n auto` | +| Configuration loading | `uv run pytest tests/test_conf.py -n auto` | +| A built-in commit convention (`commitizen/cz/.py`) | `uv run pytest "tests/test_cz_*.py" -n auto` | +| A deprecation | `uv run pytest tests/test_deprecated.py -n auto` plus the affected subsystem's tests | +| Exception classes | `uv run pytest tests/test_exceptions.py -n auto` | + +## Choosing a final check + +| Command | When to run | What it does | +|---|---|---| +| `uv run poe all` | Before pushing a PR (named in the PR template) | `format` -> `lint` -> `check-commit` -> `cover`. Auto-formats your files. | +| `uv run poe ci` | When you want to mirror CI exactly | `check-commit` -> `prek run --all-files` -> `cover`. Does **not** auto-format; fails if files need formatting. | +| `uv run poe doc:build` | After any docs change | `mkdocs build`. Prints broken-link warnings. Finite, not a server. | +| `uv run poe doc` | When iteratively editing docs | `mkdocs serve --livereload`. Runs until killed; not a verification step. | + +Recommended sequence: + +1. Iterate with targeted tests + `uv run poe format`. +2. Before push: `uv run poe all` (PR template requirement). +3. Optional: `uv run poe ci` to catch anything that `prek` will block in CI. +4. If docs changed: `uv run poe doc:build`. + +## CI failure recipes + +The CI matrix is fail-fast across Python 3.10–3.14 × ubuntu/macos/windows (see `.github/workflows/`). Inspect the earliest failing job; the others are cancelled. + +### "Format Python code...Failed" + +The `prek` formatting hook modified files. Run locally and commit the result: + +```bash +uv run poe format +git add -u && git commit --amend --no-edit +``` + +### mypy `[arg-type]` on a TypedDict construction + +Dynamically constructed dicts (e.g., from `pytest.mark.parametrize`) passed to a TypedDict-typed parameter need an explicit ignore: + +```python +@pytest.mark.parametrize("settings", [{"version_scheme": "pep440"}]) +def test_x(settings: Settings) -> None: # type: ignore[arg-type] + ... +``` + +### `pathspec 'vX.Y.Z' did not match` + +`.pre-commit-config.yaml` pins a specific tag of this repo as a hook source. When your branch is older than that tag, the hook fails because the tag is unknown. Fix by rebasing onto the latest master: + +```bash +git fetch origin master +git rebase origin/master +``` + +### `VersionProtocol` + `issubclass` `TypeError` + +`commitizen/version_schemes.py:VersionProtocol` has non-method members (properties), so it cannot be passed to `issubclass()`. For runtime validation, use `hasattr` checks against the concrete members or duck-type the value instead of subclass-checking. + +### Tests pass locally but fail in CI on Windows only + +Most often a path-separator or encoding assumption: + +- Use `pathlib.Path` and `Path(...).as_posix()` for string comparisons. +- Read files with `encoding=` (the convention is to honor `config.settings["encoding"]`; see `commitizen/providers/base_provider.py:_get_encoding`). +- Avoid hardcoded `"\n"` when comparing file contents — use `splitlines()` or set `newline=""` when writing. + +### `cz check` rejects a fixup or merge commit on the branch + +`poe check-commit` runs `cz --no-raise 3 check --rev-range origin/master..`. Squash or amend the offending commit so the branch contains only Conventional-Commit-shaped messages, or rebase to drop it. + +### Coverage drop on CodeCov + +The `cover` task generates `coverage.xml` consumed by CodeCov. If coverage drops, add tests for the new code paths before pushing. Inspect `coverage.xml` locally or re-run `uv run poe cover` and inspect the terminal report. diff --git a/docs/contributing/architecture.md b/docs/contributing/architecture.md new file mode 100644 index 000000000..90550fe3f --- /dev/null +++ b/docs/contributing/architecture.md @@ -0,0 +1,87 @@ +# Architecture Overview + +This page is a map of Commitizen's subsystems and extension points. It is aimed at contributors (human or AI) who are about to change code and need to know where things live and how they fit together. + +For end-user concepts, see the [Commands](../commands/init.md) and [Configuration](../config/configuration_file.md) sections. + +## Top-level layout + +``` +commitizen/ +├── cli.py # CLI entry point and argument parsing (uses decli) +├── commands/ # One module per CLI subcommand (commit, bump, changelog, ...) +├── config/ # Configuration discovery, parsing, and base classes +├── providers/ # Version providers (where to read/write the version) +├── changelog_formats/ # Changelog file format handlers (markdown, asciidoc, ...) +├── cz/ # Built-in commit conventions (conventional_commits, jira, customize) +├── version_schemes.py # Version schemes (pep440, semver, semver2) +├── tags.py # Tag format parsing and matching +├── changelog.py # Changelog generation engine +├── bump.py # Version-bump engine +├── defaults.py # Default settings and the Settings TypedDict +├── exceptions.py # CLI-facing exception types and exit codes +├── out.py # Standard output helpers +├── git.py # Git wrapper used by all commands +├── hooks.py # Pre/post bump hook execution +└── templates/ # Built-in Jinja2 templates for changelog rendering +``` + +## Extension points + +Commitizen is plugin-friendly. Four kinds of extensions can be registered by external packages via Python entry points; the built-in implementations use the same mechanism. + +| Kind | Entry-point group | Built-ins registered in `pyproject.toml` | Base class / Protocol | +|---|---|---|---| +| Commit convention | `commitizen.plugin` | `cz_conventional_commits`, `cz_jira`, `cz_customize` | `commitizen/cz/base.py:BaseCommitizen` | +| Version provider | `commitizen.provider` | `cargo`, `commitizen`, `composer`, `npm`, `pep621`, `poetry`, `scm`, `uv` | `commitizen/providers/base_provider.py:VersionProvider` | +| Version scheme | `commitizen.scheme` | `pep440`, `semver`, `semver2` | `commitizen/version_schemes.py:VersionProtocol` | +| Changelog format | `commitizen.changelog_format` | `markdown`, `asciidoc`, `textile`, `restructuredtext` | `commitizen/changelog_formats/base.py:BaseFormat` | + +Each kind is registered as a Python entry point and resolved through `importlib.metadata.entry_points(...)`. To add a new built-in implementation you register it in `pyproject.toml` under the appropriate `[project.entry-points."..."]` table. + +End-user documentation for these extension points lives elsewhere — see [Version Provider](../config/version_provider.md), [Customized Python Class](../customization/python_class.md), and [Changelog Template](../customization/changelog_template.md). This page focuses on where the source lives and how it is wired together. + +## Configuration layering + +Configuration is discovered, parsed, and exposed as a `Settings` TypedDict. + +1. **Discovery** — `commitizen/config/__init__.py:read_cfg` searches the working directory (and the git project root when different) for known config files in a defined order (see `commitizen/defaults.py:CONFIG_FILES`). +2. **Format-specific parsing** — `commitizen/config/factory.py:create_config` dispatches to one of: + - `commitizen/config/toml_config.py:TomlConfig` (TOML; includes `pyproject.toml` under `[tool.commitizen]`) + - `commitizen/config/json_config.py:JsonConfig` + - `commitizen/config/yaml_config.py:YAMLConfig` +3. **Defaults merge** — every parser inherits from `commitizen/config/base_config.py:BaseConfig`, which starts from `commitizen/defaults.py:DEFAULT_SETTINGS` and overlays the user values. +4. **Consumption** — commands read `config.settings[...]`; providers and formats receive the live `BaseConfig` so they can react to settings such as `encoding`, `tag_format`, and `version_scheme`. + +The `Settings` TypedDict in `defaults.py` is the authoritative list of recognized keys. Adding a new setting almost always means touching this file. + +## Command flow + +`cli.py` parses `argv` with [decli](https://github.com/woile/decli), resolves the chosen subcommand to a class under `commitizen/commands/`, then instantiates and calls it. A typical command: + +1. Reads `config.settings`. +2. Resolves dependencies (provider, scheme, changelog format) via the `get_*` helpers in the respective subpackages. +3. Does its work, surfacing user-visible text through `commitizen/out.py` and errors through `commitizen/exceptions.py` (each exception carries an exit code defined in `commitizen/exceptions.py` and documented in [Exit Codes](../exit_codes.md)). + +`cz commit` and `cz bump` are the most stateful commands — they call `git` through `commitizen/git.py`, run user-defined `pre_bump_hooks`/`post_bump_hooks` via `commitizen/hooks.py`, and may mutate version files through the active provider. + +## Templates and changelog rendering + +Changelog rendering uses Jinja2. Built-in templates live under `commitizen/templates/`. The template loader is a `ChoiceLoader` whose first loader is `FileSystemLoader(".")` and whose second loader is provided by the active commit-convention class (default: a `PackageLoader` for built-in templates), so a repository can override any built-in template by placing a file of the same name at the project root or in the configured template directory. + +## Tests mirror the source tree + +Tests are organized to mirror the source modules: + +| Source | Test | +|---|---| +| `commitizen/providers/*` | `tests/providers/`, `tests/test_factory.py` | +| `commitizen/changelog_formats/*` | `tests/test_changelog_format_*.py`, `tests/test_changelog_formats.py` | +| `commitizen/version_schemes.py` | `tests/test_version_schemes.py`, `tests/test_version_scheme_*.py` | +| `commitizen/commands/*` | `tests/commands/`, `tests/test_cli/` | +| `commitizen/config/*` | `tests/test_conf.py` | +| `commitizen/bump.py` | `tests/test_bump_*.py` | +| `commitizen/changelog.py` | `tests/test_changelog.py`, `tests/test_incremental_build.py` | +| `commitizen/tags.py` | `tests/test_tags.py` | + +When you add or modify a subsystem, the targeted test file is usually obvious from this mirror. The [targeted-test map for agents](agents/validation.md#targeted-test-map) captures the most useful selectors. diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index 446410533..165f87130 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -2,6 +2,9 @@ First, thank you for taking the time to contribute! 🎉 Your contributions help make Commitizen better for everyone. +!!! tip "Using an AI assistant?" + See [For AI Agents](agents/index.md) for agent-shaped recipes, a targeted-test map, and CI-failure playbooks that complement (and aggressively link back to) this human-facing guide. + When contributing to Commitizen, we encourage you to: 1. First, check out the [issues](https://github.com/commitizen-tools/commitizen/issues) and [Features we won't add](../features_wont_add.md) to see if there's already a discussion about the change you wish to make. diff --git a/docs/contributing/pull_request.md b/docs/contributing/pull_request.md index 4682496b4..4a808f7ff 100644 --- a/docs/contributing/pull_request.md +++ b/docs/contributing/pull_request.md @@ -26,6 +26,9 @@ We welcome contributions that use AI tools for assistance, but we have strict qu !!! note Most of our new documentation changes are, of course, generated by AI, but we still need to review it and make sure it's correct. +!!! tip "AI agents driving the PR" + See [For AI Agents](agents/index.md) for agent-shaped recipes, targeted-test selectors, and playbooks for recurring task types (adding a provider, deprecating an API, regenerating snapshots, etc.). + ![when bro's code is filled with "🔥 🚀 💥 ❌ ✅"](https://images3.memedroid.com/images/UPLOADED78/69501f1c23cab.webp) ### Guidelines for AI-Assisted PRs diff --git a/mkdocs.yml b/mkdocs.yml index 5f3190f7d..729d4ebbf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -89,6 +89,16 @@ nav: - "contributing/contributing_tldr.md" - "contributing/contributing.md" - "contributing/pull_request.md" + - Architecture Overview: "contributing/architecture.md" + - "For AI Agents": + - "contributing/agents/index.md" + - Validation Guide: "contributing/agents/validation.md" + - Playbooks: + - Add a version provider: "contributing/agents/playbooks/add-version-provider.md" + - Add a changelog format: "contributing/agents/playbooks/add-changelog-format.md" + - Add or modify a CLI flag: "contributing/agents/playbooks/add-cli-flag.md" + - Deprecate a public API: "contributing/agents/playbooks/deprecate-public-api.md" + - Update snapshots and screenshots: "contributing/agents/playbooks/update-snapshots.md" - "history.md" - Resources: "external_links.md" @@ -167,3 +177,12 @@ plugins: - contributing/contributing_tldr.md: Fast path for setting up a local dev workflow. - contributing/contributing.md: Full contributor guide. - contributing/pull_request.md: Expectations for preparing and submitting PRs. + - contributing/architecture.md: Map of Commitizen's subsystems and extension points. + For AI Agents: + - contributing/agents/index.md: Router and source-of-truth map for agent-shaped contributor docs. + - contributing/agents/validation.md: Targeted-test map, choosing a final check, and CI failure recipes. + - contributing/agents/playbooks/add-version-provider.md: Recipe for adding a built-in version provider. + - contributing/agents/playbooks/add-changelog-format.md: Recipe for adding a built-in changelog format. + - contributing/agents/playbooks/add-cli-flag.md: Recipe for adding or modifying a CLI flag. + - contributing/agents/playbooks/deprecate-public-api.md: Recipe for adding a deprecation window before removal. + - contributing/agents/playbooks/update-snapshots.md: Regenerating pytest-regressions snapshots and CLI help screenshots.