diff --git a/.github/scripts/extract_changelog.py b/.github/scripts/extract_changelog.py index 31af1049..93111408 100644 --- a/.github/scripts/extract_changelog.py +++ b/.github/scripts/extract_changelog.py @@ -1,10 +1,25 @@ #!/usr/bin/env python3 +import re import sys package, version = sys.argv[1], sys.argv[2] -sections = open(f"packages/{package}/RELEASE_NOTES.md").read().split("## ") -match = next((s for s in sections if s.startswith(version)), None) +content = open(f"packages/{package}/RELEASE_NOTES.md").read() +# Split on level-2 headers only ("## " at line start), not "### " subheaders. +sections = re.split(r"^## ", content, flags=re.MULTILINE) + + +def matches(section: str) -> bool: + # Header is commitizen-generated ("gen-v7.3.0 (2026-09-07)"); tolerate a bare + # version too ("7.3.0") for any hand-written entries. + head = section.split(None, 1)[0] if section.split() else "" + return head == version or head.endswith(f"-v{version}") + + +match = next((s for s in sections if matches(s)), None) if not match: sys.exit(f"No changelog entry found for version {version}") -print(match[len(version):].strip()) + +# Drop the header line, keep the body. +body = match.split("\n", 1)[1] if "\n" in match else "" +print(body.strip()) diff --git a/.github/scripts/update_changelog.py b/.github/scripts/update_changelog.py index 2727baee..5d3cad1a 100644 --- a/.github/scripts/update_changelog.py +++ b/.github/scripts/update_changelog.py @@ -1,13 +1,26 @@ #!/usr/bin/env python3 +import re import sys package, version, body = sys.argv[1], sys.argv[2], sys.argv[3] path = f"packages/{package}/RELEASE_NOTES.md" content = open(path).read() -sections = content.split("## ") -match = next((s for s in sections if s.startswith(version)), None) +# Split on level-2 headers only ("## " at line start), not "### " subheaders. +sections = re.split(r"^## ", content, flags=re.MULTILINE) + + +def matches(section: str) -> bool: + # Header is commitizen-generated ("gen-v7.3.0 (2026-09-07)"); tolerate a bare + # version too ("7.3.0") for any hand-written entries. + head = section.split(None, 1)[0] if section.split() else "" + return head == version or head.endswith(f"-v{version}") + + +match = next((s for s in sections if matches(s)), None) if not match: sys.exit(f"No changelog entry found for version {version}") -open(path, "w").write(content.replace(match, f"{version}\n\n{body.strip()}\n\n")) +# Keep the generated header line, replace only the body with the published notes. +header = match.split("\n", 1)[0] +open(path, "w").write(content.replace(f"## {match}", f"## {header}\n\n{body.strip()}\n\n")) diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 00000000..7431e558 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,30 @@ +# Releasing + +This monorepo publishes three independent PyPI packages, each with its own version, tag prefix, and changelog. Publish in dependency order when several changed together: **base → core → gen**. + +| Package | PyPI name | Tag prefix | Changelog | +| --- | --- | --- | --- | +| `base` | `sap-ai-sdk-base` | `base-v*` | `packages/base/RELEASE_NOTES.md` | +| `core` | `sap-ai-sdk-core` | `core-v*` | `packages/core/RELEASE_NOTES.md` | +| `gen` | `sap-ai-sdk-gen` | `gen-v*` | `packages/gen/RELEASE_NOTES.md` | + +## Flow + + +0. Commit with the package's scope → feat(base|core|gen): ... / fix(...): ... +1. Run the "bump" workflow → Actions ▸ bump ▸ Run workflow ▸ pick the package +2. draft-release (automatic) → notes auto-generated from commits, on the new tag +3. Review the draft release, then Publish → edits sync back to RELEASE_NOTES.md +4. publish-pypi (automatic) → package lands on PyPI + +Also on tag push: **api-doc.yml** (any package) regenerates the API docs and opens a PR in `SAP/ai-sdk`; **documentation.yml** (`gen` only) builds the Sphinx HTML. + +## Commit scopes + +Versions and changelog entries come **from commit messages**, not the diff — each package counts only commits matching its own scope (from `[tool.commitizen.customize]` in its `pyproject.toml`). `feat` → minor bump, `fix` → patch bump. Any other scope (`chore`, `test`, none) is **silently ignored**. + +| Package | `bump_pattern` | +| --- | --- | +| `base` | `^(feat\|fix)\(base\)` | +| `core` | `^(feat\|fix)\(core\)` | +| `gen` | `^(feat\|fix)\((gen\|prompt_registry\|evaluations)\)` |