Remote Work Tools

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

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()}))
"


Built by theluckystrike — More at zovo.one