Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
747fc46
Add .nextchanges fragment directory and collator
janniklasrose Jun 11, 2026
368042d
Add changelog-collate release workflow
janniklasrose Jun 11, 2026
7707dd1
Point contributors at .nextchanges in PR template and pr-checklist
janniklasrose Jun 11, 2026
6543e30
Demo: add test changelog fragments (revert after review)
janniklasrose Jun 12, 2026
bf196ac
Demo: collate test changelog fragments into NEXT_CHANGELOG.md (revert…
janniklasrose Jun 12, 2026
73d3550
Revert NEXT_CHANGELOG.md
janniklasrose Jun 12, 2026
25cb944
Reuse the changelog-collate task in the collate workflow
janniklasrose Jun 12, 2026
3108bff
Tighten .nextchanges guidance in pr-checklist
janniklasrose Jun 12, 2026
e92c6be
Render .nextchanges/ into CHANGELOG.md via a tagging.py wrapper
janniklasrose Jul 3, 2026
38eef21
Remove the NEXT_CHANGELOG.md collate bridge
janniklasrose Jul 3, 2026
1fd1ae2
Update changelog docs for direct-to-CHANGELOG release
janniklasrose Jul 3, 2026
74f3ddd
Merge remote-tracking branch 'origin/main' into janniklasrose/changel…
janniklasrose Jul 3, 2026
d774453
Name the release wrapper release_tagging.py, keep tagging.py pristine
janniklasrose Jul 3, 2026
dbb13e7
Revert ruff.toml comment
janniklasrose Jul 3, 2026
3a32f0a
Revert tagging.py
janniklasrose Jul 3, 2026
ae081d0
Tidy Taskfile
janniklasrose Jul 3, 2026
4b17443
Rename changelog task
janniklasrose Jul 3, 2026
04e637a
Demo: add test changelog fragments (revert after review)
janniklasrose Jul 3, 2026
7a290e2
Demo: render fragments into CHANGELOG.md (revert after review)
janniklasrose Jul 3, 2026
e0a2aa4
Revert "Demo: render fragments into CHANGELOG.md (revert after review)"
janniklasrose Jul 3, 2026
d2093d8
Revert "Demo: add test changelog fragments (revert after review)"
janniklasrose Jul 3, 2026
1d06899
Match CHANGELOG.md formatting in release_tagging.py render
janniklasrose Jul 3, 2026
e925f8d
Demo: add test changelog fragments (revert after review)
janniklasrose Jul 3, 2026
a339ab0
Demo: render fragments into CHANGELOG.md (revert after review)
janniklasrose Jul 3, 2026
fcdb4cc
Revert "Demo: render fragments into CHANGELOG.md (revert after review)"
janniklasrose Jul 3, 2026
e334168
Revert "Demo: add test changelog fragments (revert after review)"
janniklasrose Jul 3, 2026
0826ce0
Update changelog agent rule for .nextchanges/
janniklasrose Jul 3, 2026
2ee9000
Reject unexpected files under .nextchanges/
janniklasrose Jul 3, 2026
606a0ee
Expand PR links via update_github_links, not in the release wrapper
janniklasrose Jul 3, 2026
129fb7c
Clarify PR-link expansion in .nextchanges README
janniklasrose Jul 3, 2026
05f0cd0
Update .agent/skills/onboard-team-area/SKILL.md
janniklasrose Jul 10, 2026
47ad6f0
Apply suggestions from code review
janniklasrose Jul 10, 2026
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
5 changes: 3 additions & 2 deletions .agent/rules/changelog.md
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`.
6 changes: 3 additions & 3 deletions .agent/skills/onboard-team-area/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ node .github/scripts/owners.js validate
./task checks
```

No `NEXT_CHANGELOG.md` entry; this is ownership/config only. Write the PR using the `.github/PULL_REQUEST_TEMPLATE.md` sections (Why / Changes / Tests).
No `.nextchanges/` entry; this is ownership/config only. Write the PR using the `.github/PULL_REQUEST_TEMPLATE.md` sections (Why / Changes / Tests).

## Experimental vs stable, and graduation

- **Experimental** — code under `experimental/<area>/`, tests under `acceptance/experimental/<area>/`. Register it under the hidden parent in `cmd/experimental/experimental.go`, or top-level in `cmd/cmd.go` with `Hidden: true` (as `ssh` does). Experimental commands still ship enabled in every release; `Hidden` only removes them from `--help`, it does not gate or compile them out. No `NEXT_CHANGELOG` entries while experimental. To hand a build to testers, push a `bugbash-<topic>` branch (auto-builds a snapshot) and share the `internal/bugbash/exec.sh` one-liner.
- **Graduating to stable** — `git mv` the feature-complete commands to `cmd/<area>/` + `libs/<area>/`, register them top-level in `cmd/cmd.go`, keep the old `experimental` paths as deprecated cobra aliases (`sub.Hidden = true`, `sub.Deprecated = '...'`), add OWNERS rules for the new stable paths, and add the `NEXT_CHANGELOG` entry. See `experimental/aitools` graduating to top-level `aitools` (PR #4917) as the worked example.
- **Experimental** — code under `experimental/<area>/`, tests under `acceptance/experimental/<area>/`. Register it under the hidden parent in `cmd/experimental/experimental.go`, or top-level in `cmd/cmd.go` with `Hidden: true` (as `ssh` does). Experimental commands still ship enabled in every release; `Hidden` only removes them from `--help`, it does not gate or compile them out. No `.nextchanges/` entries while experimental. To hand a build to testers, push a `bugbash-<topic>` branch (auto-builds a snapshot) and share the `internal/bugbash/exec.sh` one-liner.
- **Graduating to stable** — `git mv` the feature-complete commands to `cmd/<area>/` + `libs/<area>/`, register them top-level in `cmd/cmd.go`, keep the old `experimental` paths as deprecated cobra aliases (`sub.Hidden = true`, `sub.Deprecated = '...'`), add OWNERS rules for the new stable paths, and add a `.nextchanges/` entry. See `experimental/aitools` graduating to top-level `aitools` (PR #4917) as the worked example.
10 changes: 5 additions & 5 deletions .agent/skills/pr-checklist/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ If an agent (you) authored or substantially helped author the PR, disclose it on

## Changelog entry

Add a `NEXT_CHANGELOG.md` entry when your change is user-visible. CI generates the real `CHANGELOG.md` from `NEXT_CHANGELOG.md` at release time, so never hand-edit `CHANGELOG.md` directly.
Add a changelog fragment under `.nextchanges/` when your change is user-visible. Each PR adds its own file, so entries never conflict between PRs. The release renders these fragments into the real `CHANGELOG.md`, so never hand-edit `CHANGELOG.md` directly.

**When to add an entry:**
- New or changed CLI command, flag, or subcommand behavior
Expand All @@ -69,7 +69,7 @@ Add a `NEXT_CHANGELOG.md` entry when your change is user-visible. CI generates t
- Auto-generated output changes without a corresponding user-facing change

**How to add:**
- Pick the right section (`CLI`, `Bundles`, `Dependency updates`) under the current `## Release vX.Y.Z` header.
- One or two sentences, user-facing language, no Jira links.
- Reference the PR number once it's open: after `gh pr create`, edit the entry to append ` (#NNNN)` or similar matching nearby entries.
- Match the voice and tense of the existing entries in the file.
- Create `.nextchanges/<section>/<name>.md`, picking the section folder that fits: `cli`, `bundles`, `dependency-updates`, `notable-changes`, or `api-changes`. `<name>` is arbitrary (a feature name or your PR number) — just keep it unique.
- Write one or two sentences in user-facing language, no Jira links. The leading `* ` is optional. Match the voice and tense of existing changelog entries.
- A PR link is optional: write `(#NNNN)` (with NNNN being the PR number) in the text and it's expanded to a full link automatically.
- See `.nextchanges/README.md` for details.
3 changes: 2 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ For example, were there any decisions behind the change that are not reflected i
<!-- How have you tested the changes? -->

<!-- If your PR needs to be included in the release notes for next release,
add a separate entry in NEXT_CHANGELOG.md as part of your PR. -->
add a changelog fragment: create .nextchanges/<section>/<name>.md with a
one-line description (e.g. .nextchanges/cli/quickstart.md). See .nextchanges/README.md. -->
2 changes: 1 addition & 1 deletion .github/workflows/changelog-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'override-changelog-guard')"
run: |-
echo "::error::CHANGELOG.md should not be modified directly by PRs."
echo "The release workflow manages CHANGELOG.md automatically from NEXT_CHANGELOG.md."
echo "The release workflow manages CHANGELOG.md automatically from .nextchanges/ fragments."
echo "If this is intentional, add the 'override-changelog-guard' label to this PR."
exit 1
4 changes: 2 additions & 2 deletions .github/workflows/tagging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ jobs:
PACKAGES: ${{ inputs.packages }}
run: |
if [ -n "$PACKAGES" ]; then
uv run --locked internal/genkit/tagging.py --package "$PACKAGES"
uv run --locked internal/genkit/release_tagging.py --package "$PACKAGES"
else
uv run --locked internal/genkit/tagging.py
uv run --locked internal/genkit/release_tagging.py
Comment on lines -77 to +79

Copy link
Copy Markdown
Contributor Author

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

fi
- name: Upload created tags artifact
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*.dll
*.so
*.dylib
cli
# Root binary from a bare `go build`; anchored so it doesn't also ignore
# nested paths like .nextchanges/cli/.
/cli

# Test binary, built with `go test -c`
*.test
Expand Down
53 changes: 53 additions & 0 deletions .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.
Empty file.
Empty file added .nextchanges/bundles/.gitkeep
Empty file.
Empty file added .nextchanges/cli/.gitkeep
Empty file.
Empty file.
Empty file.
1 change: 1 addition & 0 deletions .nextchanges/version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.8.0
22 changes: 17 additions & 5 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,19 @@ tasks:
- "./tools/validate_whitespace.py --fix"

links:
desc: Update GitHub links in docs
desc: Update GitHub links in CHANGELOG.md and .nextchanges/ fragments
sources:
- "**/*.md"
- CHANGELOG.md
- ".nextchanges/**/*.md"
- tools/update_github_links.py
cmds:
- "./tools/update_github_links.py"

check-changelog:
desc: Validate .nextchanges fragment placement
cmds:
- "./tools/validate_nextchanges.py"

deadcode:
desc: Check for dead code
sources:
Expand All @@ -303,14 +309,15 @@ tasks:
- "! git grep -lF databricks.com -- '*uv.lock'"

checks:
desc: Run quick checks (tidy, whitespace, links, deadcode, uv.lock)
desc: Run quick checks (tidy, whitespace, links, deadcode, changelog, uv.lock)
# Sequential: `tidy` rewrites go.mod/go.sum and any future tidy work
# touching more paths should not race with whitespace/link scanners.
cmds:
- task: tidy
- task: ws
- task: links
- task: deadcode
- task: check-changelog
- task: check-uv-lock

install-pythons:
Expand Down Expand Up @@ -878,12 +885,17 @@ tasks:
- rm .github/workflows/next-changelog.yml
- mv tagging.py internal/genkit/tagging.py
- mv tagging.py.lock internal/genkit/tagging.py.lock
# The release runs internal/genkit/release_tagging.py, a repo-owned wrapper
# that renders CHANGELOG.md from .nextchanges/ around the synced tagging.py.
# Point the workflow at it (genkit emits a bare `tagging.py`) and give the
# wrapper a matching lock (it declares the same deps).
- |
if [ "$(uname)" = "Darwin" ]; then
sed -i '' 's|tagging.py|internal/genkit/tagging.py|g' .github/workflows/tagging.yml
sed -i '' 's|tagging.py|internal/genkit/release_tagging.py|g' .github/workflows/tagging.yml
else
sed -i 's|tagging.py|internal/genkit/tagging.py|g' .github/workflows/tagging.yml
sed -i 's|tagging.py|internal/genkit/release_tagging.py|g' .github/workflows/tagging.yml
fi
- cp internal/genkit/tagging.py.lock internal/genkit/release_tagging.py.lock
- "{{.GO_TOOL}} yamlfmt .github/workflows/tagging.yml"
- task: ws

Expand Down
169 changes: 169 additions & 0 deletions internal/genkit/release_tagging.py
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this script once all of this lands?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tools/update_github_links.py? No, that's still used in task links / task checks.

Maybe misleading part of the docstring here that can just be omitted entirely - render_nextchanges just takes the fragments verbatim

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its purpose was only to check NEXT_CHANGELOG.md.

``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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider enhancing the upstream tagging.py implementation of GitHubRepo with this method to avoid monkey-patching here

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider an upstream change to have a single main()

Loading
Loading