conventional-release 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. conventional_release-0.1.0/.github/dependabot.yml +14 -0
  2. conventional_release-0.1.0/.github/pull_request_template.md +8 -0
  3. conventional_release-0.1.0/.github/workflows/ci.yml +38 -0
  4. conventional_release-0.1.0/.github/workflows/claude-review.yml +174 -0
  5. conventional_release-0.1.0/.github/workflows/pr-title.yml +36 -0
  6. conventional_release-0.1.0/.github/workflows/pr.yml +16 -0
  7. conventional_release-0.1.0/.github/workflows/release.yml +78 -0
  8. conventional_release-0.1.0/.github/workflows/tag.yml +79 -0
  9. conventional_release-0.1.0/.gitignore +11 -0
  10. conventional_release-0.1.0/CHANGELOG.md +34 -0
  11. conventional_release-0.1.0/CONTRIBUTING.md +23 -0
  12. conventional_release-0.1.0/LICENSE +202 -0
  13. conventional_release-0.1.0/Makefile +34 -0
  14. conventional_release-0.1.0/PKG-INFO +207 -0
  15. conventional_release-0.1.0/README.md +181 -0
  16. conventional_release-0.1.0/SECURITY.md +8 -0
  17. conventional_release-0.1.0/pyproject.toml +65 -0
  18. conventional_release-0.1.0/src/conventional_release/__init__.py +8 -0
  19. conventional_release-0.1.0/src/conventional_release/__main__.py +3 -0
  20. conventional_release-0.1.0/src/conventional_release/cli.py +123 -0
  21. conventional_release-0.1.0/src/conventional_release/cliff.py +197 -0
  22. conventional_release-0.1.0/src/conventional_release/config.py +164 -0
  23. conventional_release-0.1.0/src/conventional_release/git.py +68 -0
  24. conventional_release-0.1.0/src/conventional_release/py.typed +0 -0
  25. conventional_release-0.1.0/src/conventional_release/release.py +229 -0
  26. conventional_release-0.1.0/src/conventional_release/semver.py +30 -0
  27. conventional_release-0.1.0/src/conventional_release/versionfiles.py +96 -0
  28. conventional_release-0.1.0/tests/__init__.py +0 -0
  29. conventional_release-0.1.0/tests/conftest.py +50 -0
  30. conventional_release-0.1.0/tests/test_release.py +233 -0
  31. conventional_release-0.1.0/tests/test_versionfiles.py +97 -0
  32. conventional_release-0.1.0/uv.lock +594 -0
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: monthly
7
+ commit-message:
8
+ prefix: ci
9
+ - package-ecosystem: uv
10
+ directory: /
11
+ schedule:
12
+ interval: monthly
13
+ commit-message:
14
+ prefix: build
@@ -0,0 +1,8 @@
1
+ <!-- Title: a conventional commit, e.g. "feat(cli): add --json". It becomes the changelog line. -->
2
+
3
+ ## What and why
4
+
5
+ ## How it was tested
6
+
7
+ <!-- For a breaking change, add a footer:
8
+ BREAKING CHANGE: what users must do differently -->
@@ -0,0 +1,38 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ - uses: astral-sh/setup-uv@v10.2.0
17
+ - run: make lint typecheck
18
+
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ python: ["3.11", "3.12", "3.13", "3.14"]
25
+ steps:
26
+ - uses: actions/checkout@v7
27
+ - uses: astral-sh/setup-uv@v10.2.0
28
+ with:
29
+ python-version: ${{ matrix.python }}
30
+ - run: make tests
31
+
32
+ build:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v7
36
+ - uses: astral-sh/setup-uv@v10.2.0
37
+ - run: uv build
38
+ - run: uvx twine check --strict dist/*
@@ -0,0 +1,174 @@
1
+ name: claude-review
2
+
3
+ # Two jobs, one action:
4
+ # review — every non-draft PR gets an automatic review, posted as inline comments plus one
5
+ # sticky summary.
6
+ # mention — "@claude <question>" (or "@claude review" after a fix push) in a PR/issue comment
7
+ # gets an answer. Maintainers only: see the `if` on that job.
8
+ #
9
+ # Authenticated with a Claude subscription OAuth token (CLAUDE_CODE_OAUTH_TOKEN, from
10
+ # `claude setup-token`). Until that secret exists the jobs skip with a notice instead of failing:
11
+ # a missing reviewer must not turn a PR red.
12
+ #
13
+ # Public-repo safety:
14
+ # - PRs from forks get no secrets on `pull_request`, so they are skipped with the same notice.
15
+ # Never switch this to `pull_request_target`: that hands the token to code from a stranger's fork.
16
+ # - `issue_comment` runs WITH secrets on any PR, forks included, so the mention job only answers
17
+ # comments from the repo owner, members and collaborators.
18
+ # - The reviewer is read-only on the repo (contents: read). It can comment; it can never push.
19
+ #
20
+ # The action refuses to run a workflow file that differs from the copy on the default branch
21
+ # (anti-tamper), so a change to THIS file must be merged on its own before a PR carrying the
22
+ # same change can be reviewed.
23
+
24
+ on:
25
+ pull_request:
26
+ # Not on every push, because each round uses the plan's quota. After fixes, ask again with
27
+ # "@claude review".
28
+ types: [opened, reopened, ready_for_review]
29
+ issue_comment:
30
+ types: [created]
31
+ pull_request_review_comment:
32
+ types: [created]
33
+
34
+ permissions:
35
+ contents: read
36
+ pull-requests: write
37
+ issues: write
38
+ id-token: write
39
+ actions: read
40
+
41
+ # Keyed on event, number and actor. Without the event, the reviewer's own first inline comment
42
+ # (a pull_request_review_comment) cancels the review that is still running. Without the actor,
43
+ # the mention job's progress comment cancels the run that posted it.
44
+ concurrency:
45
+ group: claude-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number }}-${{ github.actor }}
46
+ cancel-in-progress: true
47
+
48
+ jobs:
49
+ review:
50
+ name: claude-review
51
+ if: >
52
+ github.event_name == 'pull_request' &&
53
+ github.event.pull_request.draft == false &&
54
+ !startsWith(github.event.pull_request.title, 'chore(release):') &&
55
+ github.actor != 'dependabot[bot]'
56
+ runs-on: ubuntu-latest
57
+ timeout-minutes: 30
58
+ steps:
59
+ - id: token
60
+ name: Is the reviewer configured?
61
+ env:
62
+ TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
63
+ run: |
64
+ if [ -n "$TOKEN" ]; then
65
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
66
+ else
67
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
68
+ echo "::notice title=claude-review skipped::CLAUDE_CODE_OAUTH_TOKEN is not available (not set, or a PR from a fork)."
69
+ fi
70
+
71
+ - if: steps.token.outputs.enabled == 'true'
72
+ uses: actions/checkout@v7
73
+ with:
74
+ fetch-depth: 0 # the review needs the base branch to diff against
75
+
76
+ - if: steps.token.outputs.enabled == 'true'
77
+ uses: anthropics/claude-code-action@v1
78
+ with:
79
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
80
+ use_sticky_comment: true
81
+ prompt: |
82
+ REPO: ${{ github.repository }}
83
+ PR NUMBER: ${{ github.event.pull_request.number }}
84
+
85
+ You are reviewing a pull request for conventional-release, a Python CLI and a set of
86
+ reusable GitHub workflows that other projects call to write their CHANGELOG, bump
87
+ versions, tag releases and publish them. Other repositories run this code in their
88
+ release pipelines, so a regression breaks releases we cannot see.
89
+
90
+ Read CONTRIBUTING.md and the README first. They define the contract:
91
+ - The CHANGELOG output is byte-compatible with standard-version: `## X.Y.Z (date)`, two
92
+ blank lines, `### Section`, `* **scope:** subject ([#N](…/issues/N)) ([sha](…))`.
93
+ Any change to the rendered shape (cliff.py templates, section order, link format) is
94
+ a breaking change for users, even if the tests were updated to match.
95
+ - The reusable workflows (.github/workflows/tag.yml, pr-title.yml) are a public API
96
+ called as `@v0`: their inputs, outputs (released, version, tag) and default
97
+ tool-spec must not change incompatibly in a minor release.
98
+ - The CLI's commands, flags, exit codes and config keys (.conventional-release.toml
99
+ and [tool.conventional-release]) are public API too.
100
+ - Version files are edited textually, one line only. Comments and formatting elsewhere
101
+ in the file must survive.
102
+ - PR titles follow Conventional Commits. The title becomes the changelog line.
103
+
104
+ Look for, in this order:
105
+ 1. Correctness bugs: version bump rules (breaking → major, feat → minor, fix → patch;
106
+ the first release keeps the version in the file), tag detection after a squash
107
+ merge, git edge cases (no tags, no remote, dirty tree, existing tag or branch).
108
+ 2. Breaking changes to the public API above that the title does not mark with `!` and a
109
+ BREAKING CHANGE footer.
110
+ 3. Workflow security: secrets reaching fork PRs, `pull_request_target`, untrusted
111
+ GitHub expressions (PR titles, comment bodies) interpolated straight into `run:`
112
+ instead of passed through `env:`, permissions wider than the job needs, or
113
+ publishing without trusted publishing.
114
+ 4. New behaviour without a test. Tests build throwaway git repos in tmp_path, see
115
+ tests/conftest.py.
116
+ 5. README drift: a changed command, flag, config key or workflow input that the
117
+ README still documents the old way.
118
+
119
+ Do NOT comment on formatting, import order or line length. ruff, mypy --strict and
120
+ actionlint already gate those in CI. Do not restate the PR description back.
121
+
122
+ Use `gh pr diff` and `gh pr view` to see the change. Post specific findings as inline
123
+ comments on the exact lines (mcp__github_inline_comment__create_inline_comment).
124
+ Then post ONE summary comment via `gh pr comment`: a short verdict, the findings
125
+ ranked by severity, and anything you could not verify. If there is nothing
126
+ significant, say so in two sentences. A short honest review beats a long padded one.
127
+
128
+ Budget: you have a hard cap of 60 tool calls, and the run fails if you exceed it. Start
129
+ from `gh pr diff --name-only` and `gh pr diff`. Read a file in full only when the diff
130
+ alone cannot answer the question. Post the summary comment no later than your 45th
131
+ call, and say what you did not get to. An unposted review is worth nothing.
132
+ claude_args: |
133
+ --max-turns 60
134
+ --allowed-tools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Read,Grep,Glob"
135
+
136
+ mention:
137
+ name: claude-mention
138
+ # Maintainers only: issue_comment runs with secrets even on fork PRs, so anyone else could
139
+ # spend the plan's quota. Never react to the bot's own comments, or it talks to itself forever.
140
+ if: >
141
+ (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
142
+ contains(github.event.comment.body, '@claude') &&
143
+ contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
144
+ github.actor != 'claude[bot]'
145
+ runs-on: ubuntu-latest
146
+ timeout-minutes: 30
147
+ steps:
148
+ - id: token
149
+ name: Is the reviewer configured?
150
+ env:
151
+ TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
152
+ run: |
153
+ if [ -n "$TOKEN" ]; then
154
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
155
+ else
156
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
157
+ echo "::notice title=claude-mention skipped::CLAUDE_CODE_OAUTH_TOKEN is not set."
158
+ fi
159
+
160
+ - if: steps.token.outputs.enabled == 'true'
161
+ uses: actions/checkout@v7
162
+ with:
163
+ fetch-depth: 0
164
+
165
+ - if: steps.token.outputs.enabled == 'true'
166
+ uses: anthropics/claude-code-action@v1
167
+ with:
168
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
169
+ # No prompt means tag mode: it answers the @claude comment in context. "@claude review"
170
+ # is a full review of the head, so it gets the review job's budget. A question gets 20.
171
+ claude_args: |
172
+ --max-turns ${{ contains(github.event.comment.body, '@claude review') && '60' || '20' }}
173
+ --append-system-prompt "You have a hard cap on tool calls (--max-turns) and the run FAILS the moment you exceed it. Work from `gh pr diff --name-only` and `gh pr diff`; read a file in full only when the diff cannot answer the question. Post your answer (for a review request, one summary comment via `gh pr comment`) by three quarters of that budget at the latest, saying plainly what you did not get to."
174
+ --allowed-tools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh issue view:*),Read,Grep,Glob"
@@ -0,0 +1,36 @@
1
+ name: pr-title
2
+
3
+ # Reusable: the PR title becomes the squash commit subject, and that subject becomes the
4
+ # changelog line — so the title must be a conventional commit. Make this check required.
5
+
6
+ on:
7
+ workflow_call:
8
+ inputs:
9
+ tool-spec:
10
+ description: "What `uvx --from` installs, e.g. `conventional-release==0.1.0`"
11
+ type: string
12
+ default: "conventional-release>=0.1,<0.2"
13
+ working-directory:
14
+ description: Directory holding the project's conventional-release config
15
+ type: string
16
+ default: "."
17
+
18
+ jobs:
19
+ pr-title:
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ steps:
24
+ # For the project's configured types; nothing from the PR is executed.
25
+ - uses: actions/checkout@v7
26
+ with:
27
+ persist-credentials: false
28
+
29
+ - uses: astral-sh/setup-uv@v10.2.0
30
+
31
+ - name: Title is a conventional commit
32
+ working-directory: ${{ inputs.working-directory }}
33
+ env:
34
+ TOOL: ${{ inputs.tool-spec }}
35
+ TITLE: ${{ github.event.pull_request.title }}
36
+ run: uvx --from "$TOOL" conventional-release check-title "$TITLE"
@@ -0,0 +1,16 @@
1
+ name: pr
2
+
3
+ # This repo checks its own PR titles with its own checkout of the tool.
4
+
5
+ on:
6
+ pull_request:
7
+ types: [opened, edited, synchronize, reopened]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ pr-title:
14
+ uses: ./.github/workflows/pr-title.yml
15
+ with:
16
+ tool-spec: "."
@@ -0,0 +1,78 @@
1
+ name: release
2
+
3
+ # The project releases itself: `make release` opens the release PR; merging it lands here.
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ tag:
14
+ uses: ./.github/workflows/tag.yml
15
+ permissions:
16
+ contents: write
17
+ with:
18
+ tool-spec: "."
19
+
20
+ build:
21
+ needs: tag
22
+ if: needs.tag.outputs.released == 'true'
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v7
26
+ with:
27
+ ref: ${{ needs.tag.outputs.tag }}
28
+ - uses: astral-sh/setup-uv@v10.2.0
29
+ - run: uv build
30
+ - uses: actions/upload-artifact@v7
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+
35
+ testpypi:
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ environment: testpypi
39
+ permissions:
40
+ id-token: write # trusted publishing, no token stored anywhere
41
+ steps:
42
+ - uses: actions/download-artifact@v8
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
47
+ with:
48
+ repository-url: https://test.pypi.org/legacy/
49
+
50
+ pypi:
51
+ needs: [tag, testpypi]
52
+ runs-on: ubuntu-latest
53
+ environment: pypi # required reviewer: the maintainer
54
+ permissions:
55
+ id-token: write
56
+ steps:
57
+ - uses: actions/download-artifact@v8
58
+ with:
59
+ name: dist
60
+ path: dist/
61
+ - uses: pypa/gh-action-pypi-publish@release/v1
62
+
63
+ # `uses: MdaaaaO/conventional-release/.github/workflows/tag.yml@v0` follows the latest 0.x.
64
+ major-tag:
65
+ needs: [tag, pypi]
66
+ runs-on: ubuntu-latest
67
+ permissions:
68
+ contents: write
69
+ steps:
70
+ - uses: actions/checkout@v7
71
+ with:
72
+ ref: ${{ needs.tag.outputs.tag }}
73
+ - env:
74
+ TAG: ${{ needs.tag.outputs.tag }}
75
+ run: |
76
+ major="${TAG%%.*}"
77
+ git tag -f "$major" "$TAG^{commit}"
78
+ git push -f origin "refs/tags/$major"
@@ -0,0 +1,79 @@
1
+ name: tag
2
+
3
+ # Reusable: the CI half of a release. Call it on every push to the default branch. When the
4
+ # pushed commit is a squash-merged `chore(release): X.Y.Z`, it tags that commit vX.Y.Z and
5
+ # publishes a GitHub Release with the version's CHANGELOG section; otherwise it does nothing.
6
+ #
7
+ # Run your publish jobs *after* it in the same workflow (`needs: tag`, `if: released`): a tag
8
+ # pushed with GITHUB_TOKEN starts no other workflow, so `on: push: tags` would never fire.
9
+ # The caller grants `contents: write`.
10
+
11
+ on:
12
+ workflow_call:
13
+ inputs:
14
+ tool-spec:
15
+ description: "What `uvx --from` installs, e.g. `conventional-release==0.1.0`"
16
+ type: string
17
+ default: "conventional-release>=0.1,<0.2"
18
+ working-directory:
19
+ description: Directory of the project inside the repository
20
+ type: string
21
+ default: "."
22
+ github-release:
23
+ description: Publish a GitHub Release with the changelog section
24
+ type: boolean
25
+ default: true
26
+ outputs:
27
+ released:
28
+ description: "'true' when this run created the tag"
29
+ value: ${{ jobs.tag.outputs.released }}
30
+ version:
31
+ description: The released version, e.g. 1.4.0
32
+ value: ${{ jobs.tag.outputs.version }}
33
+ tag:
34
+ description: The tag, e.g. v1.4.0
35
+ value: ${{ jobs.tag.outputs.tag }}
36
+
37
+ jobs:
38
+ tag:
39
+ runs-on: ubuntu-latest
40
+ permissions:
41
+ contents: write
42
+ outputs:
43
+ released: ${{ steps.detect.outputs.released }}
44
+ version: ${{ steps.detect.outputs.version }}
45
+ tag: ${{ steps.detect.outputs.tag }}
46
+ defaults:
47
+ run:
48
+ working-directory: ${{ inputs.working-directory }}
49
+ env:
50
+ TOOL: ${{ inputs.tool-spec }}
51
+ steps:
52
+ - uses: actions/checkout@v7
53
+ with:
54
+ fetch-depth: 0
55
+
56
+ - uses: astral-sh/setup-uv@v10.2.0
57
+
58
+ - id: detect
59
+ name: Is this a release commit?
60
+ run: uvx --from "$TOOL" conventional-release detect --github-output
61
+
62
+ - name: Tag the release commit
63
+ if: steps.detect.outputs.released == 'true'
64
+ env:
65
+ VERSION: ${{ steps.detect.outputs.version }}
66
+ run: |
67
+ git config user.name "github-actions[bot]"
68
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
69
+ uvx --from "$TOOL" conventional-release tag "$VERSION" --push
70
+
71
+ - name: GitHub Release
72
+ if: steps.detect.outputs.released == 'true' && inputs.github-release
73
+ env:
74
+ GH_TOKEN: ${{ github.token }}
75
+ VERSION: ${{ steps.detect.outputs.version }}
76
+ TAG: ${{ steps.detect.outputs.tag }}
77
+ run: |
78
+ uvx --from "$TOOL" conventional-release notes "$VERSION" > "$RUNNER_TEMP/notes.md"
79
+ gh release create "$TAG" --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md" --verify-tag
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .coverage
7
+ coverage.xml
8
+ htmlcov/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .mypy_cache/
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. Generated by
4
+ [conventional-release](https://github.com/MdaaaaO/conventional-release) from
5
+ [Conventional Commits](https://www.conventionalcommits.org).
6
+
7
+ ## 0.1.0 (2026-09-25)
8
+
9
+
10
+ ### Features
11
+
12
+ * standard-version-style changelog and release PRs from conventional commits ([12737d1](https://github.com/MdaaaaO/conventional-release/commit/12737d1e37eda9b5dc2dcd1e090d178a2af90a64))
13
+
14
+
15
+ ### Documentation
16
+
17
+ * readme, contributing guide and security policy ([3ff7da2](https://github.com/MdaaaaO/conventional-release/commit/3ff7da253f1f06669185f5e7862dc5c771d73ecb))
18
+
19
+
20
+ ### Tests
21
+
22
+ * changelog shape, version files and the release flow on throwaway repos ([a53ac38](https://github.com/MdaaaaO/conventional-release/commit/a53ac38a4933d831520baa8b35e332ab4d1ee91b))
23
+
24
+
25
+ ### Build System
26
+
27
+ * make targets for ci, build and release ([29cad9f](https://github.com/MdaaaaO/conventional-release/commit/29cad9fe7e452941b3bee0dc9eb8ed91b6e58c57))
28
+
29
+
30
+ ### CI
31
+
32
+ * reusable tag and pr-title workflows; the project releases itself to PyPI ([a23b86b](https://github.com/MdaaaaO/conventional-release/commit/a23b86bcb75c1ece841d201128ee123887aacd45))
33
+ * pin astral-sh/setup-uv to v10.2.0 ([#1](https://github.com/MdaaaaO/conventional-release/issues/1)) ([9ba8212](https://github.com/MdaaaaO/conventional-release/commit/9ba821294387fe13173dbfb50cae0ebc731b26dc))
34
+ * claude review on pull requests ([#2](https://github.com/MdaaaaO/conventional-release/issues/2)) ([dbacb35](https://github.com/MdaaaaO/conventional-release/commit/dbacb3580efb2a94378b7b71db0c92cdf56a8b68))
@@ -0,0 +1,23 @@
1
+ # Contributing
2
+
3
+ Thanks for helping. The short version:
4
+
5
+ - **Open an issue first** for anything bigger than a typo, so we agree on the change before you write it.
6
+ - **PR titles are [Conventional Commits](https://www.conventionalcommits.org).** PRs are
7
+ squash-merged, the title becomes the commit, and the commit becomes the changelog line. The
8
+ `pr-title` check enforces this (`feat(cli): add --json`, `fix: …`, `docs: …`). Put `!` after
9
+ the type or scope for a breaking change, and explain it in a `BREAKING CHANGE:` footer in the
10
+ PR description.
11
+ - **`make ci` passes locally** before you push. It runs ruff, `mypy --strict`, pytest with branch
12
+ coverage, and actionlint. You need [uv](https://docs.astral.sh/uv/). Run `make install` once.
13
+ - An automated Claude review comments on each PR from this repo's branches. It is advisory; a
14
+ maintainer decides. Maintainers can ask it again after a fix push with `@claude review`.
15
+ - New behaviour comes with a test. The tests build throwaway git repos in `tmp_path`; see
16
+ `tests/conftest.py`.
17
+
18
+ ## Releasing (maintainers)
19
+
20
+ `make release` (or `make release ARGS=minor`) opens the `chore(release): X.Y.Z` PR. Squash-merge
21
+ it with the title unchanged. `release.yml` then tags the merge commit, publishes the GitHub
22
+ Release, publishes to TestPyPI and then to PyPI (the `pypi` environment waits for approval), and
23
+ moves the `v0` major tag. `make release-dry` shows what would ship.