Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions internal/genkit/release_tagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
52 changes: 38 additions & 14 deletions tools/validate_nextchanges.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<section>/<file>.
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"))
Expand All @@ -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}/<section>/<name>.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)

Expand Down
Loading