mirror of
https://github.com/codeflash-ai/codeflash-agent.git
synced 2026-05-04 18:25:19 +00:00
126 lines
3.8 KiB
Python
126 lines
3.8 KiB
Python
"""Combine per-branch changelog entries into a package CHANGELOG.md.
|
|
|
|
Usage:
|
|
python scripts/combine-changelogs.py PACKAGE
|
|
|
|
Reads all .md files from packages/<PACKAGE>/changelogs/, combines them by
|
|
subsection (Enhancements / Features / Fixes), prepends the result to
|
|
packages/<PACKAGE>/CHANGELOG.md, and deletes the source entries.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import warnings
|
|
from collections import OrderedDict
|
|
from pathlib import Path
|
|
|
|
import tomlkit
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
PACKAGES_DIR = REPO_ROOT / "packages"
|
|
|
|
SUBSECTION_TYPES = ["### Enhancements", "### Features", "### Fixes"]
|
|
|
|
|
|
def ensure_changelog_folder_purity(folder: Path) -> None:
|
|
"""Raise if the changelogs folder contains non-.md files."""
|
|
if not folder.exists():
|
|
return
|
|
for name in os.listdir(folder):
|
|
if not name.endswith(".md"):
|
|
msg = (
|
|
f"Found non-markdown file {name!r} in {folder}. "
|
|
"Changelogs must be .md files."
|
|
)
|
|
raise ValueError(msg)
|
|
|
|
|
|
def parse_subsections(path: Path) -> dict[str, list[str]]:
|
|
"""Extract subsection -> lines mapping from a changelog entry."""
|
|
subsections: dict[str, list[str]] = {}
|
|
current: str | None = None
|
|
|
|
for line in path.read_text().splitlines():
|
|
if any(st in line for st in SUBSECTION_TYPES):
|
|
current = line.strip()
|
|
subsections[current] = []
|
|
elif line.strip() and current is not None:
|
|
subsections[current].append(line.strip().lstrip("-").lstrip(" "))
|
|
|
|
return subsections
|
|
|
|
|
|
def combine_files(folder: Path) -> dict[str, list[str]]:
|
|
"""Combine subsections from all changelog entries in a folder."""
|
|
ensure_changelog_folder_purity(folder)
|
|
combined: dict[str, list[str]] = {}
|
|
|
|
if not folder.exists():
|
|
return combined
|
|
|
|
for name in sorted(os.listdir(folder)):
|
|
if not name.endswith(".md"):
|
|
warnings.warn(
|
|
f"Ignoring non-markdown file {name!r} in {folder}.",
|
|
stacklevel=2,
|
|
)
|
|
continue
|
|
|
|
for section, lines in parse_subsections(folder / name).items():
|
|
combined.setdefault(section, []).extend(lines)
|
|
|
|
return combined
|
|
|
|
|
|
def serialize(combined: dict[str, list[str]], version: str) -> str:
|
|
"""Format combined subsections as a markdown release block."""
|
|
out = f"## {version}"
|
|
for section, lines in combined.items():
|
|
out += f"\n\n{section}"
|
|
for line in lines:
|
|
out += f"\n- {line}"
|
|
out += "\n\n"
|
|
return out
|
|
|
|
|
|
def main() -> None:
|
|
"""Combine changelogs for a package and update CHANGELOG.md."""
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python scripts/combine-changelogs.py PACKAGE")
|
|
sys.exit(1)
|
|
|
|
package = sys.argv[1]
|
|
pkg_dir = PACKAGES_DIR / package
|
|
changelogs_dir = pkg_dir / "changelogs"
|
|
changelog_md = pkg_dir / "CHANGELOG.md"
|
|
pyproject = pkg_dir / "pyproject.toml"
|
|
|
|
if not pyproject.exists():
|
|
print(f"No pyproject.toml at {pyproject}")
|
|
sys.exit(1)
|
|
|
|
data = tomlkit.parse(pyproject.read_text())
|
|
version = str(data["project"]["version"])
|
|
|
|
combined = OrderedDict(sorted(combine_files(changelogs_dir).items()))
|
|
if not any(combined.values()):
|
|
print(f"{package}: no changelog entries to combine.")
|
|
return
|
|
|
|
updates = serialize(combined, version)
|
|
|
|
existing = changelog_md.read_text() if changelog_md.exists() else ""
|
|
changelog_md.write_text(updates + existing)
|
|
print(f"{package}: updated {changelog_md.relative_to(REPO_ROOT)}")
|
|
|
|
# Clean up individual entries.
|
|
if changelogs_dir.exists():
|
|
for entry in changelogs_dir.iterdir():
|
|
entry.unlink()
|
|
print(f"{package}: cleaned up {changelogs_dir.relative_to(REPO_ROOT)}/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|