Skip to content

Commit d473af0

Browse files
authored
Tweak the changelog add command (baserow#5246)
1 parent 1e91a0e commit d473af0

5 files changed

Lines changed: 252 additions & 17 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
name: create-changelog
3+
description: Generate a changelog entry for the current Git branch by inspecting changed files, classifying the domain and type, writing a short public-friendly message, and running `just changelog add` after the user confirms. Use this skill whenever the user asks to "create a changelog", "add a changelog entry", "changelog this branch", "write changelog", or otherwise signals they want to record what their branch changes — even if they don't mention `just` or the exact command.
4+
---
5+
6+
# create-changelog
7+
8+
Create a changelog entry for the work on the current Git branch. The entry is registered by running:
9+
10+
```
11+
just changelog add --domain {domain} --type {type} --message '{message}'
12+
```
13+
14+
## Workflow
15+
16+
Follow these steps in order. Do not skip ahead — the user confirms before anything is committed.
17+
18+
### 1. Gather the diff
19+
20+
Run these commands from the repository root to understand what has changed on this branch relative to `develop`:
21+
22+
```bash
23+
git rev-parse --abbrev-ref HEAD
24+
git diff --name-status develop...HEAD
25+
git diff --stat develop...HEAD
26+
git log develop..HEAD --oneline
27+
```
28+
29+
`develop...HEAD` (three dots) gives the changes introduced on this branch since it diverged from `develop`, which is what a changelog entry should describe.
30+
31+
If any of the following is true, stop and tell the user there's nothing to changelog:
32+
33+
- `git diff --name-status develop...HEAD` is empty.
34+
- The current branch is `develop` itself, or is `main`/`master`.
35+
- `develop` does not exist as a ref (in which case mention it and ask how they'd like to proceed).
36+
37+
If the diff is large enough that the file list alone isn't informative, also peek at `git diff develop...HEAD -- <path>` for a few of the most-changed files to get a sense of the substance of the change. Prefer looking at a handful of representative files over dumping the entire diff.
38+
39+
### 2. Pick the domain
40+
41+
The domain must be exactly one of: `core`, `database`, `dashboard`, `builder`, `automation`, `integration`. These correspond to modules in the codebase.
42+
43+
Choose the domain with the most changed files. Use directory names and file paths as the primary signal — e.g. files under a `dashboard/` directory belong to `dashboard`. If paths are ambiguous, fall back to the substance of the changes (e.g. migration files → `database`, webhook/third-party API code → `integration`).
44+
45+
If there's a close tie, prefer the domain that contains the change with the most lines modified, or the one the branch name and commit messages most clearly point at. Don't overthink it — pick the single best match.
46+
47+
### 3. Pick the type
48+
49+
The type must be exactly one of: `bug`, `feature`, `refactor`, `breaking_change`.
50+
51+
Rough guide:
52+
53+
- `bug` — fixes incorrect behavior. Signals: commit messages with "fix", "bug", "regression"; small targeted changes; tests added alongside a fix.
54+
- `feature` — adds capability that wasn't there before. Signals: new files, new endpoints, new UI, new configuration options.
55+
- `refactor` — reshapes existing code without changing behavior. Signals: moves/renames, no new functionality, no bug being fixed, tests largely unchanged.
56+
- `breaking_change` — changes that require consumers to update. Signals: removed/renamed public APIs, changed function signatures, database migrations that drop columns, config format changes.
57+
58+
A breaking change beats the other labels when it applies — if a refactor removes a public function, it's a `breaking_change`. Otherwise, pick the single best fit and move on.
59+
60+
### 4. Write the message
61+
62+
The message describes what changed, in plain English, for a mixed audience of developers and non-technical users reading a public changelog.
63+
64+
Rules:
65+
66+
- **Max 100 characters**, including spaces. Count them.
67+
- Plain English, no jargon, no internal code names, no file paths, no class names.
68+
- Present tense, sentence case, no trailing period. Start with a verb when natural.
69+
- Describe the user-visible effect, not the implementation. "Speeds up dashboard loading" beats "Adds index to dashboard_widgets.user_id".
70+
- Don't start with the type or domain (e.g. don't write "Fix: ..." — the type and domain are separate fields).
71+
- Single line, no newlines.
72+
73+
**Examples:**
74+
75+
Good:
76+
- `Dashboards now load faster when you have many widgets`
77+
- `Fixes an error that could lose changes when saving automations`
78+
- `Adds a bulk import option for contacts`
79+
80+
Too technical:
81+
- `Refactor DashboardWidget.render() to use memoization`
82+
- `Patch NPE in AutomationSaver.persist()`
83+
84+
Too vague:
85+
- `Various improvements`
86+
- `Bug fixes`
87+
88+
### 5. Show the proposal and get confirmation
89+
90+
Present the chosen values to the user in this exact shape so they're easy to scan:
91+
92+
```
93+
Domain: <domain>
94+
Type: <type>
95+
Message: <message>
96+
```
97+
98+
Follow with a one- or two-sentence rationale explaining *why* you picked that domain and type (e.g. "7 of 9 changed files are under `builder/`, and the branch adds a new step type, so I called it a new feature.") and then the full command that will be run:
99+
100+
```
101+
just changelog add --domain <domain> --type <type> --message '<message>'
102+
```
103+
104+
Ask the user to confirm, or to tell you what to change. If they ask for changes, redo whichever steps are affected and present an updated proposal — don't run the command until the user explicitly confirms.
105+
106+
### 6. Execute
107+
108+
Once the user confirms, run the command:
109+
110+
```bash
111+
just changelog add --domain <domain> --type <type> --message '<message>'
112+
```
113+
114+
Wrap the message in single quotes so apostrophes, spaces, and punctuation pass through correctly. If the message itself contains a single quote, close-escape-open it (`'\''`) or rewrite the message to avoid it — preferably the latter, since changelog messages rarely need apostrophes.
115+
116+
The issue number is automatically extracted from the branch name (e.g. a branch named `1234-fix-something` yields issue `1234`). If no issue number is found in the branch name, the entry is created without one.
117+
118+
Show the command's output to the user and confirm it succeeded. If `just` errors (for example, the arguments aren't accepted), surface the error and offer to adjust and retry.
119+
120+
## Notes
121+
122+
- If the user runs this skill multiple times in one session with different intents, re-run the diff — don't reuse cached results, because the branch state may have changed.

