-
Notifications
You must be signed in to change notification settings - Fork 195
changelog: Prevent merge conflicts on NEXT_CHANGELOG by fragmenting it into files #5534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
747fc46
368042d
7707dd1
6543e30
bf196ac
73d3550
25cb944
3108bff
e92c6be
38eef21
1fd1ae2
74f3ddd
d774453
dbb13e7
3a32f0a
ae081d0
4b17443
04e637a
7a290e2
e0a2aa4
d2093d8
1d06899
e925f8d
a339ab0
fcdb4cc
e334168
0826ce0
2ee9000
606a0ee
129fb7c
05f0cd0
47ad6f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| --- | ||
| description: New changelog entries go in NEXT_CHANGELOG.md, not CHANGELOG.md | ||
| description: New changelog entries go in .nextchanges/, not CHANGELOG.md | ||
| globs: | ||
| - "CHANGELOG.md" | ||
| - ".nextchanges/**" | ||
| --- | ||
|
|
||
| **RULE: Do not add new release entries to `CHANGELOG.md`.** New entries for the upcoming release go in `NEXT_CHANGELOG.md`. `CHANGELOG.md` is updated automatically by the release process. | ||
| **RULE: Do not add new release entries to `CHANGELOG.md`.** For a user-visible change, add a fragment file under `.nextchanges/<section>/` (sections: `notable-changes`, `cli`, `bundles`, `dependency-updates`, `api-changes`). Each PR adds its own file, so entries never conflict. `CHANGELOG.md` is generated automatically from these fragments by the release process — never hand-edit it. See `.nextchanges/README.md`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # Changelog fragments | ||
|
|
||
| Add a changelog entry by creating a **new file** in the section folder under | ||
| `.nextchanges/` that fits your change. Each PR adds its own file, so two PRs | ||
| never touch the same path — no merge conflicts, unlike everyone editing one | ||
| shared changelog file. | ||
|
|
||
| ## How to add an entry (takes 10 seconds) | ||
|
|
||
| Create `.nextchanges/<section>/<name>.md` and write what changed: | ||
|
|
||
| ``` | ||
| Added the `databricks quickstart` command. | ||
| ``` | ||
|
|
||
| You can do this straight from the GitHub UI: **Add file → Create new file**, | ||
| type the path (e.g. `.nextchanges/cli/quickstart.md`), write a sentence, commit. | ||
|
|
||
| - `<name>` is arbitrary — a feature name (`quickstart.md`) or your PR number | ||
| (`5464.md`), whatever you like, as long as it's unique. | ||
| - The leading `* ` is optional. | ||
| - A PR link is optional. If you want one, write `(#5464)` and run `task links` | ||
| (or `task checks`) to expand it into a full markdown link in place; CI fails | ||
| if a raw `(#5464)` is left unexpanded. The release does not expand links, so | ||
| the fragment must already be expanded when it lands. | ||
| - One file is usually one entry; for several, put each on its own `* ` line. | ||
|
|
||
| ### Sections | ||
|
|
||
| | Folder | Section in the released changelog | | ||
| | --- | --- | | ||
| | `.nextchanges/notable-changes/` | Notable Changes (prominent, called out at the top) | | ||
| | `.nextchanges/cli/` | CLI | | ||
| | `.nextchanges/bundles/` | Bundles | | ||
| | `.nextchanges/dependency-updates/` | Dependency updates | | ||
| | `.nextchanges/api-changes/` | API Changes | | ||
|
|
||
| See [`.agent/skills/pr-checklist/SKILL.md`](../.agent/skills/pr-checklist/SKILL.md) | ||
| for when an entry is warranted. | ||
|
|
||
| ## How it's released | ||
|
|
||
| You don't run anything. At release time the tagging workflow renders every | ||
| fragment into the matching section of `CHANGELOG.md`, deletes the consumed | ||
| fragments, and bumps `version` to the next minor (see | ||
| `internal/genkit/tagging.py`). `./task check-changelog` validates fragment | ||
| placement and the `version` file on every PR. | ||
|
|
||
| ### `version` | ||
|
|
||
| `.nextchanges/version` holds the version of the next release (e.g. `1.4.0`). | ||
| It's bumped to the next minor automatically after each release — edit it in a | ||
| PR only to cut a patch or major release instead. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 1.8.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| # /// script | ||
| # dependencies = ["PyGithub>=2,<3", "pyjwt<2.12.0", "charset-normalizer<3.4.6"] | ||
| # /// | ||
| """databricks/cli release entrypoint. | ||
|
|
||
| Thin wrapper around the synced ``tagging.py`` — which is regenerated verbatim | ||
| from universe (``openapi/tagging/tagging.py``) by ``genkit update-sdk`` and must | ||
| stay pristine. The tagging workflow runs this file (``release_tagging.py``) | ||
| instead, so the CLI-specific behavior lives here rather than as edits to the | ||
| synced file: | ||
|
|
||
| * the changelog body is rendered from per-PR ``.nextchanges/<section>/*.md`` | ||
| fragments rather than read from a ``NEXT_CHANGELOG.md`` file, and | ||
| * the release version is read from ``.nextchanges/version`` (bumped to the next | ||
| minor on each release) rather than a hand-maintained ``## Release vX.Y.Z`` | ||
| header. | ||
|
|
||
| It injects that behavior by rebinding two module-level seams in the upstream | ||
| module (``get_next_tag_info`` and ``clean_next_changelog``, both called by name | ||
| from ``process_package``/``preview_tag_infos``) and then delegates to the | ||
| untouched ``process()`` for all commit/tag/race/recovery logic. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we make sure this doesn't regress? |
||
| """ | ||
|
|
||
| import os | ||
| from typing import Optional | ||
|
|
||
| import tagging | ||
|
|
||
| NEXTCHANGES_DIR = ".nextchanges" | ||
|
|
||
| # Tracks the version of the next release. Read at release time and bumped to the | ||
| # next minor afterward — the role NEXT_CHANGELOG.md's "## Release vX.Y.Z" header | ||
| # played upstream. | ||
| VERSION_FILE = "version" | ||
|
|
||
| # Section subdirectory -> "### " header, in changelog order. Mirrors the | ||
| # section folders documented in .nextchanges/README.md and validated by | ||
| # tools/validate_nextchanges.py — keep the three in sync. | ||
| NEXTCHANGES_SECTIONS = ( | ||
| ("notable-changes", "Notable Changes"), | ||
| ("cli", "CLI"), | ||
| ("bundles", "Bundles"), | ||
| ("dependency-updates", "Dependency Updates"), | ||
| ("api-changes", "API Changes"), | ||
| ) | ||
|
|
||
|
|
||
| def render_nextchanges(package_path: str) -> Optional[str]: | ||
| """ | ||
| Render ``<package_path>/.nextchanges/<section>/*.md`` into the changelog | ||
| body: one ``### <Section>`` block per non-empty section in | ||
| NEXTCHANGES_SECTIONS order, fragments sorted by filename. Returns None when | ||
| there are no fragments. | ||
|
|
||
| The output matches the CHANGELOG.md convention: a blank line after each | ||
| ``### <Section>`` heading, and every entry as a `` * `` bullet (a leading | ||
| space before the ``*``). A leading ``* ``/``- `` marker is optional in a | ||
| fragment and is normalized; continuation lines are left as authored. | ||
|
|
||
| Raw PR references (``(#1234)``/``#1234``) in fragments are converted to | ||
| markdown links before release by ``tools/update_github_links.py`` (the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove this script once all of this lands?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tools/update_github_links.py? No, that's still used in Maybe misleading part of the docstring here that can just be omitted entirely -
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think its purpose was only to check |
||
| ``links`` task, enforced in CI), so no link expansion happens here. | ||
| """ | ||
| base = os.path.join(os.getcwd(), package_path, NEXTCHANGES_DIR) | ||
| if not os.path.isdir(base): | ||
| return None | ||
|
|
||
| blocks = [] | ||
| for slug, header in NEXTCHANGES_SECTIONS: | ||
| section_dir = os.path.join(base, slug) | ||
| if not os.path.isdir(section_dir): | ||
| continue | ||
| entries = [] | ||
| for name in sorted(os.listdir(section_dir)): | ||
| if not name.endswith(".md") or name == "README.md": | ||
| continue | ||
| with open(os.path.join(section_dir, name)) as f: | ||
| text = f.read().strip() | ||
| if not text: | ||
| continue | ||
| first, _, rest = text.partition("\n") | ||
| if first.startswith(("* ", "- ")): | ||
| first = first[2:] | ||
| # Leading-space bullet to match CHANGELOG.md (e.g. " * entry"). | ||
| entries.append(f" * {first}" + (("\n" + rest) if rest else "")) | ||
| if entries: | ||
| # Blank line after the heading, matching CHANGELOG.md. | ||
| blocks.append(f"### {header}\n\n" + "\n".join(entries)) | ||
|
|
||
| if not blocks: | ||
| return None | ||
| return "\n\n".join(blocks) | ||
|
|
||
|
|
||
| def _version_path(package_path: str) -> str: | ||
| return os.path.join(os.getcwd(), package_path, NEXTCHANGES_DIR, VERSION_FILE) | ||
|
|
||
|
|
||
| def next_version(package: tagging.Package) -> str: | ||
| """ | ||
| Release version for this run, read from ``.nextchanges/version``. To cut a | ||
| patch or major release, edit that file in the PR; otherwise the default | ||
| (bumped after the previous release) is the next minor. | ||
| """ | ||
| with open(_version_path(package.path)) as f: | ||
| return str(tagging.Version.parse(f.read().strip().lstrip("v"))) | ||
|
|
||
|
|
||
| def get_next_tag_info(package: tagging.Package) -> Optional[tagging.TagInfo]: | ||
| """ | ||
| Replacement for ``tagging.get_next_tag_info``: build the release TagInfo | ||
| from ``.nextchanges/`` fragments. Returns None when there are no entries | ||
| (nothing to release), unless ``allow_empty_changelog`` is set in | ||
| ``.codegen.json`` — matching the pristine skip behavior. | ||
| """ | ||
| body = render_nextchanges(package.path) | ||
| if body is None and not tagging._load_codegen_config().get("allow_empty_changelog", False): | ||
| print("No .nextchanges/ entries. No changes will be made to the changelog.") | ||
| return None | ||
|
|
||
| version = next_version(package) | ||
| # write_changelog() keys off the "## Release v…" header, so include it. | ||
| content = f"## Release v{version}\n" + (f"\n{body}\n" if body else "") | ||
| return tagging.TagInfo(package=package, version=version, content=content) | ||
|
|
||
|
|
||
| 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 | ||
| ``clean_next_changelog(package.path)``, so the signature matches. | ||
| """ | ||
| base = os.path.join(os.getcwd(), package_path, NEXTCHANGES_DIR) | ||
| for slug, _ in NEXTCHANGES_SECTIONS: | ||
| section_dir = os.path.join(base, slug) | ||
| if not os.path.isdir(section_dir): | ||
| continue | ||
| for name in sorted(os.listdir(section_dir)): | ||
| if name.endswith(".md") and name != "README.md": | ||
| tagging.gh.delete_file(os.path.join(section_dir, name)) | ||
|
|
||
| version_path = _version_path(package_path) | ||
| with open(version_path) as f: | ||
| released = tagging.Version.parse(f.read().strip().lstrip("v")) | ||
| tagging.gh.add_file(version_path, f"{released.next_release_version()}\n") | ||
|
|
||
|
|
||
| def _delete_file(self, loc: str): | ||
| """``git rm`` equivalent for GitHubRepo: stage a tree deletion (sha=None).""" | ||
| local_path = os.path.relpath(loc, os.getcwd()) | ||
| print(f"Deleting file {local_path}") | ||
| self.changed_files.append(tagging.InputGitTreeElement(path=local_path, mode="100644", type="blob", sha=None)) | ||
|
|
||
|
|
||
| def install_nextchanges() -> None: | ||
| """Rebind the tagging seams to the CLI's .nextchanges behavior.""" | ||
| tagging.GitHubRepo.delete_file = _delete_file | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider enhancing the upstream tagging.py implementation of |
||
| tagging.get_next_tag_info = get_next_tag_info | ||
| tagging.clean_next_changelog = clear_nextchanges | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| install_nextchanges() | ||
| tagging.validate_git_root() | ||
| tagging.init_github() | ||
| tagging.process() | ||
|
Comment on lines
+167
to
+169
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider an upstream change to have a single |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expected and in line with updated tagging.yml rewrite in Taskfile