feat: Versionsstelle als Kompatibilitaetsfrage, Breaking-Change-Vermerk erzwungen (2.5.0)
Files changed: - CHANGES.md - INSTALL.md - VERSION - instructions/dev/stack-dev/SKILL.md - instructions/dev/version-parts.md - tools/CONTRACT.md - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_version_cmd.py - tools/chemenu/version.py
This commit is contained in:
1 parent
7fbb9a99df
commit
31662dc3ff
11 files changed
+431
-32
No files matched your search
@@ -473,6 +473,43 @@ def check_migration_for_boundary() -> list[str]:
|
||||
]
|
||||
|
||||
|
||||
def check_breaking_change_for_boundary() -> list[str]:
|
||||
"""A version that crosses the compatibility boundary must say what breaks.
|
||||
|
||||
Separate from `check_migration_for_boundary`, because the two answer
|
||||
different questions: that one asks whether the *content* has to move, this
|
||||
one whether the operator was told the swap is not drop-in at all. A
|
||||
boundary crossing with an untouched corpus - a renamed feed, artefact,
|
||||
import name or flag - satisfies that check and still leaves every existing
|
||||
instance with something to do by hand.
|
||||
|
||||
Only the newest entry is checked, for the same reason: older crossings are
|
||||
history, and re-reporting them forever would make the check noise.
|
||||
"""
|
||||
changes_path = config.ROOT / version_mod.CHANGES_FILENAME
|
||||
version_path = config.ROOT / version_mod.VERSION_FILENAME
|
||||
if not changes_path.is_file() or not version_path.is_file():
|
||||
return [] # already reported by check_version_changelog
|
||||
|
||||
text = changes_path.read_text(encoding="utf-8")
|
||||
current = version_mod.top_changes_version(text)
|
||||
previous = _second_changes_version(text)
|
||||
if current is None or previous is None:
|
||||
return [] # the first versioned entry has no predecessor to cross from
|
||||
if current.compat_key == previous.compat_key:
|
||||
return []
|
||||
|
||||
if version_mod.BREAKING_CHANGE_MARKER in (version_mod.changes_section(text, current) or ""):
|
||||
return []
|
||||
|
||||
return [
|
||||
f"{current} crosses the compatibility boundary from {previous}, so it is not a drop-in "
|
||||
f"replacement - but its {version_mod.CHANGES_FILENAME} entry carries no "
|
||||
f"`{version_mod.BREAKING_CHANGE_MARKER}` line saying what stops working. Add it "
|
||||
"(`version bump --breaking` writes it; see instructions/dev/version-parts.md)"
|
||||
]
|
||||
|
||||
|
||||
@app.command("verify")
|
||||
def verify():
|
||||
"""Check the CLI/README command tables, contract presence, type-form drift, ignore rules, and version/changelog agreement."""
|
||||
@@ -484,6 +521,7 @@ def verify():
|
||||
+ check_ignored_content()
|
||||
+ check_version_changelog()
|
||||
+ check_migration_for_boundary()
|
||||
+ check_breaking_change_for_boundary()
|
||||
)
|
||||
|
||||
if issues:
|
||||
|
||||
@@ -189,6 +189,11 @@ def bump_command(
|
||||
minor: bool = typer.Option(False, "--minor", help="Bump MINOR (resets PATCH)"),
|
||||
patch: bool = typer.Option(False, "--patch", help="Bump PATCH"),
|
||||
title: str = typer.Option(..., "--title", help="One-line title for the new CHANGES.md entry"),
|
||||
breaking: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--breaking",
|
||||
help="What stops working, for a boundary-crossing bump (recorded in CHANGES.md). Required on one, refused on any other",
|
||||
),
|
||||
no_migration: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--no-migration",
|
||||
@@ -203,10 +208,14 @@ def bump_command(
|
||||
frontmatter and leaves the prose. `docs verify` afterwards enforces that
|
||||
the two agree, so a bump with no entry cannot reach a release.
|
||||
|
||||
A bump that crosses the compatibility boundary additionally requires a
|
||||
migration document for the new version, or `--no-migration "<reason>"`.
|
||||
An instance learning that it must migrate, with nothing telling it how, is
|
||||
the gap this closes."""
|
||||
A bump that crosses the compatibility boundary - one whose new version is
|
||||
not a drop-in replacement, whether or not any content moves - requires
|
||||
`--breaking "<what stops working>"`, and on top of that either a migration
|
||||
document for the new version or `--no-migration "<reason>"`. An instance
|
||||
learning that it must migrate, with nothing telling it what broke or how to
|
||||
cross, is the gap these close. Which part to pass stays a judgment call
|
||||
this command does not make - it enforces only that a crossing says what it
|
||||
costs."""
|
||||
selected = [name for name, chosen in (("major", major), ("minor", minor), ("patch", patch)) if chosen]
|
||||
if len(selected) != 1:
|
||||
fail("Pass exactly one of --major / --minor / --patch")
|
||||
@@ -239,6 +248,23 @@ def bump_command(
|
||||
crossing = new_version.compat_key != current.compat_key
|
||||
boundary = " (crosses a compatibility boundary - instances must migrate)" if crossing else ""
|
||||
|
||||
if crossing and not breaking:
|
||||
fail(
|
||||
f"{current} -> {new_version} crosses the compatibility boundary, so it is not a "
|
||||
f"drop-in replacement - re-run with --breaking \"<what stops working, and what an "
|
||||
f"instance must do about it>\".\n"
|
||||
f"If that sentence is hard to write because nothing actually breaks - no hand-work "
|
||||
f"on update, and the old version can still be put back - then the bump is probably "
|
||||
f"not --{selected[0]}."
|
||||
)
|
||||
return
|
||||
if breaking and not crossing:
|
||||
fail(
|
||||
f"--breaking only applies to a bump that crosses the compatibility boundary; "
|
||||
f"{current} -> {new_version} does not."
|
||||
)
|
||||
return
|
||||
|
||||
if crossing and not no_migration:
|
||||
from chemenu import kb_state
|
||||
|
||||
@@ -267,6 +293,7 @@ def bump_command(
|
||||
version_mod.insert_changes_entry(
|
||||
text, new_version, today_iso(), title.strip(), author,
|
||||
no_migration_reason=no_migration.strip() if no_migration else None,
|
||||
breaking_reason=breaking.strip() if breaking else None,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user