From 7f03ff657e472a6bc7c79281084ceff5e049b6b9 Mon Sep 17 00:00:00 2001 From: Santiago Fraire Willemoes Date: Wed, 7 Oct 2026 08:43:18 +0100 Subject: [PATCH 1/3] docs(gh): update gh action tutorial --- docs/tutorials/github_actions.md | 189 +++++++++++-------------------- 1 file changed, 67 insertions(+), 122 deletions(-) diff --git a/docs/tutorials/github_actions.md b/docs/tutorials/github_actions.md index f0b2160724..bee5ee5fae 100644 --- a/docs/tutorials/github_actions.md +++ b/docs/tutorials/github_actions.md @@ -12,11 +12,11 @@ Before setting up the workflow, you'll need: 1. A personal access token with repository write permissions 2. Commitizen configured in your project (see [configuration documentation](../config/configuration_file.md)) -### Automatic version bumping +### Automatic Version Bumping To automatically execute `cz bump` in your CI and push the new commit and tag back to your repository, follow these steps: -#### Step 1: Create a personal access token +#### Step 1: Create a Personal Access Token 1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) 2. Click "Generate new token (classic)" @@ -27,7 +27,7 @@ To automatically execute `cz bump` in your CI and push the new commit and tag ba !!! warning "Important: Use Personal Access Token, not GITHUB_TOKEN" If you use `GITHUB_TOKEN` instead of `PERSONAL_ACCESS_TOKEN`, the workflow won't trigger another workflow run. This is a GitHub security feature to prevent infinite loops. The `GITHUB_TOKEN` is treated like using `[skip ci]` in other CI systems. -#### Step 2: Add the token as a repository secret +#### Step 2: Add the Token as a Repository Secret 1. Go to your repository on GitHub 2. Navigate to `Settings > Secrets and variables > Actions` @@ -36,97 +36,82 @@ To automatically execute `cz bump` in your CI and push the new commit and tag ba 5. Paste the token you copied in Step 1 6. Click "Add secret" -#### Step 3: Create the workflow file +#### Step 3: Create the Workflow File -Create a new file `.github/workflows/bumpversion.yml` in your repository with the following content: +Create a new file `.github/workflows/bump-version.yml` in your repository with the following content: -```yaml title=".github/workflows/bumpversion.yml" +```yaml title=".github/workflows/bump-version.yml" name: Bump version on: push: branches: - - master # or 'main' if that's your default branch + - main jobs: - bump-version: - if: "!startsWith(github.event.head_commit.message, 'bump:')" + bump: runs-on: ubuntu-latest - name: "Bump version and create changelog with commitizen" + permissions: + contents: write + actions: write steps: - - name: Check out - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: - token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" fetch-depth: 0 - - name: Create bump and changelog - uses: commitizen-tools/commitizen-action@master + fetch-tags: true + - uses: commitizen-tools/setup-cz@main with: - github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + python-version: "3.x" + - id: bump-version + run: | + old_sha="$(git rev-parse HEAD)" + + cz --no-raise 21 bump --yes --annotated-tag + + if [ "$(git rev-parse HEAD)" = "$old_sha" ]; then + echo "No bump-eligible commits found, skipping release." + echo "bumped=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "bumped=true" >> $GITHUB_OUTPUT + git push --follow-tags + new_version="$(cz version -p)" + echo "new_version=$new_version" >> $GITHUB_OUTPUT + new_version_tag="$(cz version -p --tag)" + echo "new_version_tag=$new_version_tag" >> $GITHUB_OUTPUT + - name: Github Release + if: steps.bump-version.outputs.bumped == 'true' + env: + GH_TOKEN: ${{ github.token }} + NEW_VERSION: ${{ steps.bump-version.outputs.new_version }} + NEW_VERSION_TAG: ${{ steps.bump-version.outputs.new_version_tag }} + run: | + gh release create "${NEW_VERSION_TAG}" --notes-file .changelog.md + cz changelog --dry-run "${NEW_VERSION}" > .changelog.md ``` #### How it works -- **Trigger**: The workflow runs on every push to the `master` branch (or `main` if you change it) -- **Conditional check**: The `if` condition prevents infinite loops by skipping the job if the commit message starts with `bump:` -- **Checkout**: Uses your personal access token to check out the repository with full history (`fetch-depth: 0`) -- **Bump**: The `commitizen-action` automatically: +- The action will trigger the workflow on every push to the `main` branch. +- The job requests `contents: write` and `actions: write` so it can push the bump commit and tag, and trigger dependent workflows. +- **Setup**: The `setup-cz` action installs the Commitizen CLI with the requested Python version +- **Bump**: The `cz bump --yes --annotated-tag` command automatically: - Determines the version increment based on your commit messages - Updates version files (as configured in your `pyproject.toml` or other config) - - Creates a new git tag + - Creates a new annotated git tag - Generates/updates the changelog - - Pushes the commit and tag back to the repository +- **Push**: `git push --follow-tags` pushes the bump commit along with the new tag back to the repository +- **Github Release**: creates a Github Release Once you push this workflow file to your repository, it will automatically run on the next push to your default branch. -Check out [commitizen-action](https://github.com/commitizen-tools/commitizen-action) for more details. - -### Creating a GitHub release - -To automatically create a GitHub release when a new version is bumped, you can extend the workflow above. +Check out [commitizen-tools/setup-cz](https://github.com/commitizen-tools/setup-cz) for more details. -The `commitizen-action` creates an environment variable called `REVISION` containing the newly created version. You can use this to create a release with the changelog content. - -```yaml title=".github/workflows/bumpversion.yml" -name: Bump version - -on: - push: - branches: - - master # or 'main' if that's your default branch - -jobs: - bump-version: - if: "!startsWith(github.event.head_commit.message, 'bump:')" - runs-on: ubuntu-latest - name: "Bump version and create changelog with commitizen" - steps: - - name: Check out - uses: actions/checkout@v6 - with: - token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" - fetch-depth: 0 - - name: Create bump and changelog - uses: commitizen-tools/commitizen-action@master - with: - github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - changelog_increment_filename: body.md - - name: Release - uses: ncipollo/release-action@v1 - with: - tag: v${{ env.REVISION }} - bodyFile: "body.md" - skipIfReleaseExists: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -``` - -You can find the complete workflow in our repository at [bumpversion.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/bumpversion.yml). - -### Previewing the version bump on pull requests +### Previewing the Version Bump on Pull Requests To help reviewers spot unexpected version bumps before merging, you can run -`cz bump --dry-run` on every pull request and post (or update) a sticky +`cz version -p` on every pull request and post (or update) a sticky comment summarizing the would-be version bump. Create `.github/workflows/pr-bump-preview.yml`: @@ -153,21 +138,20 @@ jobs: }} runs-on: ubuntu-latest steps: - - name: Check out PR head - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: - ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 fetch-tags: true persist-credentials: false - uses: commitizen-tools/setup-cz@main with: + python-version: "3.x" set-git-config: false - - name: Run cz bump --dry-run + - name: Run cz version id: dry-run run: | set +e - output="$(cz bump --dry-run --yes 2>&1)" + output="$(cz version -p --next 2>&1)" status=$? set -e { @@ -222,40 +206,9 @@ jobs: edit-mode: replace ``` -#### How it works - -- **Trigger**: `pull_request_target` runs in the context of the base - repository, which gives the workflow `pull-requests: write` permission - even for PRs from forks. We deliberately gate the job to **same-repo PRs - only** (`head.repo == base.repo`); fork PRs are skipped. This is because - `cz bump` renders [Jinja templates from the working directory][jinja] - whenever [`update_changelog_on_bump`](../config/configuration_file.md) is - enabled, and the renderer is not sandboxed — running it against - fork-controlled files under a write token would risk arbitrary code - execution and token exfiltration. Same-repo PRs are written by - collaborators who already have push access, so the same risk doesn't - apply. -- **Setup**: [`commitizen-tools/setup-cz`](https://github.com/commitizen-tools/setup-cz) - installs the Commitizen CLI; no language-specific build tooling is required. -- **Defense in depth**: `persist-credentials: false` on `actions/checkout` - keeps the workflow token out of the local git config. -- **Dry-run**: `cz bump --dry-run --yes` computes the next version (and, if - `update_changelog_on_bump` is set in your config, also the changelog - entries that would be produced). Exit code `21` (`NoneIncrementExit`) - is treated as "no eligible bump" rather than a failure. -- **Sticky comment**: [`peter-evans/find-comment`](https://github.com/peter-evans/find-comment) - looks up an existing comment by the hidden HTML marker - `` and bot author, then - [`peter-evans/create-or-update-comment`](https://github.com/peter-evans/create-or-update-comment) - edits it in place (or creates a new one on the first run when the - marker is not yet present), instead of leaving a growing trail of - comments. - -[jinja]: https://github.com/commitizen-tools/commitizen/blob/master/commitizen/changelog.py - You can find the complete workflow in our repository at [pr-bump-preview.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/pr-bump-preview.yml). -### Publishing a Python package +### Publishing a Python Package After a new version tag is created by the bump workflow, you can automatically publish your package to PyPI. @@ -280,7 +233,7 @@ After a new version tag is created by the bump workflow, you can automatically p 5. Paste the PyPI token 6. Click "Add secret" -#### Step 3: Create the publish workflow +#### Step 3: Create the Publish Workflow Create a new file `.github/workflows/pythonpublish.yml` that triggers on tag pushes: @@ -296,31 +249,23 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: "3.x" - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: latest - virtualenvs-in-project: true - virtualenvs-create: true - - name: Install dependencies + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v10 + - name: publish run: | - poetry --version - poetry install - - name: Build and publish - env: - POETRY_HTTP_BASIC_PYPI_USERNAME: __token__ - POETRY_HTTP_BASIC_PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: poetry publish --build + uv sync + uv publish --username "${PYPI_USERNAME}" --password "${PYPI_PASSWORD}" ``` -This workflow uses Poetry to build and publish the package. You can find the complete workflow in our repository at [pythonpublish.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/pythonpublish.yml). +This workflow uses uv to build and publish the package. +You can find the complete workflow in our repository at [pythonpublish.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/pythonpublish.yml). !!! note "Alternative publishing methods" You can also use [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) or other build tools like `setuptools`, `flit`, or `hatchling` to publish your package. From a60c07e6e859a7f0a8c22a8e1f727e50763bdbef Mon Sep 17 00:00:00 2001 From: Santiago Fraire Willemoes Date: Wed, 7 Oct 2026 10:11:34 +0100 Subject: [PATCH 2/3] docs(monorepo): improve docs and add docs to verify it works --- docs/tutorials/monorepo_guidance.md | 107 ++++++++++++---------------- tests/test_tags.py | 80 +++++++++++++++++++++ 2 files changed, 126 insertions(+), 61 deletions(-) diff --git a/docs/tutorials/monorepo_guidance.md b/docs/tutorials/monorepo_guidance.md index f863a9e531..6c3687975d 100644 --- a/docs/tutorials/monorepo_guidance.md +++ b/docs/tutorials/monorepo_guidance.md @@ -3,85 +3,70 @@ This tutorial assumes that your monorepo is structured with multiple components that can be released independently of each other. It also assumes that you are using conventional commits with scopes. -Here is a step-by-step example using two libraries, `library-b` and `library-z`: +Here is a step-by-step example using two libraries, `library-foo` and `library-alice`: 1. **Organize your monorepo** - For example, you might have one of these layouts: +For example, you might have one of these layouts: - ```shell-session - . - ├── library-b - │   └── .cz.toml - └── library-z - └── .cz.toml - ``` +```shell-session +. +├── library-foo +│   └── .cz.toml +└── library-alice + └── .cz.toml +``` - ```shell-session - src - ├── library-b - │   └── .cz.toml - └── library-z - └── .cz.toml - ``` +```shell-session +src +├── library-foo +│   └── .cz.toml +└── library-alice + └── .cz.toml +``` 2. **Add a Commitizen configuration for each component** - ```toml - # library-b/.cz.toml - [tool.commitizen] - name = "cz_customize" - version = "0.0.0" - tag_format = "${version}-library-b" # the component name can be a prefix or suffix with or without a separator - ignored_tag_formats = ["${version}-library-*"] # Avoid noise from other tags - update_changelog_on_bump = true - ``` - - ```toml - # library-z/.cz.toml - [tool.commitizen] - name = "cz_customize" - version = "0.0.0" - tag_format = "${version}-library-z" - ignored_tag_formats = ["${version}-library-*"] # Avoid noise from other tags - update_changelog_on_bump = true - ``` +```toml +# library-foo/.cz.toml +[tool.commitizen] +name = "cz_customize" +version = "0.0.0" +tag_format = "${version}-library-foo" # the component name can be a prefix or suffix with or without a separator +ignored_tag_formats = ["${version}-library-(?!foo$).*"] # Avoid noise from other tags +update_changelog_on_bump = true +``` -3. **Bump each component independently** +```toml +# library-alice/.cz.toml +[tool.commitizen] +name = "cz_customize" +version = "0.0.0" +tag_format = "${version}-library-alice" +ignored_tag_formats = ["${version}-library-(?!alice$).*"] # Avoid noise from other tags +update_changelog_on_bump = true +``` - ```sh - cz --config library-b/.cz.toml bump --yes - cz --config library-z/.cz.toml bump --yes - ``` +3. **Bump each component independently** +```sh +cz --config library-foo/.cz.toml bump --yes +cz --config library-alice/.cz.toml bump --yes +``` -## Changelog per component +## Changelog Per Component To filter the correct commits for each component, you'll need to define a strategy. For example: - Trigger the pipeline based on the changed path. This can have some downsides, as you'll rely on the developer not including files from unrelated components. - - [GitHub Actions](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore) uses `path` - - [Jenkins](https://www.jenkins.io/doc/book/pipeline/syntax/#built-in-conditions) uses `changeset` - - [GitLab](https://docs.gitlab.com/ee/ci/yaml/#ruleschanges) uses `rules:changes` + - [GitHub Actions](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore) uses `path` + - [Jenkins](https://www.jenkins.io/doc/book/pipeline/syntax/#built-in-conditions) uses `changeset` + - [GitLab](https://docs.gitlab.com/ee/ci/yaml/#ruleschanges) uses `rules:changes` - Filter commits by a specific pattern in the commit message (recommended) +### Filtering Commits -### Example with scope in conventional commits - -In this example, we want `library-b`'s changelog to only include commits that use the `library-b` scope. -To achieve this, we configure Commitizen to match only commit messages with that scope. - -Here is an example configuration for `library-b`: - -```toml -[tool.commitizen.customize] -changelog_pattern = "^(feat|fix)\\(library-b\\)(!)?:" # the type pattern can be a wildcard or any types you wish to include -``` - -With this configuration, a commit message like the following would be included in `library-b`'s changelog: - -```text -fix(library-b): Some awesome message -``` +For advanced use cases, filter the commits passed to `version`, `bump` or `changelog` by implementing custom conventional commit rules. +See [Filter commits before bump and changelog generation](../customization/python_class.md#filter-commits-before-bump-and-changelog-generation) diff --git a/tests/test_tags.py b/tests/test_tags.py index 3d5f8fc351..bdd6714272 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1,3 +1,5 @@ +import pytest + from commitizen.git import GitTag from commitizen.tags import TagRules @@ -140,3 +142,81 @@ def test_is_version_tag_accepts_dotless_devrelease_in_custom_tag_format(): extracted = rules.extract_version(_git_tag("version-1.2.3dev1")) assert str(extracted) == "1.2.3.dev1" + + +def test_monorepo_ignored_tag_formats_keep_own_tags(capsys: pytest.CaptureFixture): + """Regression test for the monorepo workflow in + ``docs/tutorials/monorepo_guidance.md``. + + Each component configures a suffixed ``tag_format`` and ignores sibling + component tags with a wildcard. + + Ignoring sibling tags must not reject the component's own tags. Ignored + tags are expected noise, so they must not emit "Invalid version tag" + warnings; only truly unexpected tags warn. + """ + library_foo_rules = TagRules( + tag_format="${version}-library-foo", + ignored_tag_formats=["${version}-library-(?!foo$).*"], + ) + library_zoo_rules = TagRules( + tag_format="${version}-library-zoo", + ignored_tag_formats=["${version}-library-(?!zoo$).*"], + ) + + # Own tags remain version tags, sibling tags are filtered out. + assert library_foo_rules.is_version_tag("1.0.0-library-foo") is True + assert library_foo_rules.is_version_tag("1.0.0-library-zoo") is False + + assert library_zoo_rules.is_version_tag("1.0.0-library-zoo") is True + assert library_zoo_rules.is_version_tag("1.0.0-library-foo") is False + + # Check also for prefix + assert library_foo_rules.is_ignored_tag("1.0.0-library-foobar") is True + + # The own tag is parseable, so a rejection above is a filtering problem, + # not a parsing one. + extracted = library_foo_rules.extract_version(_git_tag("1.0.0-library-foo")) + assert str(extracted) == "1.0.0" + + # Ignored tags do not warn. Unknown tags still do. + library_foo_rules.is_version_tag("1.0.0-library-zoo", warn=True) + library_foo_rules.is_version_tag("unexpected-tag", warn=True) + captured = capsys.readouterr() + assert "1.0.0-library-zoo" not in captured.err + assert "unexpected-tag" in captured.err + + +def test_monorepo_get_version_tags_filters_sibling_components( + capsys: pytest.CaptureFixture, +): + """``get_version_tags`` keeps only the component's own tags when the + repository also contains sibling component tags, as configured in + ``docs/tutorials/monorepo_guidance.md``. + + The changelog and the ``scm`` version provider use this filtering, so the + ignored wildcard must silence sibling tags without dropping this + component's own tags. + """ + tags = [ + _git_tag("1.0.0-library-b"), + _git_tag("1.0.0-library-z"), + _git_tag("1.1.0-library-b"), + _git_tag("unexpected-tag"), + ] + rules = TagRules( + tag_format="${version}-library-b", + ignored_tag_formats=["${version}-library-(?!b$).*"], + ) + + version_tags = rules.get_version_tags(tags, warn=True) + + assert [t.name for t in version_tags] == [ + "1.0.0-library-b", + "1.1.0-library-b", + ] + + # Only the truly unexpected tag warns; sibling tags are known noise. + captured = capsys.readouterr() + assert "unexpected-tag" in captured.err + assert "1.0.0-library-z" not in captured.err From f2437324092a7049de819e16d129d847c5c8af3c Mon Sep 17 00:00:00 2001 From: Santiago Fraire Willemoes Date: Wed, 7 Oct 2026 10:13:19 +0100 Subject: [PATCH 3/3] docs: reduce AGENTS size --- AGENTS.md | 153 ++++++++++++++---------------------------------------- 1 file changed, 40 insertions(+), 113 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 134d1db3f7..023faa4b35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,130 +1,57 @@ -# AGENTS instructions +# AGENT + +- When writing something intended for human consumption (comment, commit message, reply to prompt, documentation, etc.), keep answers short and concise +- Technical prose only, be direct. Use ASD-STE100 (Simplified Technical English) +- Use concise, clear, simple language. Define unavoidable jargon before using it +- Code explains the how. You must always document the code's what and why +- Always disclose if it was made by an AI model (commit footer, pull request body) with `Assisted By: AI` +- Ask for clarification if in doubt, don't assume. And if the user provides insightful context, add it to the documentation +- Always write and maintain the documentation under `docs/`, if it doesn't exist create it, use the context and intuition gathered while coding and interacting with the user. +- Don't touch blocks of code unrelated to the feature you implement. E.g. Don't add comments to a block of code if you did not create it or modify it +- Follow the Liskov substitution principle: design by contract +- Pull requests must follow the [guidelines](docs/contributing/pull_request.md) and the template in `.github/pull_request_template.md`. +- Commit messages must follow [docs/tutorials/writing_commits.md](docs/tutorials/writing_commits.md). ## 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. +`commitizen` is a tool for release management, with automatic version bump (git tag and file updates), changelog generation, and commit message enforcement (defaults to "conventional commits"). -## Project Overview +## Coding -- **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). +Review [`docs/contributing/contributing.md`](docs/contributing/contributing.md) -## General Expectations +Main commands -- **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`. - -### Commit and Pull Request Types - -Choose the Conventional Commit type based on the change's release impact, not -the files or subsystem it touches. The type controls automated version bumps: - -- `feat` triggers a minor release. -- `fix`, `perf`, and `refactor` trigger a patch release. -- `ci`, `docs`, `test`, `build`, and `chore` do not trigger a release. -- A breaking-change marker triggers a major release. - -Use the same rule for pull request titles because squash merges use the title as -the resulting commit message. In particular, use `ci:` for changes limited to -workflows or release automation. A scope does not change release impact: -`fix(ci): ...` still triggers a patch release. - -## 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) +``` +uv run poe format +uv run poe lint +uv run poe test +uv run poe ci # commit check + pre-commit hooks via `prek` + test with coverage +uv run poe all # format + lint + check-commit + coverage ``` -### 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) - -Always run at least `uv run ruff check --fix . && uv run ruff format .` before pushing. CI will fail if the formatter modifies any files. - -### 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. - -## What to Read Before Changing - -| Changing... | Read first | -|---|---| -| CLI flags/arguments | `commitizen/cli.py`, `docs/commands/.md`, `tests/test_cli/` | -| 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` | -| 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) | +## Python -## Coding Guidelines +- Write the Python code with strict type hinting +- Prefer `enum.StrEnum` to represent states +- Preserve public behavior and CLI UX (`commitizen/cli.py`), no breaking changes to APIs, CLI flags, or exit codes unless explicitly requested. +- Errors: Prefer `commitizen/exceptions.py` error types; keep messages clear for CLI users. -- **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. -- **Testing**: Follow the Arrange, Act, Assert (AAA) pattern. Visually separate these phases with blank lines or comments. +### Testing -## When Unsure +You MUST exclusively use `pytest`. +You MUST adhere to the following standards: -- 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. +- Structure: Follow the Arrange, Act, Assert (AAA) pattern. Visually separate these phases with blank lines and comments +- Fixtures over Inline Setup: Use existing `pytest` fixtures to create git projects, files, tags, etc. Do not pollute the test body with complex setup logic if it can be abstracted into a fixture +- Test Documentation: Complex tests must include a brief docstring explaining the business scenario or edge case being validated, or issue worked on. ## Documentation Guidelines -- 100% Coverage: Every new module, class, method, and function MUST have a docstring. No exceptions +- ALWAYS document functions and classes +- Use Google Docstring Style with these major modifications: + - NEVER include type hints in the docstring. We rely exclusively on Python's PEP 484 type signatures + - Classes MUST include an example. Any class documentation must contain a brief usage example formatted in markdown + - Class Attributes MUST be documented. The class docstring must document the instance variables initialized in `__init__` within an `Attributes:` section +- If the code is too complex or solutions are discarded, document them in a **Notes** section - Even simple functions or internal helpers require documentation to explain their context - -### Docstring Format: Modified Google Style - -Use Google Docstring Style with these major modifications: - -1. **NEVER include type hints in the docstring.** We rely exclusively on Python's PEP 484 type signatures. -2. **Classes MUST include an example.** Any class documentation must contain a brief usage example formatted in Markdown. -3. **Class Attributes MUST be documented.** The class docstring must document the instance variables initialized in `__init__` within an `Attributes:` section. - -**Format Rules:** - -- `Args:`, `Returns:`, and `Attributes:` sections must describe the _semantic meaning_ and _constraints_ of the variables, not their types. -- Omit the type in the lists (e.g., use `user_id: The ID of the user`, NOT `user_id (int): The ID of the user`). - -### Content Focus: The "What" and "Why" - -Code explains _how_. Your docstrings must explain _what_ and _why_. - -When documenting, you may include: - -1. **The Core Intent** What business or technical rule is this solving? -2. **Considerations:** Architectural choices. Why was this approach chosen over the obvious alternative? Why the complexity (if introduced)? -3. **Discarded Approaches:** If a simpler method wasn't used (e.g., avoiding an ORM feature for raw SQL, or caching strategies), explain what was discarded and why. -4. **Edge Cases & Gotchas:** What weird scenarios does this code handle? - -In the end, the reader must understand the intention behind the decisions, to avoid making the same mistakes.