multicz 0.1.0__tar.gz → 0.2.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.1.0 → multicz-0.2.0}/PKG-INFO +9 -2
- {multicz-0.1.0 → multicz-0.2.0}/README.md +8 -1
- {multicz-0.1.0 → multicz-0.2.0}/pyproject.toml +1 -1
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/cli.py +68 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/config.py +26 -2
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/__init__.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/changelog.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/commits.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/components.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/debian.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/discovery.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/planner.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/state.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/validation.py +0 -0
- {multicz-0.1.0 → multicz-0.2.0}/src/multicz/writers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: multicz
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -293,7 +293,14 @@ Each component declares:
|
|
|
293
293
|
* `mirrors` — files that should reflect this component's version (e.g. a
|
|
294
294
|
Helm chart's `appVersion` mirroring the app version);
|
|
295
295
|
* `triggers` — other components whose bumps should trigger this one;
|
|
296
|
-
* `changelog` — path to a `CHANGELOG.md` the planner should keep in sync
|
|
296
|
+
* `changelog` — path to a `CHANGELOG.md` the planner should keep in sync;
|
|
297
|
+
* `post_bump` — shell commands run after the writes to regenerate
|
|
298
|
+
lockfiles (`uv lock`, `npm install --package-lock-only`,
|
|
299
|
+
`cargo update --workspace`, `helm dependency update charts/foo`,
|
|
300
|
+
`bundle lock`, `composer update --lock`, `go mod tidy`, …). Files
|
|
301
|
+
modified by these commands are auto-detected and folded into the
|
|
302
|
+
release commit, so the lockfile and the version it pins land
|
|
303
|
+
atomically.
|
|
297
304
|
|
|
298
305
|
The planner runs three passes:
|
|
299
306
|
|
|
@@ -265,7 +265,14 @@ Each component declares:
|
|
|
265
265
|
* `mirrors` — files that should reflect this component's version (e.g. a
|
|
266
266
|
Helm chart's `appVersion` mirroring the app version);
|
|
267
267
|
* `triggers` — other components whose bumps should trigger this one;
|
|
268
|
-
* `changelog` — path to a `CHANGELOG.md` the planner should keep in sync
|
|
268
|
+
* `changelog` — path to a `CHANGELOG.md` the planner should keep in sync;
|
|
269
|
+
* `post_bump` — shell commands run after the writes to regenerate
|
|
270
|
+
lockfiles (`uv lock`, `npm install --package-lock-only`,
|
|
271
|
+
`cargo update --workspace`, `helm dependency update charts/foo`,
|
|
272
|
+
`bundle lock`, `composer update --lock`, `go mod tidy`, …). Files
|
|
273
|
+
modified by these commands are auto-detected and folded into the
|
|
274
|
+
release commit, so the lockfile and the version it pins land
|
|
275
|
+
atomically.
|
|
269
276
|
|
|
270
277
|
The planner runs three passes:
|
|
271
278
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import shlex
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
@@ -1006,6 +1007,50 @@ def _git(repo: Path, *args: str) -> str:
|
|
|
1006
1007
|
return result.stdout
|
|
1007
1008
|
|
|
1008
1009
|
|
|
1010
|
+
def _porcelain_paths(repo: Path) -> set[str]:
|
|
1011
|
+
"""Repo-relative paths currently dirty in the working tree.
|
|
1012
|
+
|
|
1013
|
+
Used to diff working-tree state before vs. after running ``post_bump``
|
|
1014
|
+
hooks: anything that wasn't dirty before but is dirty after came from
|
|
1015
|
+
a hook, and gets pulled into the release commit.
|
|
1016
|
+
"""
|
|
1017
|
+
out = subprocess.run(
|
|
1018
|
+
["git", "status", "--porcelain"],
|
|
1019
|
+
cwd=repo, capture_output=True, text=True,
|
|
1020
|
+
)
|
|
1021
|
+
if out.returncode != 0:
|
|
1022
|
+
return set()
|
|
1023
|
+
paths: set[str] = set()
|
|
1024
|
+
for line in out.stdout.splitlines():
|
|
1025
|
+
if len(line) < 4:
|
|
1026
|
+
continue
|
|
1027
|
+
rest = line[3:]
|
|
1028
|
+
# Renames render as "OLD -> NEW"; we care about the new path only.
|
|
1029
|
+
if " -> " in rest:
|
|
1030
|
+
rest = rest.split(" -> ", 1)[1]
|
|
1031
|
+
paths.add(rest)
|
|
1032
|
+
return paths
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _run_post_bump_hook(repo: Path, command: str) -> None:
|
|
1036
|
+
"""Execute a single ``post_bump`` shell command in ``repo``."""
|
|
1037
|
+
args = shlex.split(command)
|
|
1038
|
+
if not args:
|
|
1039
|
+
return
|
|
1040
|
+
console.print(f" [dim]post_bump:[/] {command}")
|
|
1041
|
+
result = subprocess.run(
|
|
1042
|
+
args, cwd=repo, capture_output=True, text=True
|
|
1043
|
+
)
|
|
1044
|
+
if result.returncode != 0:
|
|
1045
|
+
err.print(
|
|
1046
|
+
f"[red]post_bump hook failed[/] (exit {result.returncode}): "
|
|
1047
|
+
f"{command}"
|
|
1048
|
+
)
|
|
1049
|
+
if result.stderr.strip():
|
|
1050
|
+
err.print(result.stderr.strip())
|
|
1051
|
+
raise typer.Exit(code=1)
|
|
1052
|
+
|
|
1053
|
+
|
|
1009
1054
|
def _resolve_maintainer(repo: Path, configured: str | None) -> str:
|
|
1010
1055
|
"""Pick a Debian-format maintainer string ``Name <email>``.
|
|
1011
1056
|
|
|
@@ -1375,6 +1420,29 @@ def bump(
|
|
|
1375
1420
|
sign_commits_flag = sign or config.project.sign_commits
|
|
1376
1421
|
sign_tags_flag = sign or config.project.sign_tags
|
|
1377
1422
|
|
|
1423
|
+
# post_bump hooks: run after every file write (bump_files, mirrors,
|
|
1424
|
+
# changelog, state) so commands like `uv lock`, `npm install
|
|
1425
|
+
# --package-lock-only`, `cargo update --workspace`, `helm dependency
|
|
1426
|
+
# update` see the new pyproject.toml / package.json / Chart.yaml /
|
|
1427
|
+
# Cargo.toml. Files modified by hooks are auto-detected via the git
|
|
1428
|
+
# status diff and folded into ``written`` so they ride the release
|
|
1429
|
+
# commit. Hooks only run when ``not dry_run`` — same gate as the
|
|
1430
|
+
# writes themselves.
|
|
1431
|
+
if not dry_run and applied:
|
|
1432
|
+
hook_components = [
|
|
1433
|
+
n for n in applied if config.components[n].post_bump
|
|
1434
|
+
]
|
|
1435
|
+
if hook_components:
|
|
1436
|
+
before_dirty = _porcelain_paths(repo)
|
|
1437
|
+
for name in hook_components:
|
|
1438
|
+
for command in config.components[name].post_bump:
|
|
1439
|
+
_run_post_bump_hook(repo, command)
|
|
1440
|
+
after_dirty = _porcelain_paths(repo)
|
|
1441
|
+
for relpath in sorted(after_dirty - before_dirty):
|
|
1442
|
+
path = (repo / relpath).resolve()
|
|
1443
|
+
if path.is_file() and path not in written:
|
|
1444
|
+
written.append(path)
|
|
1445
|
+
|
|
1378
1446
|
if not dry_run and commit and written:
|
|
1379
1447
|
rel_paths = [str(p.relative_to(repo)) for p in written]
|
|
1380
1448
|
_git(repo, "add", "--", *rel_paths)
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
16
|
import re
|
|
17
|
+
import shlex
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any, Literal
|
|
19
20
|
|
|
@@ -116,12 +117,35 @@ class Component(BaseModel):
|
|
|
116
117
|
ignored_types: list[str] = Field(default_factory=list)
|
|
117
118
|
version_scheme: Literal["semver", "pep440"] = "semver"
|
|
118
119
|
artifacts: list[Artifact] = Field(default_factory=list)
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
# Shell commands run after multicz has rewritten this component's
|
|
121
|
+
# bump_files but before it stages them for the release commit. The
|
|
122
|
+
# canonical use-case is regenerating lockfiles that depend on the
|
|
123
|
+
# version multicz just wrote (uv.lock, package-lock.json, Cargo.lock,
|
|
124
|
+
# Chart.lock, …). Each entry is parsed via shlex.split and executed
|
|
125
|
+
# in the repo root. Files modified by these hooks are auto-detected
|
|
126
|
+
# and joined to the release commit.
|
|
127
|
+
post_bump: list[str] = Field(default_factory=list)
|
|
128
|
+
|
|
129
|
+
@field_validator("paths", "exclude_paths", "post_bump")
|
|
121
130
|
@classmethod
|
|
122
131
|
def _strip_globs(cls, value: list[str]) -> list[str]:
|
|
123
132
|
return [v.strip() for v in value if v.strip()]
|
|
124
133
|
|
|
134
|
+
@field_validator("post_bump")
|
|
135
|
+
@classmethod
|
|
136
|
+
def _validate_post_bump_shellable(cls, value: list[str]) -> list[str]:
|
|
137
|
+
"""Surface bad quoting at config-load time (i.e. via
|
|
138
|
+
``multicz validate``) instead of at bump-run time."""
|
|
139
|
+
for entry in value:
|
|
140
|
+
try:
|
|
141
|
+
shlex.split(entry)
|
|
142
|
+
except ValueError as exc:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"post_bump entry {entry!r} is not a valid shell "
|
|
145
|
+
f"command: {exc}"
|
|
146
|
+
) from exc
|
|
147
|
+
return value
|
|
148
|
+
|
|
125
149
|
@model_validator(mode="after")
|
|
126
150
|
def _merge_triggers_alias(self) -> Component:
|
|
127
151
|
"""Fold ``triggers`` (legacy name) into ``depends_on`` (canonical).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|