Best Tools for Remote Team Changelog Review
A changelog that nobody reads is written by a process that nobody runs. Remote engineering teams need changelog generation that happens automatically at release time, surfaces to the people who need it (product, support, users), and maintains a browsable history without someone manually editing a CHANGELOG.md on every merge.
The Problem with Manual Changelogs
- Engineers forget to add entries
- Entries get written in batch before release, losing accuracy
- Product managers don’t know what shipped until the release notes are published
- Support teams can’t search “when did behavior X change”
Automated generation from commit messages and PR titles fixes the first three. A proper review step fixes the accuracy problem.
Tool 1: git-cliff (Best Commit-Based Generator)
git-cliff reads your git log and generates a structured changelog from conventional commits. It’s fast, Rust-based, and highly configurable.
Install:
# macOS
brew install git-cliff
# From cargo
cargo install git-cliff
# Linux binary
curl -LO https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-x86_64-unknown-linux-musl.tar.gz
tar xf git-cliff-*.tar.gz && sudo mv git-cliff /usr/local/bin/
cliff.toml — config at repo root:
[changelog]
header = """
# Changelog\n
All notable changes are documented here.\n
"""
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] — {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [Unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}\
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))\
{% endfor %}
{% endfor %}\n
"""
trim = true
[git]
conventional_commits = true
filter_unconventional = true
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactoring" },
{ message = "^docs", group = "Documentation" },
{ message = "^chore\\(deps\\)", group = "Dependencies" },
{ message = "^chore", skip = true },
{ message = "^ci", skip = true },
]
protect_breaking_commits = true
filter_commits = true
tag_pattern = "v[0-9].*"
Generate the changelog:
# Unreleased changes since last tag
git cliff --unreleased
# Full changelog
git cliff --output CHANGELOG.md
# Just since last release (for release notes)
git cliff --latest --strip all
# Bump version and generate changelog
git cliff --bump --output CHANGELOG.md
git tag "$(git cliff --bumped-version)"
Tool 2: release-please (Google’s Automated Release PRs)
release-please opens a release PR automatically after each merge to main. The PR contains a versioned CHANGELOG.md update and a version bump. When you’re ready to release, merge the PR — no manual changelog writing.
.github/workflows/release-please.yml
name: Release Please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: node # or: python, go, ruby, simple
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
release-please-config.json
{
"packages": {
".": {
"release-type": "node",
"changelog-sections": [
{"type": "feat", "section": "Features"},
{"type": "fix", "section": "Bug Fixes"},
{"type": "perf", "section": "Performance"},
{"type": "revert", "section": "Reverts"},
{"type": "docs", "section": "Documentation"},
{"type": "deps", "section": "Dependencies"},
{"type": "refactor", "section": "Code Refactoring", "hidden": false}
],
"bump-minor-pre-major": true,
"pull-request-title-pattern": "chore: release ${version}"
}
}
}
The result: every feature/fix landed to main gets a rolling release PR that accumulates entries. Your team reviews the PR before merging to release.
Tool 3: Keep a Changelog with PR Gate
For teams that prefer manually written changelogs with automated enforcement:
CHANGELOG.md format (Keep a Changelog):
# Changelog
## [Unreleased]
### Added
- New user profile page with activity history
### Fixed
- Timeout error when uploading files over 100MB
## [2.4.1] — 2026-03-15
### Fixed
- Pagination bug on the dashboard causing duplicate results
GitHub Actions gate — fail PRs that modify code without a changelog entry:
# .github/workflows/changelog-gate.yml
name: Changelog Gate
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
check-changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for changelog update
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
// Skip docs-only, chore, and dependency PRs
const skipLabels = ["documentation", "chore", "dependencies", "ci/cd"];
const prLabels = pr.labels.map(l => l.name);
if (prLabels.some(l => skipLabels.includes(l))) {
console.log("Skipping changelog check for non-feature PR");
return;
}
// Check if CHANGELOG.md was modified
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const changelogUpdated = files.some(f => f.filename === "CHANGELOG.md");
if (!changelogUpdated) {
core.setFailed(
"This PR changes code but does not update CHANGELOG.md.\n" +
"Add an entry under [Unreleased], or add the 'chore'/'documentation' label to skip."
);
}
Slack Release Summary
Post a formatted changelog summary to Slack when a release is published:
# .github/workflows/release-notify.yml
on:
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract release notes
id: notes
run: |
# Get body of latest release
NOTES=$(gh release view "${{ github.event.release.tag_name }}" --json body -q .body)
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post to Slack
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": ":rocket: *${{ github.repository }} ${{ github.event.release.tag_name }} released*",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":rocket: *<${{ github.event.release.html_url }}|${{ github.repository }} ${{ github.event.release.tag_name }}>* released\n\n${{ steps.notes.outputs.notes }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK }}
Querying Changelog History
Once you have a machine-readable CHANGELOG.md, you can answer “when did X change”:
# Find all entries that mention a specific feature
grep -A 5 -i "authentication" CHANGELOG.md
# List all releases since a date
awk '/^## \[/ && /202[56]/' CHANGELOG.md
# Export last 3 releases as JSON using git-cliff
git cliff --latest=3 --output=- --strip=all | python3 -c "
import sys, json, re
content = sys.stdin.read()
releases = re.split(r'^## ', content, flags=re.M)[1:]
for r in releases:
lines = r.strip().split('\n')
version = lines[0].split(' ')[0].strip('[]')
print(json.dumps({'version': version, 'content': r.strip()}))
"
Related Reading
- How to Automate Pull Request Labeling
- Best Tools for Remote Team Post-Mortems
- Best Tools for Remote Team Sprint Velocity
Built by theluckystrike — More at zovo.one