| name | write-changelog |
| description | Update docs/overview/changelog.md with entries for changes since a base commit, in Zulip's curated changelog style. |
| argument-hint | [base_commit] |
Write Changelog
Surveys commits since a base point and folds user-facing entries into
docs/overview/changelog.md in the historical curated style. The hard
part is judgment — which commits matter to users, how tersely to phrase
them, and when to fold a refinement into an existing entry rather than
add a new bullet.
This tool is designed to replace a laborious manual
git log --stat <base>..upstream/main
not-taking process for identifying changes to consider recording in
the changelog.
When to use
- An in-development major release (
### Zulip Server X.0 is
_Unreleased_) needs its draft refreshed. Typical cadence: every
1–4 weeks between betas.
- A maintenance release on a stable branch (e.g.,
11.5 on
11.x) is being prepared. Section shape and rules differ —
see "Maintenance release mode" below.
Workflow
1. Identify scope
Resolve the base commit (use the user's argument if given; otherwise
default to the last commit on upstream/main that materially touched
the changelog):
git log -10 --format=%H upstream/main -- docs/overview/changelog.md
If the base is a stable release tag (e.g., 11.4), this is a
maintenance release — switch to "Maintenance release mode" below.
Read the existing in-progress section
(### Zulip Server X.0 / X.0-betaN) end-to-end. The skill folds
new content into the existing organization rather than reinventing it.
The changelog for a beta is always a draft of the changelog of a final
release. So if the existing section is ### Zulip Server X.0-betaN
and a new beta is being cut, rename the heading and update the
_Released_ date in place, rather than adding new changes.
Check for a parallel maintenance release that may also have shipped
in the same window. List tags by date and look for any stable
release whose major version differs from the target's:
git for-each-ref --sort=creatordate --format='%(refname:short) %(creatordate:short)' 'refs/tags/*'
Changelogs for maintenance releases always enter the main branch via
cherry-picking from the original changelog on the maintenance release
branch.
2. Survey commits
Start with the commit list:
git log --stat <base>..upstream/main --format="%H %s"
Then walk these signal sources, in roughly decreasing reliability:
-
api_docs/changelog.md deltas. For each new feature-level
entry, ask: "Does this need a Full changelog bullet, an Upgrade
note, both, or neither?" Keep in mind that sometimes, backend
changes for an unfinished feature and merged months before the
feature is completed.
git diff <base>..upstream/main -- api_docs/changelog.md
-
Help-center source tree. Newly-added articles signal newly
documented features. The path varies by era: currently
starlight_help/src/content/docs/*.mdx; pre-Starlight (≤9.x) it
was help/*.md then templates/zerver/help/*.md. Help-center
commits often land days to weeks after the underlying feature,
so this can catch features whose original commit predates the
base. Bulk migration deltas inside existing files are noise.
git log --diff-filter=A --name-only <base>..upstream/main \
-- starlight_help/ help/ templates/zerver/help/
-
zulip_update_announcements.py. The in-app "what's new" feed is curated
here; every new entry is a changelog highlight candidate.
git diff <base>..upstream/main -- zerver/lib/zulip_update_announcements.py
-
Added, Removed, or greatly changed integrations.
git diff --stat <base>..upstream/main zerver/lib/integrations.py zerver/webhooks/*/*.{md,py}
-
CVEs are not detected mechanically. They're managed manually
by the security team and are essentially always backported. By
the time a major release ships, the relevant CVEs are already
documented in the maintenance changelogs that shipped during the
cycle — cross-reference those rather than re-discovering. Ask the
security team if anything is unclear.
For ambiguous commit subjects, read the body with git show <sha>.
Cluster commits sharing a Fixes #N reference; they often map to
one entry. For commits whose user impact is still unclear, consider
having a subagent follow links to GitHub issues or chat.zulip.org
threads.
3. Filter to user-facing changes
Skip — almost never changelog material:
- Refactors and prep commits ("Extract", "Refactor", "Move",
"Inline"); pure typing refactors ("Replace
dict[str, Any] with
dataclass", "Use literal types").
- Test infrastructure (
tests:, test_X:).
- Dev tooling and CI (
requirements:, dependencies:, lint:,
mypy:, eslint:, ruff:, ci:, provision:, install-*:,
Vagrantfile:, pre-commit:).
- Translation / metadata churn (
locale/, mailmap).
docs: commits that fix typos, dead labels, or canonical-URL
meta. Include docs: commits that document a behavior change
or improve developer-facing tooling (e.g., autogenerated Sphinx
labels, new install-flag documentation).
- API capability commits whose
api_docs/changelog.md entry
preserves the legacy event/endpoint format. Protocol
optimizations behind a client capability flag aren't user-facing
and need only be documented in the separate API changelog.
- Migrations whose user-visible effect is described by a separate
feature commit.
- Reverts of unreleased commits (and remove the original entry from
the changelog if it had been added).
- Same-cycle fixups. A bug fix or small extension to a feature
added in the same release does not get its own entry — the
original feature entry already represents the work.
- Visual polish fixups ("Increase gap", "Vertically align",
"Fix spacing", "Restyle border-radius"). Don't emit a per-commit
entry; if there are enough such commits, roll them up into one
umbrella bullet ("Fixed several visual polish issues across
modals, popovers, …").
- Anything internal that doesn't impact the experience of users,
organization administrators, or server administrators. A handful
of major technical improvements or extended migrations can be
highlighted at the end of the section.
Include:
- New user-visible features, redesigns, settings, search operators,
keyboard shortcuts, Markdown syntax additions.
- New webhook integrations, or material changes to existing ones
(new event types, silent-mention support, output rewrites).
- Performance work users will notice ("2x faster web app load in
large organizations").
- Security/hardening: non-CVE hardening (e.g., a dependency upgrade
closing a low-severity issue, an XSS-resistance audit) lands as a
short Improved/Hardened bullet in the Full feature changelog.
CVEs use the dedicated format in "Maintenance release mode".
- Platform support changes (added/removed OS, Postgres,
Python major versions).
- API/settings changes that might require a system administrator to update
common classes of integrations with their Zulip server.
- Themed-cluster rollups: when many small improvements share an
area, group them under one bullet — e.g., "Fixed several visual
polish issues across modals, popovers, the saved-snippets
dropdown, stream-privacy decoration, recent-view headers, and
subscriber-list scrolling." (12.0-beta2) captures 10+ commits no
one individually merits. The shape is "Fixed/Improved several X
across A, B, C, …".
When in doubt: "can you imagine a user or administrator who would be
sad if this commit were silently reverted before release?" If no,
skip.
4. Draft Full feature changelog entries
(Skip in maintenance mode — that has its own ordering, see below.)
For each candidate, decide: new entry, fold into an existing
entry, or skip. Prefer folding when the new commit refines,
extends, or parallels something already covered (see "Folding
rules" below).
Group entries by the loose ordering used in existing sections:
Added → Redesigned → Improved → Renamed → (clusters in a subsystem) →
Integrations → Fixed → Performance/Security/Infrastructure. Slot each
new entry near existing entries of the same shape or adjacent
topic; don't append to the end of the list.
Important behavior changes that don't cleanly fit any of the
above categories can go before the "Added" section as a small
lead cluster. The 11.0 section is the canonical example: it opens
with bullets like "The compose box now offers to convert large
amounts of pasted text into an uploaded file.", "Policies for
automatically following topics they initiate are now applied
when…", "Notices generated by moving messages are now always
marked as read for the acting user.", "Email notifications and
Notification Bot now use topic permalinks." Each names a behavior
change too narrative-weighted for "Added"/"Improved" but that the
reader needs to know up front.
After drafting, scan for umbrella opportunities. When 4+
commits cluster in one subsystem (webhooks/*, compose:,
portico:) and were individually skipped or look thin on their own,
write a single rollup bullet ("Improved many webhook integrations,
including Travis CI…, PagerDuty…, Jira…").
5. Promote Highlights
A new entry belongs in #### Highlights if it's one of:
- A top-level user-visible feature (a new view, compose mode, auth
backend, media format, major settings panel).
- A redesign large enough that screenshots would change.
- An infrastructure change with broad self-hoster impact (new OS
support, Postgres major version, Docker container rework).
- A protocol or security milestone (e.g., E2EE push notifications
becoming generally available).
- Anything else that could reasonably change whether a reader
is interested in choosing Zulip over alternative chat systems.
For each candidate:
- Check the existing Highlights first. for whether an existing
highlight should be extended.
- Move (don't duplicate) the entry from the full list to
#### Highlights. Sub-features that expand on the highlight stay
in the full list, unless they flow naturally as a second sentence.
- Expand it slightly: Highlights are 1–3 sentences and can
include some context.
- Pattern-match for prominence level. Find the most-similar
existing Highlight (across all past releases) and match it.
- A newly-added help-center article (see Step 2's path list) often
confirms a feature meets the bar.
When polishing existing Highlights (Step 8), aim for the same length
or shorter, not adding clarifying detail on each addition.
When uncertain, leave the entry in the full list and note it as a
Highlights candidate in the summary.
6. Propose Upgrade notes
Upgrade notes go under ### Upgrade notes for X.0. Most runs find
none — that's normal. Propose one when the delta contains:
- A removed or renamed setting in
zproject/*settings.py.
- A required setting that previously had a default, or a default
changed in a way that affects behavior.
- A removed installation platform or dropped Postgres / Python /
Node major version.
- Any other change that could be reasonably expected to require some
system administrators to take manual action as part of the upgrade.
- A migration expected to take a long time on large installations,
or any command admins must run manually.
Detection is mechanical, not abstract:
git diff <base>..upstream/main -- \
zproject/default_settings.py \
zproject/prod_settings_template.py \
zproject/computed_settings.py
git log --diff-filter=D --name-status <base>..upstream/main -- \
'puppet/' 'scripts/' 'zproject/'
git log <base>..upstream/main \
--grep='Remove .* support' \
--grep='no longer support' -i
Treat any net removal from prod_settings_template.py as an
upgrade note candidate by default.
When drafting, mirror historical Upgrade notes style: link to the
relevant operations doc, name affected settings in backticks, and
tell the admin what concrete action to take.
7. Place entries
Edit docs/overview/changelog.md directly. When folding, edit the
existing bullet in place rather than adding a new one.
Redundancy check before each new entry. Grep the existing
target section for the feature/integration name; if something
matches, default to fold or skip. This catches commits whose
subjects look like new-feature additions but actually document or
extend something already in the changelog.
8. Verify
Re-read the full updated section once for:
- Duplicate or near-duplicate entries (dedupe pre-existing ones too
— the previous draft sometimes has them).
- Highlights vs. Full feature changelog dedupe. No bullet
should appear in both. (11.0's "Migrated translation platform
from Transifex to Weblate" was a real historical case where it
ended up in both.)
- Grammar, typos, missing periods.
- Operator and syntax formatting (operators with their colon, e.g.,
mentions:, is:followed; settings in backticks).
- Compound entries that should split, or sibling entries that
should fold.
- Style consistency with the last few releases' sections.
Polish pass. Re-read each existing bullet you didn't touch and
copyedit obvious infelicities.
Retraction check — look for commits walking back prior claims:
git log <base>..upstream/main --grep='retract\|walk back\|incorrectly\|never shipped' -i
If a feature was claimed in the previous draft but reverted or
scoped down, edit or remove the prior entry — don't leave it stale.
Then:
./tools/lint --fix docs/overview/changelog.md
Report a summary: candidate commits surveyed, entries added/folded,
Highlights or Upgrade notes proposed, parallel maintenance release
section if any. Include interesting choices — places where rules
conflicted or you weren't sure what was accurate.
Style rules
When uncertain, find the closest existing bullet in the file and
mimic it.
- One sentence per entry, two only when truly necessary
(e.g., a redesign with multiple aspects worth naming).
- Active voice. Start with
Added, Improved, Redesigned,
Renamed, Removed, Fixed, or a noun-phrase ("The Markdown
process now…", "Channel privacy icons now appear…"). End with a
period.
- No commit-message-style justification. Don't say "matching
Y" or "(Fixes #N)". State what changed, not why. Use
"previously…" only when the prior behavior is itself the
user-visible point of the bullet (e.g., 11.0's "Topic permalinks
now consistently prefer the latest message in a topic for
anchoring; previously, the oldest message was sometimes used.")
— not as bare commit-message justification for a change.
- No exhaustive enumeration. "Many event types" beats listing
five of them.
- Technical names in backticks. Operators with their colon
(
mentions:, is:followed); settings, file types, env vars in
backticks. Use (e.g., ...) (lowercase, with periods).
- Avoid colloquial words that don't fit the documentation's
professional tone. Don't speculate about backports or future
releases — flag those for the user instead.
- Wrap at ~70 chars except links and long identifiers.
- Be precise. Double-check implicatures — e.g., naming "a new
type of GitHub integration" shouldn't suggest there was no prior
GitHub integration.
- Linking to the Help Center on zulip.com or ReadTheDocs via
relative links is welcome (historical entries don't because of
time pressure, not policy).
- Use of Zulip standard terminology and readability for a sysadmin
who has never look at the Zulip implementation. Highlights
should use user-facing terminology like the help center does.
Folding rules
When a new commit refines or extends an existing entry, edit that
entry in place. Examples from the 12.0-beta2 tag:
- KLIPY GIF provider → folded into the existing GIF picker
redesign as "Giphy, Tenor, and KLIPY".
- GIF picker resize → folded into the same redesign as "with nice
keyboard UI, a resizable popover, and…".
- Integrations catalog and integration doc page redesigns →
folded into the existing API/legal docs redesign entry.
- SAML
full_name sync → appended onto the existing SAML email-
sync entry as a second sentence.
- LDAP email-sync extended to periodic
sync_ldap_user_data runs
→ appended onto the existing LDAP/SAML email-sync entry rather
than written as a parallel bullet. (When an X-on-event feature
gets a Y-on-other-event follow-up, write one bullet covering both.)
- Mattermost importer gaining bot/self-DM/multi-team support →
folded onto the existing data-import-tools entry as a second
sentence ("…The Mattermost importer now supports combining
multiple teams, importing bot users, and self-DMs.").
- Removed-integrations cluster (Hubot, Dark Sky, Codeship-as-legacy
in 12.0-beta2) → folded into the existing "Removed several
integrations…" parenthetical list rather than as new bullets.
Only fold removals that share both subsystem path AND feature
category — e.g., webhook removals fold together; data-import
tool removals fold together; don't mix the two (Gitter's data
import tool removal does not fold into a webhook list).
- Operational-follow-up fold: when a bug-fix commit lands together
with a follow-up that adds a cron job, retry, validation, or
detection tool guarding the same class of bug, fold the follow-up
as a second sentence on the originating fix. The 11.1
subscriber-count bullet is the canonical example: "Fixed
subscriber counts after data import being incorrect in the
database, which could cause removing channel subscribers to
crash after a data import. Also added a daily refresh to cached
subscriber counts, in case of race conditions."
When NOT to fold: if no existing entry names the feature area,
write a new bullet. E.g., Discord auth got its own bullet in
12.0-beta2 because no prior auth-backend bullet covered it. The
folding rule is "look for a home" not "always fold."
Within-subsystem rollup. When 4+ commits each lightly improve
distinct items in one subsystem (e.g., five separate webhook
integrations each getting 2–3 polish commits), write one rollup
bullet listing the affected items parenthetically — see the
12.0-beta2 webhook entry: "Improved many webhook integrations,
including Travis CI (…), PagerDuty (…), Jira (…), GitLab (…), and
Harbor (…)." Several consecutive refactor commits that appear
to be from a single pull request should not be treated as
distinct items.
The general pattern: if there's already an entry that names the
feature area, look for a way to grow it before writing a new bullet.
It's often necessary to remove less important details when folding to
preserve the overall level of conciseness of the entry.
Maintenance release mode
When the base is a stable release tag (e.g., 11.4), the workflow
runs but with several differences. 10.3 is the strongest single
exemplar to compare against — it has a clear non-XSS CVE bullet,
a "Fixed an important bug where…" admin-impact phrasing, and
infrastructure tooling bullets at the right granularity. 11.5, 11.6,
and 10.4 are useful secondary references.
Section shape: add ### Zulip Server N.M under
## Zulip Server N.x series — no #### Highlights, no
#### Full feature changelog. Body is a flat bulleted list with
_Released YYYY-MM-DD_ (or _Unreleased_ while drafting).
Survey range: the stable branch, not upstream/main:
git log <prev-stable-tag>..stable-N --format="%H %s"
api_docs/changelog.md will typically be empty for this range —
that's expected, not a signal nothing is user-facing.
Ordering: CVEs first, then user-visible fixes (web/mobile,
email/imports), then self-hosting and infrastructure, then
translations. CVE entries describe the vulnerability, not the
fix — e.g., from 10.3, "CVE-2025-47930: Restrictions on creating
public or private channels were incorrectly not applied when
editing the channel type for an existing channel. This issue only
impacted configurations where users could create private channels
but not public channels, or vice versa." Note any required user
interaction; credit reporters when commit metadata names them.
Same-release fix analysis. Compare against the previous
maintenance release within this major release series. Ask "would a
self-hoster running N.M-1 notice this?".
CVE absorbs same-CVE hardening. Backport commits that fix or
harden the same vulnerability as a CVE entry fold into that CVE
bullet rather than getting their own entry. Look for shared CVE
numbers in commit messages, or path-traversal/sanitize/XSS commits
clustered in time around the CVE-numbered commit.
Splitter, not lumper, but with judgment. Distinct bugs go in
distinct bullets — admins skim for the issue they hit. The folding
rules above are for major-release narrative flow; maintenance
changelogs are reference material. But the splitter rule is
not "include everything you don't fold"; the
"would a self-hoster on N.M-1 actually notice and care" gut-check
still suppresses small internal Puppet refactors, dev-tooling
cleanups, etc. Several small fixes touching the same widget
across one cycle (stream_list:, channel_folders:, sidebar tweaks)
usually fold into a single "Fixed several bugs in the channel list
sidebar" bullet rather than three thin entries.
Tone: descriptive — "Fixed X.", "Improved Y.", "Updated Z." —
minimal prose, name affected settings in backticks, mention exact
subsystem (postfix, setup-certbot, nginx, S3, the email
gateway). Slightly more admin-facing detail than major-release
entries.
Upgrade notes are rare in maintenance releases (backports usually
preserve compatibility) but possible. Apply Step 6's commands; expect
zero hits most of the time.
Final notes
Report to the user notes on any changelog items that might need
follow-up work or documentation before we can happily advertise them
in a release blog post. Report to the user any changelog items that
should perhaps be advertised in landing pages
(templates/corporate/).