changelog/src/changelog.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python3
22

3+
34
import os
45
import shutil
56
from pathlib import Path
@@ -17,38 +18,69 @@
1718
# Parent directory
1819
default_path = str(Path(os.path.dirname(__file__)).parent)
1920

21+
DOMAIN_TYPES = list(domain_types.keys())
22+
ENTRY_TYPES = list(changelog_entry_types.keys())
23+
2024

2125
@app.command()
22-
def add(working_dir: Optional[str] = typer.Option(default=default_path)):
23-
domain_type = typer.prompt(
26+
def add(
27+
working_dir: Optional[str] = typer.Option(default=default_path),
28+
domain: Optional[str] = typer.Option(None, "--domain", help=f"The module domain this changelog pertains to. One of: {', '.join(DOMAIN_TYPES)}."),
29+
entry_type: Optional[str] = typer.Option(
30+
None, "--type", help=f"The entry type this changelog is. One of: {', '.join(ENTRY_TYPES)}.",
31+
),
32+
message: Optional[str] = typer.Option(
33+
None, "--message", help="The changelog message. Describe in non-technical language what the bug, feature or refactor accomplishes."
34+
),
35+
issue: Optional[str] = typer.Option(None, "--issue", help="The GitHub issue ID. Defaults to finding it through the branch name prefix."),
36+
):
37+
domain_type = domain or typer.prompt(
2438
"Domain",
25-
type=Choice(list(domain_types.keys())),
39+
type=Choice(DOMAIN_TYPES),
2640
default=DatabaseDomain.type,
2741
)
28-
changelog_type = typer.prompt(
42+
if domain_type not in DOMAIN_TYPES:
43+
raise typer.BadParameter(
44+
f"must be one of: {', '.join(DOMAIN_TYPES)}", param_hint="--domain"
45+
)
46+
47+
changelog_type = entry_type or typer.prompt(
2948
"Type of changelog",
30-
type=Choice(list(changelog_entry_types.keys())),
49+
type=Choice(ENTRY_TYPES),
3150
default=BugChangelogEntry.type,
3251
)
33-
issue_number = typer.prompt(
34-
"Issue number", type=str, default=ChangelogHandler.get_issue_number() or ""
35-
)
36-
37-
message = typer.prompt("Message", default="")
38-
39-
if issue_number.isdigit():
52+
if changelog_type not in ENTRY_TYPES:
53+
raise typer.BadParameter(
54+
f"must be one of: {', '.join(ENTRY_TYPES)}",
55+
param_hint="--type",
56+
)
57+
if issue is not None:
58+
issue_number = issue
59+
elif domain is not None and entry_type is not None and message is not None:
60+
issue_number = ChangelogHandler.get_issue_number() or ""
61+
else:
62+
issue_number = typer.prompt(
63+
"Issue number", type=str, default=ChangelogHandler.get_issue_number() or ""
64+
)
65+
final_message = message or typer.prompt("Message", default="")
66+
67+
if issue_number and not str(issue_number).isdigit():
68+
raise typer.BadParameter("must be a numeric GitHub issue ID", param_hint="--issue")
69+
70+
if str(issue_number).isdigit():
4071
issue_number = int(issue_number)
4172

4273
if issue_number == "":
4374
issue_number = None
4475

45-
ChangelogHandler(working_dir).add_entry(
76+
path = ChangelogHandler(working_dir).add_entry(
4677
domain_type,
4778
changelog_type,
48-
message,
79+
final_message,
4980
issue_number=issue_number,
5081
issue_origin="github", # All new changelogs originate from GitHub
5182
)
83+
typer.echo(path)
5284

5385

5486
@app.command()

changelog/src/changelog_entry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import abc
22
import os
33
from datetime import datetime, timezone
4-
from typing import Dict, List, Optional, Union
4+
from typing import Dict, List, Optional, Union, Any
55

66
GITLAB_URL = os.environ.get("GITLAB_URL", "https://gitlab.com/baserow/baserow")
77
GITHUB_URL = os.environ.get("GITHUB_URL", "https://github.com/baserow/baserow")
@@ -21,7 +21,7 @@ def generate_entry_dict(
2121
issue_origin: str,
2222
issue_number: Optional[int] = None,
2323
bullet_points: List[str] = None,
24-
) -> Dict[str, any]:
24+
) -> Dict[str, Any]:
2525
if bullet_points is None:
2626
bullet_points = []
2727

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import json
2+
3+
from typer.testing import CliRunner
4+
5+
from src.changelog import app
6+
7+
runner = CliRunner()
8+
9+
10+
def _invoke_add(tmp_path, domain="database", entry_type="bug", message="A test fix", issue="42"):
11+
return runner.invoke(
12+
app,
13+
[
14+
"add",
15+
"--working-dir", str(tmp_path),
16+
"--domain", domain,
17+
"--type", entry_type,
18+
"--message", message,
19+
"--issue", issue,
20+
],
21+
)
22+
23+
24+
def test_add_creates_entry_file(tmp_path):
25+
result = _invoke_add(tmp_path)
26+
assert result.exit_code == 0
27+
file_path = result.output.strip()
28+
assert file_path.endswith(".json")
29+
with open(file_path) as f:
30+
entry = json.load(f)
31+
assert entry["message"] == "A test fix"
32+
assert entry["domain"] == "database"
33+
34+
35+
def test_add_includes_issue_number(tmp_path):
36+
result = _invoke_add(tmp_path, issue="99")
37+
assert result.exit_code == 0
38+
file_path = result.output.strip()
39+
with open(file_path) as f:
40+
entry = json.load(f)
41+
assert entry["issue_number"] == 99
42+
43+
44+
def test_add_without_issue(tmp_path):
45+
result = _invoke_add(tmp_path, issue="")
46+
assert result.exit_code == 0
47+
file_path = result.output.strip()
48+
with open(file_path) as f:
49+
entry = json.load(f)
50+
assert entry.get("issue_number") is None
51+
52+
53+
def test_add_invalid_domain(tmp_path):
54+
result = _invoke_add(tmp_path, domain="not_a_real_domain")
55+
assert result.exit_code != 0
56+
57+
58+
def test_add_invalid_type(tmp_path):
59+
result = _invoke_add(tmp_path, entry_type="not_a_real_type")
60+
assert result.exit_code != 0
61+
62+
63+
def test_add_invalid_issue(tmp_path):
64+
result = _invoke_add(tmp_path, issue="not-a-number")
65+
assert result.exit_code != 0
66+
67+
68+
def test_add_without_issue_flag_runs_noninteractively(tmp_path):
69+
result = runner.invoke(
70+
app,
71+
[
72+
"add",
73+
"--working-dir", str(tmp_path),
74+
"--domain", "database",
75+
"--type", "bug",
76+
"--message", "A test fix",
77+
],
78+
)
79+
assert result.exit_code == 0
80+
assert result.output.strip().endswith(".json")

justfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1175,10 +1175,11 @@ env-clear:
11751175
fi
11761176

11771177
# Run changelog command (e.g., just changelog add, just changelog release 2.3.0)
1178+
[positional-arguments]
11781179
[group('5 - utilities')]
11791180
[doc("Changelog: just changelog <add|release|generate|purge>")]
11801181
changelog *args:
1181-
cd backend && uv run --group changelog python ../changelog/src/changelog.py {{ args }}
1182+
cd backend && uv run --group changelog python ../changelog/src/changelog.py "$@"
11821183

11831184
# Run changelog tests
11841185
[group('4 - testing')]

0 commit comments

Comments
 (0)