multicz 0.1.0__tar.gz → 0.2.1__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.1}/PKG-INFO +9 -2
- {multicz-0.1.0 → multicz-0.2.1}/README.md +8 -1
- {multicz-0.1.0 → multicz-0.2.1}/pyproject.toml +1 -1
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/cli.py +94 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/config.py +26 -2
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/__init__.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/changelog.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/commits.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/components.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/debian.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/discovery.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/planner.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/state.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/validation.py +0 -0
- {multicz-0.1.0 → multicz-0.2.1}/src/multicz/writers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: multicz
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
6
|
+
import shlex
|
|
5
7
|
import subprocess
|
|
6
8
|
import sys
|
|
7
9
|
from pathlib import Path
|
|
@@ -1006,6 +1008,59 @@ def _git(repo: Path, *args: str) -> str:
|
|
|
1006
1008
|
return result.stdout
|
|
1007
1009
|
|
|
1008
1010
|
|
|
1011
|
+
def _porcelain_paths(repo: Path) -> set[str]:
|
|
1012
|
+
"""Repo-relative paths currently dirty in the working tree.
|
|
1013
|
+
|
|
1014
|
+
Used to identify candidate paths to hash before/after running
|
|
1015
|
+
``post_bump`` hooks. A pure set diff would miss a file that's
|
|
1016
|
+
dirty both before and after with different content — the
|
|
1017
|
+
canonical case being ``uv run`` itself silently re-syncing
|
|
1018
|
+
``uv.lock`` before multicz even gets to run.
|
|
1019
|
+
"""
|
|
1020
|
+
out = subprocess.run(
|
|
1021
|
+
["git", "status", "--porcelain"],
|
|
1022
|
+
cwd=repo, capture_output=True, text=True,
|
|
1023
|
+
)
|
|
1024
|
+
if out.returncode != 0:
|
|
1025
|
+
return set()
|
|
1026
|
+
paths: set[str] = set()
|
|
1027
|
+
for line in out.stdout.splitlines():
|
|
1028
|
+
if len(line) < 4:
|
|
1029
|
+
continue
|
|
1030
|
+
rest = line[3:]
|
|
1031
|
+
# Renames render as "OLD -> NEW"; we care about the new path only.
|
|
1032
|
+
if " -> " in rest:
|
|
1033
|
+
rest = rest.split(" -> ", 1)[1]
|
|
1034
|
+
paths.add(rest)
|
|
1035
|
+
return paths
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _hash_file(path: Path) -> str | None:
|
|
1039
|
+
try:
|
|
1040
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
1041
|
+
except OSError:
|
|
1042
|
+
return None
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def _run_post_bump_hook(repo: Path, command: str) -> None:
|
|
1046
|
+
"""Execute a single ``post_bump`` shell command in ``repo``."""
|
|
1047
|
+
args = shlex.split(command)
|
|
1048
|
+
if not args:
|
|
1049
|
+
return
|
|
1050
|
+
console.print(f" [dim]post_bump:[/] {command}")
|
|
1051
|
+
result = subprocess.run(
|
|
1052
|
+
args, cwd=repo, capture_output=True, text=True
|
|
1053
|
+
)
|
|
1054
|
+
if result.returncode != 0:
|
|
1055
|
+
err.print(
|
|
1056
|
+
f"[red]post_bump hook failed[/] (exit {result.returncode}): "
|
|
1057
|
+
f"{command}"
|
|
1058
|
+
)
|
|
1059
|
+
if result.stderr.strip():
|
|
1060
|
+
err.print(result.stderr.strip())
|
|
1061
|
+
raise typer.Exit(code=1)
|
|
1062
|
+
|
|
1063
|
+
|
|
1009
1064
|
def _resolve_maintainer(repo: Path, configured: str | None) -> str:
|
|
1010
1065
|
"""Pick a Debian-format maintainer string ``Name <email>``.
|
|
1011
1066
|
|
|
@@ -1375,6 +1430,45 @@ def bump(
|
|
|
1375
1430
|
sign_commits_flag = sign or config.project.sign_commits
|
|
1376
1431
|
sign_tags_flag = sign or config.project.sign_tags
|
|
1377
1432
|
|
|
1433
|
+
# post_bump hooks: run after every file write (bump_files, mirrors,
|
|
1434
|
+
# changelog, state) so commands like `uv lock`, `npm install
|
|
1435
|
+
# --package-lock-only`, `cargo update --workspace`, `helm dependency
|
|
1436
|
+
# update` see the new pyproject.toml / package.json / Chart.yaml /
|
|
1437
|
+
# Cargo.toml. Files modified by hooks are auto-detected and folded
|
|
1438
|
+
# into ``written`` so they ride the release commit.
|
|
1439
|
+
#
|
|
1440
|
+
# Detection compares content hashes — not just the dirty-paths set —
|
|
1441
|
+
# because the entry point is typically ``uv run multicz bump``, and
|
|
1442
|
+
# ``uv run`` re-syncs the venv (which can rewrite ``uv.lock``) before
|
|
1443
|
+
# multicz code runs at all. By the time we snapshot, uv.lock is
|
|
1444
|
+
# already in the dirty set; a set diff would miss the *second*
|
|
1445
|
+
# rewrite the post_bump hook performs against the new pyproject. The
|
|
1446
|
+
# hash comparison catches it.
|
|
1447
|
+
if not dry_run and applied:
|
|
1448
|
+
hook_components = [
|
|
1449
|
+
n for n in applied if config.components[n].post_bump
|
|
1450
|
+
]
|
|
1451
|
+
if hook_components:
|
|
1452
|
+
before_dirty = _porcelain_paths(repo)
|
|
1453
|
+
before_hashes: dict[str, str | None] = {
|
|
1454
|
+
relpath: _hash_file(repo / relpath)
|
|
1455
|
+
for relpath in before_dirty
|
|
1456
|
+
}
|
|
1457
|
+
for name in hook_components:
|
|
1458
|
+
for command in config.components[name].post_bump:
|
|
1459
|
+
_run_post_bump_hook(repo, command)
|
|
1460
|
+
after_dirty = _porcelain_paths(repo)
|
|
1461
|
+
hook_modified: set[str] = {
|
|
1462
|
+
relpath
|
|
1463
|
+
for relpath in after_dirty
|
|
1464
|
+
if relpath not in before_dirty
|
|
1465
|
+
or _hash_file(repo / relpath) != before_hashes.get(relpath)
|
|
1466
|
+
}
|
|
1467
|
+
for relpath in sorted(hook_modified):
|
|
1468
|
+
path = (repo / relpath).resolve()
|
|
1469
|
+
if path.is_file() and path not in written:
|
|
1470
|
+
written.append(path)
|
|
1471
|
+
|
|
1378
1472
|
if not dry_run and commit and written:
|
|
1379
1473
|
rel_paths = [str(p.relative_to(repo)) for p in written]
|
|
1380
1474
|
_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
|