multicz 1.2.1__tar.gz → 1.3.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-1.2.1 → multicz-1.3.0}/PKG-INFO +1 -1
- {multicz-1.2.1 → multicz-1.3.0}/pyproject.toml +8 -1
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/changelog/markdown.py +34 -1
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/__init__.py +1 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/bump.py +16 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/plan.py +13 -0
- multicz-1.3.0/src/multicz/cli/commands/plugins.py +29 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/release_notes.py +7 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/status.py +10 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/presenters.py +145 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/results.py +6 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/config/models.py +6 -0
- multicz-1.3.0/src/multicz/plugins/__init__.py +47 -0
- multicz-1.3.0/src/multicz/plugins/builtin/__init__.py +9 -0
- multicz-1.3.0/src/multicz/plugins/builtin/deprecation/__init__.py +17 -0
- multicz-1.3.0/src/multicz/plugins/builtin/deprecation/plugin.py +216 -0
- multicz-1.3.0/src/multicz/plugins/builtin/deprecation/scanner.py +142 -0
- multicz-1.3.0/src/multicz/plugins/protocol.py +137 -0
- multicz-1.3.0/src/multicz/plugins/registry.py +90 -0
- multicz-1.3.0/src/multicz/plugins/runner.py +148 -0
- {multicz-1.2.1 → multicz-1.3.0}/README.md +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/__main__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/changelog/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/changelog/bucket.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/changelog/debian.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/_shared.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/artifacts.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/changed.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/changelog.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/check.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/config.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/explain.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/get.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/graph.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/init.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/state.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/cli/commands/validate.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/commits/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/commits/git.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/commits/parse.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/config/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/config/components.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/config/sources.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/_common.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/cargo.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/context.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/debian.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/go.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/gradle.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/helm.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/node.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/python.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/registry.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/discovery/relations.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/_base.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/_common.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/json.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/plain.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/properties.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/regex.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/toml.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/formats/yaml.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/planner/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/planner/build.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/planner/plan.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/planner/reasons.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/state/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/_base.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/_cycle.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/_git.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/bump_files.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/changelog.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/cycles.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/mirrors.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/overlap.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/state.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/validation/versions.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/writers/__init__.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/writers/_base.py +0 -0
- {multicz-1.2.1 → multicz-1.3.0}/src/multicz/writers/debian_changelog.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: multicz
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits.
|
|
5
5
|
Keywords: semver,monorepo,helm,conventional-commits,release,versioning
|
|
6
6
|
Author: Chris
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "multicz"
|
|
6
|
-
version = "1.
|
|
6
|
+
version = "1.3.0"
|
|
7
7
|
description = "Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits."
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
authors = [
|
|
@@ -34,6 +34,13 @@ dependencies = [
|
|
|
34
34
|
[project.scripts]
|
|
35
35
|
multicz = "multicz.cli:app"
|
|
36
36
|
|
|
37
|
+
# Built-in plugins registered via the same entry-point group third-
|
|
38
|
+
# party plugins use. Disable a built-in by setting
|
|
39
|
+
# `[plugins.<name>] enabled = false` in multicz.toml — there is no
|
|
40
|
+
# privileged loader path.
|
|
41
|
+
[project.entry-points."multicz.plugins"]
|
|
42
|
+
deprecation = "multicz.plugins.builtin.deprecation:DeprecationPlugin"
|
|
43
|
+
|
|
37
44
|
[project.urls]
|
|
38
45
|
Homepage = "https://github.com/goabonga/multicz"
|
|
39
46
|
Repository = "https://github.com/goabonga/multicz"
|
|
@@ -35,11 +35,15 @@ from collections.abc import Iterable, Mapping, Sequence
|
|
|
35
35
|
from dataclasses import dataclass
|
|
36
36
|
from datetime import date
|
|
37
37
|
from pathlib import Path
|
|
38
|
+
from typing import TYPE_CHECKING
|
|
38
39
|
|
|
39
40
|
from ..commits import BumpRule, Commit
|
|
40
41
|
from ..config import ChangelogSection, _default_changelog_sections
|
|
41
42
|
from .bucket import bucket_commits
|
|
42
43
|
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from ..plugins import ChangelogEntry as PluginChangelogEntry
|
|
46
|
+
|
|
43
47
|
|
|
44
48
|
@dataclass(frozen=True)
|
|
45
49
|
class CascadeEntry:
|
|
@@ -81,6 +85,7 @@ def render_body(
|
|
|
81
85
|
cascades: Sequence[CascadeEntry] | None = None,
|
|
82
86
|
cascade_title: str = "Dependencies",
|
|
83
87
|
cascade_format: str = "Track `{upstream}` `{upstream_version}`",
|
|
88
|
+
plugin_sections: Sequence[PluginChangelogEntry] | None = None,
|
|
84
89
|
) -> str:
|
|
85
90
|
"""Render the section bodies (no leading H2).
|
|
86
91
|
|
|
@@ -125,7 +130,17 @@ def render_body(
|
|
|
125
130
|
)
|
|
126
131
|
cascade_groups.setdefault(section_name, []).append(line)
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
# Plugin-contributed sections (e.g. "Deprecated", "Removed" from the
|
|
134
|
+
# built-in deprecation plugin) — grouped by section name, merged with
|
|
135
|
+
# any matching commit/cascade bucket.
|
|
136
|
+
plugin_groups: dict[str, list[str]] = {}
|
|
137
|
+
if plugin_sections:
|
|
138
|
+
for entry in plugin_sections:
|
|
139
|
+
if not entry.section or not entry.lines:
|
|
140
|
+
continue
|
|
141
|
+
plugin_groups.setdefault(entry.section, []).extend(entry.lines)
|
|
142
|
+
|
|
143
|
+
if bucketed.is_empty and not cascade_groups and not plugin_groups:
|
|
129
144
|
return "_No notable changes._\n"
|
|
130
145
|
|
|
131
146
|
def _commit_line(commit: Commit) -> str:
|
|
@@ -174,6 +189,20 @@ def render_body(
|
|
|
174
189
|
for title, lines in cascade_groups.items():
|
|
175
190
|
ordered.append((title, list(lines)))
|
|
176
191
|
|
|
192
|
+
# Plugin sections land last (after commits + cascades) so they read
|
|
193
|
+
# as a clear "meta" block — typically Deprecated/Removed notices.
|
|
194
|
+
# If a plugin reuses an existing title (e.g. "Features"), merge
|
|
195
|
+
# rather than create a duplicate H3.
|
|
196
|
+
existing_titles = {title for title, _ in ordered}
|
|
197
|
+
for title, lines in plugin_groups.items():
|
|
198
|
+
if title in existing_titles:
|
|
199
|
+
for entry in ordered:
|
|
200
|
+
if entry[0] == title:
|
|
201
|
+
entry[1].extend(lines)
|
|
202
|
+
break
|
|
203
|
+
else:
|
|
204
|
+
ordered.append((title, list(lines)))
|
|
205
|
+
|
|
177
206
|
if not ordered:
|
|
178
207
|
return "_No notable changes._\n"
|
|
179
208
|
|
|
@@ -199,6 +228,7 @@ def render_section(
|
|
|
199
228
|
cascades: Sequence[CascadeEntry] | None = None,
|
|
200
229
|
cascade_title: str = "Dependencies",
|
|
201
230
|
cascade_format: str = "Track `{upstream}` `{upstream_version}`",
|
|
231
|
+
plugin_sections: Sequence[PluginChangelogEntry] | None = None,
|
|
202
232
|
) -> str:
|
|
203
233
|
"""Render the markdown for a single release section."""
|
|
204
234
|
when = (today or date.today()).isoformat()
|
|
@@ -211,6 +241,7 @@ def render_section(
|
|
|
211
241
|
cascades=cascades,
|
|
212
242
|
cascade_title=cascade_title,
|
|
213
243
|
cascade_format=cascade_format,
|
|
244
|
+
plugin_sections=plugin_sections,
|
|
214
245
|
)
|
|
215
246
|
return f"## [{version}] - {when}\n\n" + body
|
|
216
247
|
|
|
@@ -265,6 +296,7 @@ def update_changelog_file(
|
|
|
265
296
|
cascades: Sequence[CascadeEntry] | None = None,
|
|
266
297
|
cascade_title: str = "Dependencies",
|
|
267
298
|
cascade_format: str = "Track `{upstream}` `{upstream_version}`",
|
|
299
|
+
plugin_sections: Sequence[PluginChangelogEntry] | None = None,
|
|
268
300
|
) -> None:
|
|
269
301
|
"""Render a new section and merge it into ``path`` (creating the file if needed).
|
|
270
302
|
|
|
@@ -283,6 +315,7 @@ def update_changelog_file(
|
|
|
283
315
|
cascades=cascades,
|
|
284
316
|
cascade_title=cascade_title,
|
|
285
317
|
cascade_format=cascade_format,
|
|
318
|
+
plugin_sections=plugin_sections,
|
|
286
319
|
)
|
|
287
320
|
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
288
321
|
if drop_prereleases:
|
|
@@ -15,6 +15,7 @@ import typer
|
|
|
15
15
|
from ...changelog import update_changelog_file
|
|
16
16
|
from ...config import ComponentMatcher
|
|
17
17
|
from ...formats import write_value
|
|
18
|
+
from ...plugins import has_errors, run_enrich_changelog, run_post_plan
|
|
18
19
|
from ...state import (
|
|
19
20
|
STATE_SCHEMA_VERSION,
|
|
20
21
|
ComponentState,
|
|
@@ -230,6 +231,15 @@ def bump(
|
|
|
230
231
|
presenters.render_bump_empty(output=output)
|
|
231
232
|
return
|
|
232
233
|
|
|
234
|
+
# Plugin hook — gate the bump on installed plugins (deprecation
|
|
235
|
+
# removal policy, license check, etc.). Errors abort here BEFORE
|
|
236
|
+
# any write touches the repo. Warnings + infos surface but proceed.
|
|
237
|
+
violations = run_post_plan(config, repo, plan)
|
|
238
|
+
if violations:
|
|
239
|
+
presenters.render_plugin_violations(violations, output=output)
|
|
240
|
+
if has_errors(violations):
|
|
241
|
+
raise typer.Exit(code=1)
|
|
242
|
+
|
|
233
243
|
matcher = ComponentMatcher(config.components)
|
|
234
244
|
applied: list[AppliedBump] = []
|
|
235
245
|
written: list[Path] = []
|
|
@@ -268,6 +278,11 @@ def bump(
|
|
|
268
278
|
# this is the only thing that explains *why* the
|
|
269
279
|
# release exists.
|
|
270
280
|
cascade_entries = _cascade_entries_for(planned, plan, config)
|
|
281
|
+
# Let plugins (e.g. deprecation) contribute custom sections
|
|
282
|
+
# — Deprecated / Removed / SECURITY / etc.
|
|
283
|
+
plugin_entries = run_enrich_changelog(
|
|
284
|
+
config, repo, plan, planned.component
|
|
285
|
+
)
|
|
271
286
|
changelog_path = repo / comp.changelog
|
|
272
287
|
update_changelog_file(
|
|
273
288
|
changelog_path,
|
|
@@ -281,6 +296,7 @@ def bump(
|
|
|
281
296
|
cascades=cascade_entries,
|
|
282
297
|
cascade_title=config.project.cascade_section_title,
|
|
283
298
|
cascade_format=config.project.cascade_changelog_format,
|
|
299
|
+
plugin_sections=plugin_entries,
|
|
284
300
|
)
|
|
285
301
|
if changelog_path not in written:
|
|
286
302
|
written.append(changelog_path)
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
12
|
+
from ...plugins import run_post_plan, run_status_lines
|
|
12
13
|
from .. import app, err, presenters
|
|
13
14
|
from .._shared import (
|
|
14
15
|
_build_plan_or_exit,
|
|
@@ -85,3 +86,15 @@ def plan_cmd(
|
|
|
85
86
|
presenters.append_plan_summary(summary, plan_obj, header="Release plan")
|
|
86
87
|
|
|
87
88
|
presenters.render_plan(plan_obj, config, output=output)
|
|
89
|
+
|
|
90
|
+
# Preview the post_plan hook so users see violations BEFORE running
|
|
91
|
+
# `multicz bump` — same contract as the real gate, but never aborts.
|
|
92
|
+
violations = run_post_plan(config, repo, plan_obj)
|
|
93
|
+
if violations:
|
|
94
|
+
presenters.render_plugin_violations(violations, output=output)
|
|
95
|
+
|
|
96
|
+
# status_lines surface actionable advice from each plugin
|
|
97
|
+
# (e.g. "remove deprecation X before bumping to 3.0").
|
|
98
|
+
advice = run_status_lines(config, repo, plan_obj)
|
|
99
|
+
if advice:
|
|
100
|
+
presenters.render_plugin_advice(advice, output=output)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
4
|
+
"""``multicz plugins`` - list discovered plugins and their config state."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ...plugins import DEFAULT_REGISTRY
|
|
11
|
+
from .. import app, presenters
|
|
12
|
+
from .._shared import _load
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command(name="plugins")
|
|
16
|
+
def plugins_cmd(
|
|
17
|
+
output: str = typer.Option("text", "--output", "-o", help="text | json"),
|
|
18
|
+
) -> None:
|
|
19
|
+
"""List every plugin discovered via the multicz.plugins entry-point
|
|
20
|
+
group with its enabled/disabled state and config section.
|
|
21
|
+
|
|
22
|
+
Useful to verify a third-party plugin is picked up after install,
|
|
23
|
+
confirm one is disabled via ``enabled = false``, or see at a glance
|
|
24
|
+
which plugins gate the next ``multicz bump``.
|
|
25
|
+
"""
|
|
26
|
+
_, config = _load()
|
|
27
|
+
plugins = list(DEFAULT_REGISTRY)
|
|
28
|
+
plugins_table: dict[str, dict] = getattr(config, "plugins", {}) or {}
|
|
29
|
+
presenters.render_plugins_list(plugins, plugins_table, output=output)
|
|
@@ -18,6 +18,7 @@ from ...commits import (
|
|
|
18
18
|
tag_prefix,
|
|
19
19
|
)
|
|
20
20
|
from ...config import ComponentMatcher
|
|
21
|
+
from ...plugins import run_enrich_changelog
|
|
21
22
|
from .. import app, err, presenters
|
|
22
23
|
from .._shared import (
|
|
23
24
|
_build_plan_or_exit,
|
|
@@ -159,6 +160,12 @@ def release_notes_cmd(
|
|
|
159
160
|
# Cascades only attach to upcoming bumps - past tag mode
|
|
160
161
|
# has no plan reasons to draw from.
|
|
161
162
|
cascades=tuple(_cascade_entries_for(bump, plan_obj, config)),
|
|
163
|
+
# Plugin contributions for this component (Deprecated /
|
|
164
|
+
# Removed sections from the built-in deprecation plugin,
|
|
165
|
+
# plus anything third-party plugins add).
|
|
166
|
+
plugin_sections=tuple(
|
|
167
|
+
run_enrich_changelog(config, repo, plan_obj, name)
|
|
168
|
+
),
|
|
162
169
|
))
|
|
163
170
|
|
|
164
171
|
if not sections:
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
|
|
10
|
+
from ...plugins import run_post_plan, run_status_lines
|
|
10
11
|
from .. import app, presenters
|
|
11
12
|
from .._shared import _build_plan_or_exit, _load
|
|
12
13
|
|
|
@@ -24,3 +25,12 @@ def status(
|
|
|
24
25
|
repo, config = _load()
|
|
25
26
|
plan_obj = _build_plan_or_exit(repo, config, since=since)
|
|
26
27
|
presenters.render_status_table(plan_obj)
|
|
28
|
+
|
|
29
|
+
# Surface plugin violations + advice so `status` doubles as a
|
|
30
|
+
# "what would break if I ran bump right now" probe.
|
|
31
|
+
violations = run_post_plan(config, repo, plan_obj)
|
|
32
|
+
if violations:
|
|
33
|
+
presenters.render_plugin_violations(violations, output="text")
|
|
34
|
+
advice = run_status_lines(config, repo, plan_obj)
|
|
35
|
+
if advice:
|
|
36
|
+
presenters.render_plugin_advice(advice, output="text")
|
|
@@ -149,6 +149,150 @@ def render_bump_empty(*, output: str) -> None:
|
|
|
149
149
|
)
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
def render_plugins_list(plugins, plugins_config, *, output: str) -> None:
|
|
153
|
+
"""Render the ``multicz plugins`` listing.
|
|
154
|
+
|
|
155
|
+
``plugins`` is the iterable of discovered :class:`Plugin` instances
|
|
156
|
+
(typically ``list(DEFAULT_REGISTRY)``). ``plugins_config`` is
|
|
157
|
+
``config.plugins`` — the raw TOML ``[plugins.<name>]`` map keyed by
|
|
158
|
+
plugin name.
|
|
159
|
+
|
|
160
|
+
Three discrete states are surfaced:
|
|
161
|
+
|
|
162
|
+
* **active** — ``[plugins.<name>]`` declared in multicz.toml and not
|
|
163
|
+
explicitly disabled. The runner will invoke it.
|
|
164
|
+
* **disabled** — section declared but ``enabled = false``. The user
|
|
165
|
+
has opted out on purpose.
|
|
166
|
+
* **inactive** — the plugin is discovered via its entry point but
|
|
167
|
+
no ``[plugins.<name>]`` section is declared. It will *not* run;
|
|
168
|
+
the project hasn't opted in.
|
|
169
|
+
|
|
170
|
+
Text output is a 4-column table; JSON is the machine-readable
|
|
171
|
+
shape for CI consumers.
|
|
172
|
+
"""
|
|
173
|
+
rows = []
|
|
174
|
+
for plugin in plugins:
|
|
175
|
+
configured = plugin.name in plugins_config
|
|
176
|
+
section = plugins_config.get(plugin.name, {}) or {}
|
|
177
|
+
if not configured:
|
|
178
|
+
status = "inactive"
|
|
179
|
+
elif section.get("enabled", True):
|
|
180
|
+
status = "active"
|
|
181
|
+
else:
|
|
182
|
+
status = "disabled"
|
|
183
|
+
impl_cls = type(plugin)
|
|
184
|
+
module = impl_cls.__module__
|
|
185
|
+
rows.append(
|
|
186
|
+
{
|
|
187
|
+
"name": plugin.name,
|
|
188
|
+
"status": status,
|
|
189
|
+
"configured": configured,
|
|
190
|
+
"enabled": status == "active",
|
|
191
|
+
"module": module,
|
|
192
|
+
"class": impl_cls.__name__,
|
|
193
|
+
"config": section,
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
if output == "json":
|
|
197
|
+
console.print_json(data={"plugins": rows})
|
|
198
|
+
return
|
|
199
|
+
if not rows:
|
|
200
|
+
console.print("[dim]no plugins discovered[/]")
|
|
201
|
+
console.print(
|
|
202
|
+
"[dim]install a plugin or check it declares the "
|
|
203
|
+
"[bold]multicz.plugins[/] entry-point group[/]"
|
|
204
|
+
)
|
|
205
|
+
return
|
|
206
|
+
table = Table()
|
|
207
|
+
table.add_column("Plugin", style="bold")
|
|
208
|
+
table.add_column("Status")
|
|
209
|
+
table.add_column("Module")
|
|
210
|
+
table.add_column("Config section")
|
|
211
|
+
status_render = {
|
|
212
|
+
"active": "[green]active[/]",
|
|
213
|
+
"disabled": "[yellow]disabled[/]",
|
|
214
|
+
"inactive": "[dim]inactive[/]",
|
|
215
|
+
}
|
|
216
|
+
for row in rows:
|
|
217
|
+
status_cell = status_render[row["status"]]
|
|
218
|
+
cfg = row["config"]
|
|
219
|
+
# Square brackets in TOML section names trigger Rich's markup
|
|
220
|
+
# parser — escape them so they render literally in the table.
|
|
221
|
+
section_label = f"\\[plugins.{row['name']}]"
|
|
222
|
+
if row["status"] == "inactive":
|
|
223
|
+
cfg_cell = (
|
|
224
|
+
f"[dim](not in multicz.toml — add {section_label} "
|
|
225
|
+
"to activate)[/]"
|
|
226
|
+
)
|
|
227
|
+
elif cfg:
|
|
228
|
+
keys = ", ".join(f"{k}={v!r}" for k, v in cfg.items())
|
|
229
|
+
cfg_cell = f"{section_label} {keys}"
|
|
230
|
+
else:
|
|
231
|
+
cfg_cell = f"{section_label} [dim](defaults)[/]"
|
|
232
|
+
table.add_row(row["name"], status_cell, row["module"], cfg_cell)
|
|
233
|
+
console.print(table)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def render_plugin_advice(lines, *, output: str) -> None:
|
|
237
|
+
"""Render :meth:`Plugin.status_lines` output — free-form actionable
|
|
238
|
+
text plugins return to inform the user (e.g. "3 deprecations marked
|
|
239
|
+
for removal in v3.0").
|
|
240
|
+
|
|
241
|
+
Text output prefixes each line with a magenta arrow so it stands
|
|
242
|
+
out from the bump table without screaming like a violation.
|
|
243
|
+
"""
|
|
244
|
+
if output == "json":
|
|
245
|
+
console.print_json(data={"advice": list(lines)})
|
|
246
|
+
return
|
|
247
|
+
if not lines:
|
|
248
|
+
return
|
|
249
|
+
console.print()
|
|
250
|
+
for line in lines:
|
|
251
|
+
console.print(f" [magenta]→[/] {line}")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def render_plugin_violations(violations, *, output: str) -> None:
|
|
255
|
+
"""Render the list of :class:`multicz.plugins.Violation` raised by
|
|
256
|
+
installed plugins after :meth:`Plugin.post_plan`.
|
|
257
|
+
|
|
258
|
+
Text output groups by severity (errors first, then warnings, then
|
|
259
|
+
infos) and prefixes each line with the plugin name. JSON output
|
|
260
|
+
emits an array suited for CI consumers.
|
|
261
|
+
"""
|
|
262
|
+
if output == "json":
|
|
263
|
+
console.print_json(
|
|
264
|
+
data={
|
|
265
|
+
"violations": [
|
|
266
|
+
{
|
|
267
|
+
"severity": v.severity.value,
|
|
268
|
+
"message": v.message,
|
|
269
|
+
"plugin": v.plugin,
|
|
270
|
+
"component": v.component,
|
|
271
|
+
"file": str(v.file) if v.file else None,
|
|
272
|
+
"line": v.line,
|
|
273
|
+
}
|
|
274
|
+
for v in violations
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
return
|
|
279
|
+
colour = {"error": "red", "warning": "yellow", "info": "cyan"}
|
|
280
|
+
glyph = {"error": "✗", "warning": "!", "info": "·"}
|
|
281
|
+
by_severity: dict[str, list] = {"error": [], "warning": [], "info": []}
|
|
282
|
+
for v in violations:
|
|
283
|
+
by_severity[v.severity.value].append(v)
|
|
284
|
+
for sev in ("error", "warning", "info"):
|
|
285
|
+
for v in by_severity[sev]:
|
|
286
|
+
loc = ""
|
|
287
|
+
if v.file:
|
|
288
|
+
loc = f" [dim]({v.file}{':' + str(v.line) if v.line else ''})[/]"
|
|
289
|
+
comp = f"[dim]\\[{v.component}][/] " if v.component else ""
|
|
290
|
+
console.print(
|
|
291
|
+
f" [{colour[sev]}]{glyph[sev]}[/] {comp}{v.message}"
|
|
292
|
+
f" [dim](from {v.plugin})[/]" + loc
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
152
296
|
def _git_summary_to_json(git: GitSummary) -> dict:
|
|
153
297
|
"""Project a GitSummary back to the legacy ``git_summary`` dict shape.
|
|
154
298
|
|
|
@@ -455,6 +599,7 @@ def render_release_notes(
|
|
|
455
599
|
cascades=list(s.cascades) if s.cascades else None,
|
|
456
600
|
cascade_title=config.project.cascade_section_title,
|
|
457
601
|
cascade_format=config.project.cascade_changelog_format,
|
|
602
|
+
plugin_sections=list(s.plugin_sections) if s.plugin_sections else None,
|
|
458
603
|
)
|
|
459
604
|
if multi:
|
|
460
605
|
range_label = (
|
|
@@ -75,6 +75,11 @@ class ReleaseNotesSection:
|
|
|
75
75
|
Replaces the per-section dict carried through ``sections: list[dict]``.
|
|
76
76
|
``cascades`` is empty for the ``--tag`` retrospective mode (no plan
|
|
77
77
|
reasons exist).
|
|
78
|
+
|
|
79
|
+
``plugin_sections`` carries the output of every plugin's
|
|
80
|
+
:meth:`enrich_changelog` for this component — Deprecated/Removed
|
|
81
|
+
notices, security advisories, etc. — appended below the commit-driven
|
|
82
|
+
sections at render time.
|
|
78
83
|
"""
|
|
79
84
|
|
|
80
85
|
component: str
|
|
@@ -82,6 +87,7 @@ class ReleaseNotesSection:
|
|
|
82
87
|
to_version: str
|
|
83
88
|
commits: tuple[Commit, ...]
|
|
84
89
|
cascades: tuple[CascadeEntry, ...] = ()
|
|
90
|
+
plugin_sections: tuple = ()
|
|
85
91
|
|
|
86
92
|
|
|
87
93
|
# ============ validate ============
|
|
@@ -318,6 +318,12 @@ class Config(BaseModel):
|
|
|
318
318
|
|
|
319
319
|
project: ProjectSettings = Field(default_factory=ProjectSettings)
|
|
320
320
|
components: dict[str, Component]
|
|
321
|
+
# Free-form sub-tables consumed by installed plugins. multicz itself
|
|
322
|
+
# never validates the inner shape — each plugin is responsible for
|
|
323
|
+
# parsing its own ``plugins[<plugin-name>]`` slice. Allows
|
|
324
|
+
# ``[plugins.deprecation]`` in multicz.toml without the core needing
|
|
325
|
+
# to know what fields the deprecation plugin uses.
|
|
326
|
+
plugins: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
321
327
|
|
|
322
328
|
@model_validator(mode="before")
|
|
323
329
|
@classmethod
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
4
|
+
"""multicz plugin system.
|
|
5
|
+
|
|
6
|
+
Plugins extend the bump / planning / changelog pipeline without touching
|
|
7
|
+
the core. They are discovered via the ``multicz.plugins`` entry-point
|
|
8
|
+
group (``importlib.metadata``), so any package on PYTHONPATH that
|
|
9
|
+
registers an entry point participates automatically.
|
|
10
|
+
|
|
11
|
+
Built-in plugins live under :mod:`multicz.plugins.builtin` and are
|
|
12
|
+
registered in multicz's own pyproject.toml.
|
|
13
|
+
|
|
14
|
+
See :mod:`multicz.plugins.protocol` for the hook contract.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .protocol import (
|
|
18
|
+
BasePlugin,
|
|
19
|
+
ChangelogEntry,
|
|
20
|
+
Plugin,
|
|
21
|
+
PluginContext,
|
|
22
|
+
Severity,
|
|
23
|
+
Violation,
|
|
24
|
+
)
|
|
25
|
+
from .registry import DEFAULT_REGISTRY, ENTRY_POINT_GROUP, PluginRegistry
|
|
26
|
+
from .runner import (
|
|
27
|
+
has_errors,
|
|
28
|
+
run_enrich_changelog,
|
|
29
|
+
run_post_plan,
|
|
30
|
+
run_status_lines,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"DEFAULT_REGISTRY",
|
|
35
|
+
"ENTRY_POINT_GROUP",
|
|
36
|
+
"BasePlugin",
|
|
37
|
+
"ChangelogEntry",
|
|
38
|
+
"Plugin",
|
|
39
|
+
"PluginContext",
|
|
40
|
+
"PluginRegistry",
|
|
41
|
+
"Severity",
|
|
42
|
+
"Violation",
|
|
43
|
+
"has_errors",
|
|
44
|
+
"run_enrich_changelog",
|
|
45
|
+
"run_post_plan",
|
|
46
|
+
"run_status_lines",
|
|
47
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
4
|
+
"""Built-in multicz plugins shipped alongside the core.
|
|
5
|
+
|
|
6
|
+
Each built-in lives in its own submodule and registers via
|
|
7
|
+
``[project.entry-points."multicz.plugins"]`` in multicz's pyproject.toml
|
|
8
|
+
— same path third-party plugins use, so there's no privileged loader.
|
|
9
|
+
"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 Chris <goabonga@pm.me>
|
|
3
|
+
|
|
4
|
+
"""Built-in deprecation-policy plugin.
|
|
5
|
+
|
|
6
|
+
Detects deprecation markers in component sources and enforces a
|
|
7
|
+
"remove by version N" policy at bump time. Also contributes
|
|
8
|
+
``Deprecated`` / ``Removed`` sections to changelogs and release notes
|
|
9
|
+
when relevant.
|
|
10
|
+
|
|
11
|
+
See :mod:`multicz.plugins.builtin.deprecation.plugin` for the public
|
|
12
|
+
:class:`DeprecationPlugin` entry point.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .plugin import DeprecationPlugin
|
|
16
|
+
|
|
17
|
+
__all__ = ["DeprecationPlugin"]
|