diff --git a/.codegen.json b/.codegen.json index ee2037e0ee..0866ebe7ae 100644 --- a/.codegen.json +++ b/.codegen.json @@ -8,6 +8,13 @@ "python/uv.lock": "name = \"databricks-bundles\"\nversion = \"$VERSION\"", "libs/template/templates/default/library/versions.tmpl": "{{define \"latest_databricks_bundles_version\" -}}$VERSION{{- end}}" }, + "nextchanges_sections": { + "notable-changes": "Notable Changes", + "cli": "CLI", + "bundles": "Bundles", + "dependency-updates": "Dependency Updates", + "api-changes": "API Changes" + }, "toolchain": { "required": [ "go" diff --git a/internal/genkit/release_tagging.py b/internal/genkit/release_tagging.py index 8a34b595a2..75180a7d03 100644 --- a/internal/genkit/release_tagging.py +++ b/internal/genkit/release_tagging.py @@ -75,7 +75,7 @@ def render_nextchanges(package_path: str) -> Optional[str]: continue entries = [] for name in sorted(os.listdir(section_dir)): - if not name.endswith(".md") or name == "README.md": + if not name.endswith(".md"): continue with open(os.path.join(section_dir, name)) as f: text = f.read().strip() @@ -132,8 +132,8 @@ def clear_nextchanges(package_path: str) -> None: Replacement for ``tagging.clean_next_changelog``: stage deletion of the ``.nextchanges/`` fragments consumed by this release and bump ``.nextchanges/version`` to the next minor (its post-release default; teams - can still override it in a PR). Section directories and their README.md are - left in place. ``process_package`` calls this as + can still override it in a PR). Section directories are left in place. + ``process_package`` calls this as ``clean_next_changelog(package.path)``, so the signature matches. """ base = os.path.join(os.getcwd(), package_path, NEXTCHANGES_DIR) @@ -142,7 +142,7 @@ def clear_nextchanges(package_path: str) -> None: if not os.path.isdir(section_dir): continue for name in sorted(os.listdir(section_dir)): - if name.endswith(".md") and name != "README.md": + if name.endswith(".md"): tagging.gh.delete_file(os.path.join(section_dir, name)) version_path = _version_path(package_path) diff --git a/tools/validate_nextchanges.py b/tools/validate_nextchanges.py index c89eb9b008..67f008d714 100755 --- a/tools/validate_nextchanges.py +++ b/tools/validate_nextchanges.py @@ -12,47 +12,65 @@ """ import argparse +import json import pathlib import re import sys CHANGELOG_DIR = ".nextchanges" - -# Known section subdirectories. Mirrors NEXTCHANGES_SECTIONS in -# internal/genkit/tagging.py — keep the two in sync. -SECTIONS = ("notable-changes", "cli", "bundles", "dependency-updates", "api-changes") +CODEGEN_FILE = ".codegen.json" +NEXTCHANGES_SECTIONS_KEY = "nextchanges_sections" # .nextchanges/version holds the next release version; the release reads it and # bumps it. Accept a bare semver (optionally v-prefixed), e.g. 1.4.0 / v1.4.0. VERSION_FILE = "version" SEMVER_RE = re.compile(r"^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$") -# Non-fragment files allowed to sit alongside fragments at any depth. -SCAFFOLDING = ("README.md", ".gitkeep") +ROOT_README = "README.md" +SECTION_SCAFFOLDING = (".gitkeep",) + + +def load_sections(root): + codegen_path = root / CODEGEN_FILE + try: + codegen = json.loads(codegen_path.read_text(encoding="utf-8")) + except FileNotFoundError as err: + raise ValueError(f"{CODEGEN_FILE} is missing") from err + except json.JSONDecodeError as err: + raise ValueError(f"{CODEGEN_FILE} is not valid JSON: {err}") from err + + sections = codegen.get(NEXTCHANGES_SECTIONS_KEY) + if not isinstance(sections, dict) or not sections: + raise ValueError(f"{CODEGEN_FILE} must define a non-empty {NEXTCHANGES_SECTIONS_KEY} object") + return tuple(sections) -def find_problems(changelog_dir): + +def find_problems(changelog_dir, sections): """Return a list of ``(path, message)`` for anything unexpected under ``.nextchanges/``: files that aren't a section fragment or known scaffolding, empty fragments, and a missing/malformed version file.""" problems = [] + known_sections = set(sections) for path in sorted(changelog_dir.rglob("*")): if path.is_dir(): continue rel = path.relative_to(changelog_dir) name = path.name - # Root-level: only the version file and scaffolding belong here. + # Root-level: only the version file and root documentation belong here. This prevents + # someone accidentally putting a .md into .nextchanges thinking it would be picked up. if len(rel.parts) == 1: - if name != VERSION_FILE and name not in SCAFFOLDING: + if name != VERSION_FILE and name != ROOT_README: problems.append((path, "unexpected file at .nextchanges root")) continue # Section-level: .nextchanges/
/. - if len(rel.parts) == 2 and rel.parts[0] in SECTIONS: - if name in SCAFFOLDING: - continue + if len(rel.parts) == 2 and rel.parts[0] in known_sections: if not name.endswith(".md"): + # Only allow scaffolding files to exist + if name in SECTION_SCAFFOLDING: + continue problems.append((path, "unexpected file (fragments must be *.md)")) elif not path.read_text(encoding="utf-8").strip(): problems.append((path, "empty fragment")) @@ -78,12 +96,18 @@ def main(argv=None): if not changelog_dir.is_dir(): return - problems = find_problems(changelog_dir) + try: + sections = load_sections(args.root) + except ValueError as err: + print(err, file=sys.stderr) + sys.exit(1) + + problems = find_problems(changelog_dir, sections) if problems: for path, msg in problems: print(f"{path}: {msg}", file=sys.stderr) print(f"\nFragments must live at {CHANGELOG_DIR}/
/.md", file=sys.stderr) - print(f"Valid sections: {', '.join(SECTIONS)}", file=sys.stderr) + print(f"Valid sections: {', '.join(sections)}", file=sys.stderr) print(f"{CHANGELOG_DIR}/{VERSION_FILE} must hold the next release version.", file=sys.stderr) sys.exit(1)