multicz 0.3.0__tar.gz → 0.5.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.
- multicz-0.5.0/PKG-INFO +92 -0
- multicz-0.5.0/README.md +64 -0
- {multicz-0.3.0 → multicz-0.5.0}/pyproject.toml +7 -1
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/__init__.py +4 -1
- multicz-0.5.0/src/multicz/__main__.py +6 -0
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/changelog.py +78 -37
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/cli.py +47 -21
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/commits.py +4 -1
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/components.py +3 -0
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/config.py +30 -4
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/debian.py +5 -2
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/discovery.py +13 -10
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/planner.py +9 -6
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/state.py +4 -1
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/validation.py +6 -3
- {multicz-0.3.0 → multicz-0.5.0}/src/multicz/writers.py +65 -6
- multicz-0.3.0/PKG-INFO +0 -1651
- multicz-0.3.0/README.md +0 -1623
- multicz-0.3.0/src/multicz/__main__.py +0 -3
multicz-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: multicz
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits.
|
|
5
|
+
Keywords: semver,monorepo,helm,conventional-commits,release,versioning
|
|
6
|
+
Author: Chris
|
|
7
|
+
Author-email: Chris <goabonga@pm.me>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
16
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
17
|
+
Requires-Dist: typer>=0.12
|
|
18
|
+
Requires-Dist: pydantic>=2.7
|
|
19
|
+
Requires-Dist: tomlkit>=0.13
|
|
20
|
+
Requires-Dist: ruamel-yaml>=0.18
|
|
21
|
+
Requires-Dist: pathspec>=0.12
|
|
22
|
+
Requires-Dist: packaging>=24.0
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Project-URL: Homepage, https://github.com/goabonga/multicz
|
|
25
|
+
Project-URL: Repository, https://github.com/goabonga/multicz
|
|
26
|
+
Project-URL: Issues, https://github.com/goabonga/multicz/issues
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# multicz
|
|
30
|
+
|
|
31
|
+
Multi-component versioning for monorepos. Bump a Python app, its Docker image,
|
|
32
|
+
and the Helm chart that deploys it from a single conventional-commit history -
|
|
33
|
+
each with its own version line and its own git tag.
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="https://github.com/goabonga/multicz/raw/main/docs/demo.gif" alt="multicz demo" width="720">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv tool install multicz # or: pipx install multicz / pip install multicz
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# 1. scaffold a multicz.toml in the repo root
|
|
49
|
+
multicz init
|
|
50
|
+
|
|
51
|
+
# 2. see what would bump from the current commit window
|
|
52
|
+
multicz changed
|
|
53
|
+
multicz explain api
|
|
54
|
+
|
|
55
|
+
# 3. apply the bump (writes version files + changelog, commits, tags)
|
|
56
|
+
multicz bump --commit --tag
|
|
57
|
+
|
|
58
|
+
# 4. ship it
|
|
59
|
+
git push --follow-tags
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For multi-component setups (api + chart, app + image + helm, ...), declare
|
|
63
|
+
each component's paths in `multicz.toml` and let mirrors and triggers cascade
|
|
64
|
+
related versions. See the [docs](https://goabonga.github.io/multicz/) for the
|
|
65
|
+
full configuration reference.
|
|
66
|
+
|
|
67
|
+
## What it does
|
|
68
|
+
|
|
69
|
+
- **Per-component versions** - each component has its own version line and
|
|
70
|
+
its own git tag (`api-v1.2.0`, `chart-v0.5.0`).
|
|
71
|
+
- **Conventional-commit driven** - `feat:` → minor, `fix:` → patch,
|
|
72
|
+
`BREAKING CHANGE:` → major. Scopes route the bump to the right component.
|
|
73
|
+
- **Mirrors and triggers** - bump api `1.2.0` → `1.3.0` and the Helm chart's
|
|
74
|
+
`appVersion` follows; the chart's own version cascades a patch.
|
|
75
|
+
- **No network, no auto-updates.** Pure `git` + filesystem. Same input yields
|
|
76
|
+
the same plan byte-for-byte.
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
Published at **<https://goabonga.github.io/multicz/>**:
|
|
81
|
+
|
|
82
|
+
- [Get started](https://goabonga.github.io/multicz/get-started/) - install, minimal config, first bump
|
|
83
|
+
- [Concepts](https://goabonga.github.io/multicz/concepts/) - components, mirrors, triggers, cascades, bump policies
|
|
84
|
+
- [Configuration](https://goabonga.github.io/multicz/configuration/) - full `multicz.toml` reference
|
|
85
|
+
- [CLI](https://goabonga.github.io/multicz/cli/) - every command and flag
|
|
86
|
+
- [Recipes](https://goabonga.github.io/multicz/recipes/) - FastAPI + Helm walkthrough, CI matrix gating, release candidates
|
|
87
|
+
- [Why multicz?](https://goabonga.github.io/multicz/why/) - vs. semantic-release, Commitizen, Changesets, bump-my-version
|
|
88
|
+
- [Security](https://goabonga.github.io/multicz/security/) - guarantees and CI hardening
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
[MIT](https://github.com/goabonga/multicz/blob/main/LICENSE)
|
multicz-0.5.0/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# multicz
|
|
2
|
+
|
|
3
|
+
Multi-component versioning for monorepos. Bump a Python app, its Docker image,
|
|
4
|
+
and the Helm chart that deploys it from a single conventional-commit history -
|
|
5
|
+
each with its own version line and its own git tag.
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://github.com/goabonga/multicz/raw/main/docs/demo.gif" alt="multicz demo" width="720">
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv tool install multicz # or: pipx install multicz / pip install multicz
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. scaffold a multicz.toml in the repo root
|
|
21
|
+
multicz init
|
|
22
|
+
|
|
23
|
+
# 2. see what would bump from the current commit window
|
|
24
|
+
multicz changed
|
|
25
|
+
multicz explain api
|
|
26
|
+
|
|
27
|
+
# 3. apply the bump (writes version files + changelog, commits, tags)
|
|
28
|
+
multicz bump --commit --tag
|
|
29
|
+
|
|
30
|
+
# 4. ship it
|
|
31
|
+
git push --follow-tags
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For multi-component setups (api + chart, app + image + helm, ...), declare
|
|
35
|
+
each component's paths in `multicz.toml` and let mirrors and triggers cascade
|
|
36
|
+
related versions. See the [docs](https://goabonga.github.io/multicz/) for the
|
|
37
|
+
full configuration reference.
|
|
38
|
+
|
|
39
|
+
## What it does
|
|
40
|
+
|
|
41
|
+
- **Per-component versions** - each component has its own version line and
|
|
42
|
+
its own git tag (`api-v1.2.0`, `chart-v0.5.0`).
|
|
43
|
+
- **Conventional-commit driven** - `feat:` → minor, `fix:` → patch,
|
|
44
|
+
`BREAKING CHANGE:` → major. Scopes route the bump to the right component.
|
|
45
|
+
- **Mirrors and triggers** - bump api `1.2.0` → `1.3.0` and the Helm chart's
|
|
46
|
+
`appVersion` follows; the chart's own version cascades a patch.
|
|
47
|
+
- **No network, no auto-updates.** Pure `git` + filesystem. Same input yields
|
|
48
|
+
the same plan byte-for-byte.
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
Published at **<https://goabonga.github.io/multicz/>**:
|
|
53
|
+
|
|
54
|
+
- [Get started](https://goabonga.github.io/multicz/get-started/) - install, minimal config, first bump
|
|
55
|
+
- [Concepts](https://goabonga.github.io/multicz/concepts/) - components, mirrors, triggers, cascades, bump policies
|
|
56
|
+
- [Configuration](https://goabonga.github.io/multicz/configuration/) - full `multicz.toml` reference
|
|
57
|
+
- [CLI](https://goabonga.github.io/multicz/cli/) - every command and flag
|
|
58
|
+
- [Recipes](https://goabonga.github.io/multicz/recipes/) - FastAPI + Helm walkthrough, CI matrix gating, release candidates
|
|
59
|
+
- [Why multicz?](https://goabonga.github.io/multicz/why/) - vs. semantic-release, Commitizen, Changesets, bump-my-version
|
|
60
|
+
- [Security](https://goabonga.github.io/multicz/security/) - guarantees and CI hardening
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
[MIT](https://github.com/goabonga/multicz/blob/main/LICENSE)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
1
4
|
[project]
|
|
2
5
|
name = "multicz"
|
|
3
|
-
version = "0.
|
|
6
|
+
version = "0.5.0"
|
|
4
7
|
description = "Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits."
|
|
5
8
|
readme = "README.md"
|
|
6
9
|
authors = [
|
|
@@ -46,6 +49,9 @@ dev = [
|
|
|
46
49
|
"pytest-cov>=5.0",
|
|
47
50
|
"ruff>=0.6",
|
|
48
51
|
]
|
|
52
|
+
docs = [
|
|
53
|
+
"zensical>=0.0.39",
|
|
54
|
+
]
|
|
49
55
|
|
|
50
56
|
[tool.ruff]
|
|
51
57
|
line-length = 100
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
1
4
|
"""Render and insert per-component CHANGELOG.md sections.
|
|
2
5
|
|
|
3
6
|
A *section* is the chunk of markdown describing a single release for one
|
|
@@ -16,8 +19,8 @@ component:
|
|
|
16
19
|
The list of section buckets and their titles is configurable via
|
|
17
20
|
``ProjectSettings.changelog_sections`` so each project can pick its own
|
|
18
21
|
vocabulary (Features/Fixes vs. keep-a-changelog's Added/Changed/Fixed,
|
|
19
|
-
etc.). Commits whose type matches no section are silently dropped
|
|
20
|
-
the changelog focused on user-visible changes
|
|
22
|
+
etc.). Commits whose type matches no section are silently dropped - keep
|
|
23
|
+
the changelog focused on user-visible changes - unless
|
|
21
24
|
``other_section_title`` is set, which buckets them under that title.
|
|
22
25
|
|
|
23
26
|
When written into an existing file the new section lands directly after
|
|
@@ -42,10 +45,22 @@ class CascadeEntry:
|
|
|
42
45
|
"""A non-commit reason for a bump (mirror or trigger), surfaced in
|
|
43
46
|
the changelog so cascade-only releases describe what made them
|
|
44
47
|
happen instead of rendering ``_No notable changes._``.
|
|
48
|
+
|
|
49
|
+
``section`` and ``format`` are optional per-entry overrides for the
|
|
50
|
+
default ``cascade_title`` / ``cascade_format`` passed to
|
|
51
|
+
:func:`render_body`. When ``section`` matches an existing
|
|
52
|
+
commit-driven section (``Features``, ``Fixes``, ``Breaking
|
|
53
|
+
changes``, ...), the cascade line is appended to that section's
|
|
54
|
+
bucket. Otherwise it creates a new section under that name. An
|
|
55
|
+
explicit empty-string ``section`` (``""``) is **not** the disable
|
|
56
|
+
switch — to disable the cascade rendering entirely, leave
|
|
57
|
+
``section=None`` and pass ``cascade_title=""`` to ``render_body``.
|
|
45
58
|
"""
|
|
46
59
|
|
|
47
60
|
upstream: str
|
|
48
61
|
upstream_version: str
|
|
62
|
+
section: str | None = None
|
|
63
|
+
format: str | None = None
|
|
49
64
|
|
|
50
65
|
_PREAMBLE = (
|
|
51
66
|
"# Changelog\n"
|
|
@@ -79,23 +94,32 @@ def render_body(
|
|
|
79
94
|
"""
|
|
80
95
|
sections = list(sections) if sections is not None else _default_changelog_sections()
|
|
81
96
|
relevant = [c for c in commits if c.is_conventional]
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
|
|
98
|
+
# Group cascades by their *resolved* section name. Per-entry overrides
|
|
99
|
+
# take precedence over the global cascade_title; an empty resolved
|
|
100
|
+
# section name means "drop this entry" (preserves the legacy
|
|
101
|
+
# cascade_title="" disable switch when no per-entry section is set).
|
|
102
|
+
cascade_groups: dict[str, list[str]] = {}
|
|
103
|
+
if cascades:
|
|
84
104
|
for entry in cascades:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
section_name = entry.section if entry.section is not None else cascade_title
|
|
106
|
+
if not section_name:
|
|
107
|
+
continue
|
|
108
|
+
fmt = entry.format if entry.format is not None else cascade_format
|
|
109
|
+
line = fmt.format(
|
|
110
|
+
upstream=entry.upstream,
|
|
111
|
+
upstream_version=entry.upstream_version,
|
|
90
112
|
)
|
|
91
|
-
|
|
113
|
+
cascade_groups.setdefault(section_name, []).append(line)
|
|
114
|
+
|
|
115
|
+
if not relevant and not cascade_groups:
|
|
92
116
|
return "_No notable changes._\n"
|
|
93
117
|
|
|
94
118
|
breaking: list[Commit] = []
|
|
95
119
|
if breaking_title:
|
|
96
120
|
breaking = [c for c in relevant if c.breaking]
|
|
97
121
|
|
|
98
|
-
|
|
122
|
+
commit_buckets: dict[str, list[Commit]] = {}
|
|
99
123
|
breaking_set = {id(c) for c in breaking}
|
|
100
124
|
for section in sections:
|
|
101
125
|
type_set = {t.lower() for t in section.types}
|
|
@@ -104,7 +128,7 @@ def render_body(
|
|
|
104
128
|
if id(c) not in breaking_set and c.type.lower() in type_set
|
|
105
129
|
]
|
|
106
130
|
if items:
|
|
107
|
-
|
|
131
|
+
commit_buckets[section.title] = items
|
|
108
132
|
|
|
109
133
|
if other_title:
|
|
110
134
|
claimed = {t.lower() for s in sections for t in s.types}
|
|
@@ -113,35 +137,52 @@ def render_body(
|
|
|
113
137
|
if id(c) not in breaking_set and c.type.lower() not in claimed
|
|
114
138
|
]
|
|
115
139
|
if leftovers:
|
|
116
|
-
|
|
140
|
+
commit_buckets[other_title] = leftovers
|
|
117
141
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
def _commit_line(commit: Commit) -> str:
|
|
143
|
+
scope = f"**{commit.scope}**: " if commit.scope else ""
|
|
144
|
+
return f"{scope}{commit.subject} (`{commit.sha[:7]}`)"
|
|
145
|
+
|
|
146
|
+
# Build the final ordered list of (title, lines). For each
|
|
147
|
+
# commit-driven section we render the commits first, then append any
|
|
148
|
+
# cascade lines targeting that same title — so a mirror routed to
|
|
149
|
+
# ``Features`` lands at the bottom of the existing ``### Features``
|
|
150
|
+
# bucket rather than creating a parallel one.
|
|
151
|
+
ordered: list[tuple[str, list[str]]] = []
|
|
152
|
+
|
|
153
|
+
if breaking:
|
|
154
|
+
merged = [_commit_line(c) for c in breaking]
|
|
155
|
+
merged.extend(cascade_groups.pop(breaking_title, []))
|
|
156
|
+
ordered.append((breaking_title, merged))
|
|
126
157
|
|
|
127
|
-
|
|
158
|
+
for section in sections:
|
|
159
|
+
if section.title in commit_buckets:
|
|
160
|
+
merged = [_commit_line(c) for c in commit_buckets[section.title]]
|
|
161
|
+
merged.extend(cascade_groups.pop(section.title, []))
|
|
162
|
+
ordered.append((section.title, merged))
|
|
163
|
+
|
|
164
|
+
if other_title and other_title in commit_buckets:
|
|
165
|
+
merged = [_commit_line(c) for c in commit_buckets[other_title]]
|
|
166
|
+
merged.extend(cascade_groups.pop(other_title, []))
|
|
167
|
+
ordered.append((other_title, merged))
|
|
168
|
+
|
|
169
|
+
# Whatever cascade sections are left have no matching commit bucket —
|
|
170
|
+
# render them after the commit-driven sections in their original
|
|
171
|
+
# insertion order.
|
|
172
|
+
for title, lines in cascade_groups.items():
|
|
173
|
+
ordered.append((title, list(lines)))
|
|
174
|
+
|
|
175
|
+
if not ordered:
|
|
128
176
|
return "_No notable changes._\n"
|
|
129
177
|
|
|
130
|
-
|
|
178
|
+
out: list[str] = []
|
|
131
179
|
for title, items in ordered:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
for
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if cascade_lines:
|
|
139
|
-
lines.append(f"### {cascade_title}")
|
|
140
|
-
lines.append("")
|
|
141
|
-
for entry in cascade_lines:
|
|
142
|
-
lines.append(f"- {entry}")
|
|
143
|
-
lines.append("")
|
|
144
|
-
return "\n".join(lines)
|
|
180
|
+
out.append(f"### {title}")
|
|
181
|
+
out.append("")
|
|
182
|
+
for item in items:
|
|
183
|
+
out.append(f"- {item}")
|
|
184
|
+
out.append("")
|
|
185
|
+
return "\n".join(out)
|
|
145
186
|
|
|
146
187
|
|
|
147
188
|
def render_section(
|
|
@@ -223,7 +264,7 @@ def update_changelog_file(
|
|
|
223
264
|
"""Render a new section and merge it into ``path`` (creating the file if needed).
|
|
224
265
|
|
|
225
266
|
``drop_prereleases=True`` removes any prior ``## [<version>-<pre>.<n>]``
|
|
226
|
-
sections from the file before inserting the new release section
|
|
267
|
+
sections from the file before inserting the new release section -
|
|
227
268
|
used by the ``promote`` finalize strategy.
|
|
228
269
|
"""
|
|
229
270
|
section = render_section(
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
1
4
|
"""Command line interface for multicz."""
|
|
2
5
|
|
|
3
6
|
from __future__ import annotations
|
|
@@ -64,7 +67,7 @@ err = Console(stderr=True)
|
|
|
64
67
|
|
|
65
68
|
|
|
66
69
|
_BARE_CONFIG = """\
|
|
67
|
-
# multicz.toml
|
|
70
|
+
# multicz.toml - generic stub. Edit paths and bump_files to match your repo.
|
|
68
71
|
# Run `multicz init` (without --bare) to scan the working tree and generate
|
|
69
72
|
# a config tailored to the manifests it actually contains.
|
|
70
73
|
|
|
@@ -130,7 +133,7 @@ def init(
|
|
|
130
133
|
``charts/*/Chart.yaml``, ``package.json``, ``Cargo.toml``, ``go.mod``,
|
|
131
134
|
``gradle.properties`` and ``debian/changelog``; one component is
|
|
132
135
|
emitted per detected manifest. ``--bare`` writes a generic
|
|
133
|
-
single-component stub instead
|
|
136
|
+
single-component stub instead - useful when bootstrapping a brand
|
|
134
137
|
new repo.
|
|
135
138
|
|
|
136
139
|
\b
|
|
@@ -277,7 +280,7 @@ def _append_step_summary(path: Path, lines: list[str]) -> None:
|
|
|
277
280
|
|
|
278
281
|
Mirrors GitHub Actions' ``$GITHUB_STEP_SUMMARY`` semantics: each
|
|
279
282
|
step's content is appended; the runner concatenates everything into
|
|
280
|
-
the workflow's run-page summary. Safe to call from local shells
|
|
283
|
+
the workflow's run-page summary. Safe to call from local shells -
|
|
281
284
|
the file is just a text file.
|
|
282
285
|
"""
|
|
283
286
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -306,7 +309,7 @@ def _append_plan_summary(path: Path, plan_obj, *, header: str) -> None:
|
|
|
306
309
|
lines.append("")
|
|
307
310
|
for bump in plan_obj:
|
|
308
311
|
lines.append(
|
|
309
|
-
f"### `{bump.component}`
|
|
312
|
+
f"### `{bump.component}` - {bump.current} → {bump.next} "
|
|
310
313
|
f"({bump.kind})"
|
|
311
314
|
)
|
|
312
315
|
lines.append("")
|
|
@@ -340,7 +343,7 @@ def _append_bump_summary(
|
|
|
340
343
|
tag_index = {t.split("-v", 1)[0] if "-v" in t else None: t for t in tags}
|
|
341
344
|
# Fall back to format string lookup when tag_format isn't `<comp>-v<ver>`.
|
|
342
345
|
for name, info in applied.items():
|
|
343
|
-
tag = tag_index.get(name) or "
|
|
346
|
+
tag = tag_index.get(name) or "-"
|
|
344
347
|
for t in tags:
|
|
345
348
|
if config.tag_format_for(name).format(
|
|
346
349
|
component=name, version=info["next"]
|
|
@@ -442,7 +445,7 @@ def plan_cmd(
|
|
|
442
445
|
force: list[str] = typer.Option(
|
|
443
446
|
None, "--force",
|
|
444
447
|
help="Force-bump <name>:<kind>. Repeatable. Bypasses commit "
|
|
445
|
-
"detection
|
|
448
|
+
"detection - use for manual rebuilds (CVE base image refresh, "
|
|
446
449
|
"weekly artefact rebuild, …).",
|
|
447
450
|
),
|
|
448
451
|
summary: Path = typer.Option(
|
|
@@ -538,7 +541,7 @@ def state(
|
|
|
538
541
|
repo, config = _load()
|
|
539
542
|
if config.project.state_file is None:
|
|
540
543
|
err.print(
|
|
541
|
-
"[red]no state_file configured[/]
|
|
544
|
+
"[red]no state_file configured[/] - set "
|
|
542
545
|
"[bold][project].state_file[/] in multicz.toml"
|
|
543
546
|
)
|
|
544
547
|
raise typer.Exit(code=1)
|
|
@@ -577,7 +580,7 @@ def changed(
|
|
|
577
580
|
None, "--since",
|
|
578
581
|
help="Reference to compare against (e.g. origin/main, HEAD~5). "
|
|
579
582
|
"When omitted, each component is compared against its own "
|
|
580
|
-
"last tag
|
|
583
|
+
"last tag - same window as the planner uses for bumps.",
|
|
581
584
|
),
|
|
582
585
|
output: str = typer.Option(
|
|
583
586
|
"text", "--output", "-o", help="text | json",
|
|
@@ -605,7 +608,7 @@ def changed(
|
|
|
605
608
|
component: ${{ fromJson(needs.detect.outputs.changed) }}
|
|
606
609
|
|
|
607
610
|
Without --since, the answer is per-component (same window as the
|
|
608
|
-
planner). With --since, every component shares the reference
|
|
611
|
+
planner). With --since, every component shares the reference -
|
|
609
612
|
ideal for "what changed in this PR vs main".
|
|
610
613
|
|
|
611
614
|
Release commits matching ``project.release_commit_pattern`` are
|
|
@@ -744,7 +747,7 @@ def release_notes_cmd(
|
|
|
744
747
|
notes.
|
|
745
748
|
|
|
746
749
|
\b
|
|
747
|
-
Default (notes for the upcoming bump
|
|
750
|
+
Default (notes for the upcoming bump - same set as `plan`):
|
|
748
751
|
multicz release-notes api
|
|
749
752
|
multicz release-notes --all
|
|
750
753
|
|
|
@@ -948,7 +951,7 @@ def explain(
|
|
|
948
951
|
bump = plan_obj.bumps.get(component)
|
|
949
952
|
if bump is None:
|
|
950
953
|
console.print(
|
|
951
|
-
f"[bold]{component}[/]: [dim]no bump pending
|
|
954
|
+
f"[bold]{component}[/]: [dim]no bump pending - "
|
|
952
955
|
"no relevant commits since the last tag[/]"
|
|
953
956
|
)
|
|
954
957
|
return
|
|
@@ -1013,7 +1016,7 @@ def _porcelain_paths(repo: Path) -> set[str]:
|
|
|
1013
1016
|
|
|
1014
1017
|
Used to identify candidate paths to hash before/after running
|
|
1015
1018
|
``post_bump`` hooks. A pure set diff would miss a file that's
|
|
1016
|
-
dirty both before and after with different content
|
|
1019
|
+
dirty both before and after with different content - the
|
|
1017
1020
|
canonical case being ``uv run`` itself silently re-syncing
|
|
1018
1021
|
``uv.lock`` before multicz even gets to run.
|
|
1019
1022
|
"""
|
|
@@ -1108,7 +1111,7 @@ def _component_relevant_commits(
|
|
|
1108
1111
|
(project + component, union) are skipped entirely.
|
|
1109
1112
|
|
|
1110
1113
|
When ``since_stable`` is True, the range starts at the previous
|
|
1111
|
-
*stable* tag instead
|
|
1114
|
+
*stable* tag instead - used by the ``consolidate`` and ``promote``
|
|
1112
1115
|
finalize strategies.
|
|
1113
1116
|
"""
|
|
1114
1117
|
import re
|
|
@@ -1217,10 +1220,10 @@ def _release_commit_message(
|
|
|
1217
1220
|
|
|
1218
1221
|
Available placeholders:
|
|
1219
1222
|
|
|
1220
|
-
* ``{summary}``
|
|
1221
|
-
* ``{components}``
|
|
1222
|
-
* ``{body}``
|
|
1223
|
-
* ``{count}``
|
|
1223
|
+
* ``{summary}`` - ``api 1.2.0 -> 1.3.0, chart 0.4.0 -> 0.5.0``
|
|
1224
|
+
* ``{components}`` - ``api v1.3.0, chart v0.5.0`` (versions only, ``v`` prefixed)
|
|
1225
|
+
* ``{body}`` - bullet list with kind annotations
|
|
1226
|
+
* ``{count}`` - number of components bumped
|
|
1224
1227
|
|
|
1225
1228
|
Literal ``{`` and ``}`` in a template should be escaped as ``{{`` / ``}}``.
|
|
1226
1229
|
"""
|
|
@@ -1284,12 +1287,12 @@ def bump(
|
|
|
1284
1287
|
None, "--commit-message", "-m",
|
|
1285
1288
|
help="Verbatim release commit message (overrides the project's "
|
|
1286
1289
|
"release_commit_message template). Like 'git commit -m', no "
|
|
1287
|
-
"placeholders are expanded
|
|
1290
|
+
"placeholders are expanded - the string is used as-is.",
|
|
1288
1291
|
),
|
|
1289
1292
|
force: list[str] = typer.Option(
|
|
1290
1293
|
None, "--force",
|
|
1291
1294
|
help="Force-bump <name>:<kind>. Repeatable. Bypasses commit "
|
|
1292
|
-
"detection
|
|
1295
|
+
"detection - use for manual rebuilds (e.g. weekly base "
|
|
1293
1296
|
"image refresh: `--force api:patch`).",
|
|
1294
1297
|
),
|
|
1295
1298
|
sign: bool = typer.Option(
|
|
@@ -1328,7 +1331,7 @@ def bump(
|
|
|
1328
1331
|
console.print_json(data={"bumps": {}})
|
|
1329
1332
|
else:
|
|
1330
1333
|
console.print(
|
|
1331
|
-
"[dim]no bumps pending
|
|
1334
|
+
"[dim]no bumps pending - "
|
|
1332
1335
|
"use [bold]--force <name>:<kind>[/] for a manual bump[/]"
|
|
1333
1336
|
)
|
|
1334
1337
|
return
|
|
@@ -1390,10 +1393,33 @@ def bump(
|
|
|
1390
1393
|
upstream_planned = plan.bumps.get(reason.upstream)
|
|
1391
1394
|
if upstream_planned is None:
|
|
1392
1395
|
continue
|
|
1396
|
+
# When the cascade comes from a mirror, look up
|
|
1397
|
+
# the matching mirror declaration on the upstream
|
|
1398
|
+
# component to pick up its optional
|
|
1399
|
+
# `changelog_section` / `changelog_format`. Trigger
|
|
1400
|
+
# cascades have no such customization handle and
|
|
1401
|
+
# always use the project-level defaults.
|
|
1402
|
+
section_override: str | None = None
|
|
1403
|
+
format_override: str | None = None
|
|
1404
|
+
if isinstance(reason, MirrorReason):
|
|
1405
|
+
upstream_component = config.components.get(
|
|
1406
|
+
reason.upstream
|
|
1407
|
+
)
|
|
1408
|
+
if upstream_component is not None:
|
|
1409
|
+
for mirror in upstream_component.mirrors:
|
|
1410
|
+
if (
|
|
1411
|
+
str(mirror.file) == reason.file
|
|
1412
|
+
and mirror.key == reason.key
|
|
1413
|
+
):
|
|
1414
|
+
section_override = mirror.changelog_section
|
|
1415
|
+
format_override = mirror.changelog_format
|
|
1416
|
+
break
|
|
1393
1417
|
cascade_entries.append(
|
|
1394
1418
|
CascadeEntry(
|
|
1395
1419
|
upstream=reason.upstream,
|
|
1396
1420
|
upstream_version=upstream_planned.next,
|
|
1421
|
+
section=section_override,
|
|
1422
|
+
format=format_override,
|
|
1397
1423
|
)
|
|
1398
1424
|
)
|
|
1399
1425
|
seen_upstreams.add(reason.upstream)
|
|
@@ -1462,7 +1488,7 @@ def bump(
|
|
|
1462
1488
|
# Cargo.toml. Files modified by hooks are auto-detected and folded
|
|
1463
1489
|
# into ``written`` so they ride the release commit.
|
|
1464
1490
|
#
|
|
1465
|
-
# Detection compares content hashes
|
|
1491
|
+
# Detection compares content hashes - not just the dirty-paths set -
|
|
1466
1492
|
# because the entry point is typically ``uv run multicz bump``, and
|
|
1467
1493
|
# ``uv run`` re-syncs the venv (which can rewrite ``uv.lock``) before
|
|
1468
1494
|
# multicz code runs at all. By the time we snapshot, uv.lock is
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
1
4
|
"""Conventional Commit parsing over a git history range.
|
|
2
5
|
|
|
3
6
|
Resolves the latest tag matching a component's tag prefix, lists the commits
|
|
@@ -88,7 +91,7 @@ class Commit:
|
|
|
88
91
|
``minor``: ``feat``.
|
|
89
92
|
``patch``: ``fix``, ``perf``, ``revert``. A revert is a
|
|
90
93
|
user-visible change (something was removed or restored), and a
|
|
91
|
-
patch is the conservative answer
|
|
94
|
+
patch is the conservative answer - the next release isn't a
|
|
92
95
|
feature or breaking change, but it isn't nothing either.
|
|
93
96
|
|
|
94
97
|
Other types (``chore``, ``docs``, ``style``, ``refactor``,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
1
4
|
"""Configuration schema for multicz.
|
|
2
5
|
|
|
3
6
|
The config lives in ``multicz.toml`` at the repo root. It declares one or more
|
|
@@ -48,6 +51,29 @@ class FileKey(BaseModel):
|
|
|
48
51
|
key: str | None = None
|
|
49
52
|
|
|
50
53
|
|
|
54
|
+
class Mirror(FileKey):
|
|
55
|
+
"""A mirror target with optional changelog customization.
|
|
56
|
+
|
|
57
|
+
Inherits ``file`` / ``key`` from :class:`FileKey`. Adds two optional
|
|
58
|
+
fields that customize how the resulting cascade line is rendered in
|
|
59
|
+
the *downstream* component's CHANGELOG when this mirror triggers a
|
|
60
|
+
bump there:
|
|
61
|
+
|
|
62
|
+
* ``changelog_section`` - routes the cascade line to a specific
|
|
63
|
+
section. Can be the title of an existing section
|
|
64
|
+
(``Features``, ``Fixes``, ``Dependencies``, ...) or any custom
|
|
65
|
+
name. When unset, the line falls under the project-level
|
|
66
|
+
``cascade_section_title`` (default: ``Dependencies``).
|
|
67
|
+
* ``changelog_format`` - overrides the default cascade phrase for
|
|
68
|
+
this specific mirror. The template accepts ``{upstream}`` and
|
|
69
|
+
``{upstream_version}`` placeholders. When unset, the project-level
|
|
70
|
+
``cascade_changelog_format`` applies.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
changelog_section: str | None = None
|
|
74
|
+
changelog_format: str | None = None
|
|
75
|
+
|
|
76
|
+
|
|
51
77
|
class Artifact(BaseModel):
|
|
52
78
|
"""A build artifact a component produces.
|
|
53
79
|
|
|
@@ -84,7 +110,7 @@ class DebianSettings(BaseModel):
|
|
|
84
110
|
|
|
85
111
|
The component's version is read from the topmost stanza of
|
|
86
112
|
``changelog`` (default ``debian/changelog``) and a new stanza is
|
|
87
|
-
*prepended* on every bump
|
|
113
|
+
*prepended* on every bump - older stanzas are never rewritten.
|
|
88
114
|
"""
|
|
89
115
|
|
|
90
116
|
model_config = ConfigDict(extra="forbid")
|
|
@@ -103,7 +129,7 @@ class Component(BaseModel):
|
|
|
103
129
|
paths: list[str] = Field(min_length=1)
|
|
104
130
|
exclude_paths: list[str] = Field(default_factory=list)
|
|
105
131
|
bump_files: list[FileKey] = Field(default_factory=list)
|
|
106
|
-
mirrors: list[
|
|
132
|
+
mirrors: list[Mirror] = Field(default_factory=list)
|
|
107
133
|
# depends_on lists upstream components whose bump should cascade into
|
|
108
134
|
# this one. ``triggers`` is kept as a parse-time alias for users who
|
|
109
135
|
# already wrote it that way; both names normalise to ``depends_on``.
|
|
@@ -310,12 +336,12 @@ class Config(BaseModel):
|
|
|
310
336
|
f"component name {name!r} is too long "
|
|
311
337
|
f"(max {COMPONENT_NAME_MAX_LEN} chars). Component names "
|
|
312
338
|
"appear in git tags, file paths, JSON output, and release "
|
|
313
|
-
"notes
|
|
339
|
+
"notes - keep them short."
|
|
314
340
|
)
|
|
315
341
|
if not COMPONENT_NAME_RE.match(name):
|
|
316
342
|
raise ValueError(
|
|
317
343
|
f"invalid component name {name!r}: must match "
|
|
318
|
-
f"{COMPONENT_NAME_RE.pattern}
|
|
344
|
+
f"{COMPONENT_NAME_RE.pattern} - "
|
|
319
345
|
"no slashes, colons, spaces, or path-like characters; "
|
|
320
346
|
"must start and end with a letter or digit. Component "
|
|
321
347
|
"names land in git tags, file paths, JSON output, and "
|