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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: multicz
3
- Version: 0.1.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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "multicz"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
- @field_validator("paths", "exclude_paths")
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