The contract of updates
You just merged a PR that fixes a panic in parse_json. You bump the version to 0.4.2 and run cargo publish. A downstream user sees the update in their dependency tree. They run cargo update. Their build fails. They open an issue asking what broke. You check the git log. It's a wall of "fix typo", "wip", "refactor", and "update deps". You can't tell them what changed. They can't figure out how to fix their code. They pin your crate to the old version and stop watching for updates.
This is the changelog gap. A changelog is the contract between you and your users. It tells them exactly what changed, why the version number jumped, and whether they can upgrade safely. Without it, every release is a gamble. Users assume the worst when documentation is missing. They assume you broke something and didn't notice. Your crate becomes frozen in time because no one trusts the update.
What a changelog actually does
A changelog is a human-readable record of changes between versions. It lives in CHANGELOG.md at the root of your crate. The community standard is the "Keep a Changelog" format. This format uses version headers, ISO 8601 dates, and categorized bullet points. It's not just a list of commits. It's a curated summary of impact.
The changelog serves three purposes. First, it helps users decide if they can upgrade. They scan for breaking changes or fixes for bugs they hit. Second, it explains Semantic Versioning jumps. A major version bump means breaking changes. The changelog lists them. A patch version means fixes. The changelog lists them. Third, it acts as a historical record for you. When you're debugging a regression six months later, the changelog tells you what changed in that release. It captures intent that git diffs often obscure.
The community convention is strict about the file name. Use CHANGELOG.md. Not Changelog.md. Not CHANGELOG.txt. Linux is case-sensitive. Tools like cargo-release and GitHub look for the exact name. If you get the case wrong, tools miss your changelog. Also, use ISO 8601 dates. 2024-05-20. Not May 20, 2024. ISO dates sort correctly in text editors and scripts. This small detail saves headaches when you have hundreds of releases.
The standard format
The Keep a Changelog format groups changes into categories. The standard categories are Added, Changed, Deprecated, Removed, Fixed, and Security. Each version gets a header with the version number and release date. Changes go under the appropriate category.
# Changelog
## [Unreleased]
## [0.4.2] - 2024-05-20
### Fixed
- Panic in `parse_json` when input contains trailing commas.
## [0.4.1] - 2024-05-15
### Added
- `Parser::from_bytes` for zero-copy parsing.
### Changed
- Renamed `buf.len()` to `buf.size()` for consistency with `std::vec::Vec`.
The Unreleased section is the working draft. You add entries here as you merge PRs. When you cut a release, you move the Unreleased block down, add the date, and create a new empty Unreleased block at the top. This workflow ensures the changelog is always up to date. It prevents the "I forgot to write the changelog" disaster at release time.
Link to PRs or issues when relevant. This gives users a path to more detail if they need it.
### Fixed
- Race condition in connection pool when idle timeout fires during active request. [#142](https://github.com/user/crate/issues/142)
Keep entries concise but specific. "Fixed bugs" tells the user nothing. "Fixed panic in parse_json when input contains trailing commas" tells the user exactly what is safe. Vague changelogs erode trust. Users stop upgrading because they can't assess the risk.
Mapping changes to SemVer
Semantic Versioning defines what goes in the changelog based on the version bump. The changelog must match the version number. If they don't match, users lose trust.
A major version bump (1.0.0 to 2.0.0) signals breaking changes. The changelog must list every breaking change. Use the Changed, Removed, or Deprecated categories. Explain what broke and how to migrate. If you rename a public function, downstream code breaks with E0432 (unresolved import) or E0061 (function call arguments). The changelog points users to the fix. If you change a return type, users hit E0308 (mismatched types). The changelog warns them.
A minor version bump (1.0.0 to 1.1.0) adds functionality without breaking changes. The changelog lists Added items. It may list Changed items if they are backward-compatible. For example, adding a new field to a struct is compatible. Adding a new method to a trait is not compatible for implementors, so that requires a major bump. The changelog clarifies these distinctions.
A patch version bump (1.0.0 to 1.0.1) fixes bugs. The changelog lists Fixed items. It should not contain breaking changes. If a patch version contains a breaking change, you violated SemVer. Users will catch this. They will open issues. They will pin your crate.
Before 1.0.0, the rules are looser. 0.2.0 can break things. The changelog should still document breaking changes. Users expect instability in 0.x versions, but they still need to know what changed. Use the Changed category to mark breaking changes in pre-1.0 releases. Add a note at the top of the changelog: "This crate is pre-1.0. Breaking changes may occur in minor versions." This sets expectations.
The unreleased workflow
Maintaining a changelog is a habit. The best workflow is to update the changelog as part of every PR. When a contributor opens a PR, they add a bullet point to CHANGELOG.md under Unreleased. They pick the category. They write a concise description. The maintainer reviews the entry along with the code. If the category is wrong, the maintainer corrects it. If the description is vague, the maintainer asks for detail.
This workflow distributes the work. The maintainer doesn't have to retroactively guess what changed. The contributor knows the impact of their change. The changelog stays current. When release time comes, you just add the date and bump the version.
Tools can help automate this. cargo-release can bump versions and update the changelog if configured. GitHub Actions can generate changelogs from commit messages. Automation is useful, but human review is better for categorization. A bot can't always tell if a change is breaking or not. It can't write a clear migration guide. Use tools to aggregate, but use humans to curate.
Convention aside: some crates use CHANGELOG.md and some use NEWS.md. The Rust community overwhelmingly prefers CHANGELOG.md. Stick to that name. It's what users expect. It's what tools support.
Real-world example
Here's a realistic changelog for a crate that has reached 1.0.0. It shows how to handle deprecations, breaking changes, and security fixes.
# Changelog
## [Unreleased]
### Added
- `Client::with_proxy` to configure HTTP proxies.
### Fixed
- Memory leak in `Buffer::resize` when capacity exceeds 4GB.
## [1.2.0] - 2024-06-01
### Added
- `Config::timeout` option to control request latency.
### Changed
- `Client::new` now requires a `Config` struct instead of individual arguments. This reduces boilerplate and prevents argument order errors.
### Deprecated
- `Client::set_timeout` is deprecated. Use `Config::timeout` instead. This method will be removed in version 2.0.
### Fixed
- Race condition in connection pool when idle timeout fires during active request.
## [1.1.0] - 2024-05-15
### Added
- `Parser::from_bytes` for zero-copy parsing.
### Fixed
- Panic in `parse_json` when input contains trailing commas.
## [1.0.0] - 2024-04-01
### Changed
- Stabilized API. All public interfaces are now considered stable.
- Renamed `buf.len()` to `buf.size()` for consistency with `std::vec::Vec`.
### Removed
- Deprecated `Client::old_method` from 0.9.x.
### Security
- Fixed buffer overflow in `decode_base64` when input is malformed. CVE-2024-1234.
Notice the Deprecated section. It warns users about upcoming removals. This gives users time to migrate. The Security section highlights critical fixes. Users need to see this immediately. The Changed section in 1.0.0 explains the stabilization. It lists breaking changes that happened during the pre-1.0 phase.
Treat the changelog as a migration guide for your users. If you broke something, tell them how to fix it.
Pitfalls and compiler fallout
The biggest pitfall is forgetting to update the changelog. You merge a PR. You bump the version. You publish. You forget the changelog. Users update. They hit errors. They open issues. You look at the git log. You have no idea what changed. You spend hours digging through diffs to explain a fix that should have been one line in the changelog. Update the changelog with every PR. Make it part of your checklist.
Another pitfall is vague entries. "Fixed bugs" is useless. "Updated dependencies" is noise. Users care about impact. "Fixed panic in parse_int when input is empty" is useful. "Updated tokio to 1.35.0" is useful only if it brings a fix or feature. Otherwise, it's noise. Filter the noise. Focus on what matters to the user.
SemVer violations are fatal. If you put a breaking change in a patch version, users will catch it. They'll see E0277 (trait bound not satisfied) because you removed a trait impl. They'll see E0507 (cannot move out of borrowed content) because you changed ownership semantics. They'll assume you don't care about stability. They'll fork your crate or find an alternative. Respect SemVer. Document breaking changes in major versions only.
If you rename a function, downstream code breaks with E0432 (unresolved import). If you change a function signature, users hit E0061 (function call arguments). If you change a return type, users get E0308 (mismatched types). The changelog prevents these errors from being surprises. It tells users what to expect. It gives them a chance to fix their code before they upgrade.
Convention aside: some developers argue that cargo doc is enough. It's not. cargo doc shows the current API. It doesn't show what changed. It doesn't show deprecations. It doesn't show fixes. Users need the changelog to decide if they should update. Docs explain how to use the crate. The changelog explains why to update.
Choosing your approach
Use the Keep a Changelog format when you want a standard structure that tools and humans can parse reliably. Use a git-log-based changelog when your commit messages follow a strict convention like Conventional Commits and you have automation to filter noise. Use automated changelog generators when you have a high volume of small PRs and need to aggregate them without manual overhead. Use no changelog only for private, throwaway crates that no one else depends on; public crates without changelogs signal that the author doesn't care about the user experience.
Your changelog is the first line of defense against user churn. Write it.