Remote Work Tools

Manually writing changelogs means they either don’t get written or they’re vague summaries that don’t help users understand what changed. Automated changelogs generated from structured commit messages give you release notes that are accurate, consistent, and require zero manual work.

The prerequisite is conventional commits. Without structured commit messages, automated tools can’t categorize changes.


git-cliff: Highly Configurable

git-cliff is a Rust-based changelog generator that reads conventional commits and produces Markdown. It’s the most flexible option — you control exactly what appears and how it’s formatted.

Install:

# macOS
brew install git-cliff

# Linux
cargo install git-cliff

# Docker
docker run -v "$(pwd)":/app orhunp/git-cliff:latest

Initialize configuration:

git cliff --init

This creates cliff.toml. A production-ready config:

# cliff.toml
[changelog]
header = "# Changelog\n\nAll notable changes to this project are documented in this file.\n"
body = """
{% 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="") }}](https://github.com/your-org/your-repo/commit/{{ commit.id }}))\
  {% if commit.breaking %} [**BREAKING**]{% endif %}
{% endfor %}
{% endfor %}\n
"""
footer = ""
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 = "^docs", group = "Documentation" },
  { message = "^refactor", group = "Refactoring" },
  { message = "^ci", group = "CI/CD" },
  { message = "^chore\\(deps\\)", group = "Dependencies" },
  { message = "^chore", skip = true },
  { message = "^style", skip = true },
  { message = "^test", skip = true },
]
protect_breaking_commits = true
filter_commits = true
tag_pattern = "v[0-9]*"
skip_tags = "v0\\.1\\..*"
sort_commits = "newest"

Generate changelog for the current version:

# From last tag to HEAD
git cliff --output CHANGELOG.md

# For a specific version
git cliff --tag v1.2.0 --output CHANGELOG.md

# Only changes since last tag (for release notes)
git cliff --unreleased --strip header

GitHub Actions: Automated Release on Tag

Trigger changelog generation and GitHub Release creation on every version tag:

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'

jobs:
  release:
    name: Create Release
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history needed for git-cliff

      - name: Generate changelog
        id: changelog
        uses: orhun/git-cliff-action@v3
        with:
          config: cliff.toml
          args: --verbose --current --strip header
        env:
          OUTPUT: CHANGELOG.md

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          body: ${{ steps.changelog.outputs.content }}
          draft: false
          prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') }}

release-please: Automated PRs

release-please (by Google) takes a different approach — it opens a “release PR” that accumulates changes until you’re ready to release, then merges it to create the tag and changelog automatically.

# .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:
          token: ${{ secrets.GITHUB_TOKEN }}
          release-type: node  # or go, python, rust, simple

Configure which commit types go in the changelog:

// .release-please-manifest.json
{
  ".": "1.0.0"
}
// release-please-config.json
{
  "packages": {
    ".": {
      "release-type": "node",
      "changelog-sections": [
        {"type": "feat", "section": "Features"},
        {"type": "fix", "section": "Bug Fixes"},
        {"type": "perf", "section": "Performance"},
        {"type": "deps", "section": "Dependencies"},
        {"type": "revert", "section": "Reverts"}
      ],
      "bump-minor-pre-major": true,
      "draft": false,
      "prerelease": false
    }
  }
}

release-please works well for library/package maintainers. For application deploys, git-cliff with manual tagging gives more control.


conventional-changelog-cli (npm Ecosystem)

For JavaScript projects already using npm, conventional-changelog-cli integrates into your existing workflow:

npm install --save-dev conventional-changelog-cli

Add to package.json:

{
  "scripts": {
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "version": "npm run changelog && git add CHANGELOG.md"
  }
}

The version script runs automatically when you call npm version:

# Bump patch version (fix commits)
npm version patch

# Bump minor version (feat commits)
npm version minor

# Bump major version (breaking changes)
npm version major

This updates package.json, generates CHANGELOG.md, commits both, and creates a git tag — all in one command.


Enforcing Conventional Commits

Automated changelogs require conventional commit format. Enforce it:

# Install commitlint
npm install --save-dev @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
};
# .github/workflows/commitlint.yml
name: Commitlint
on:
  pull_request:
    types: [opened, synchronize, reopened, edited]

jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Lint commit messages
        uses: wagoid/commitlint-github-action@v6
        with:
          configFile: commitlint.config.js
          failOnWarnings: false
          helpURL: https://www.conventionalcommits.org

This runs on every PR and blocks merge if any commit message doesn’t match the convention.


Keep CHANGELOG.md Always Current

For projects where the changelog should be continuously updated (not just at release time), generate it in CI on every merge to main:

# .github/workflows/update-changelog.yml
name: Update Changelog

on:
  push:
    branches: [main]

permissions:
  contents: write

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Install git-cliff
        run: cargo install git-cliff

      - name: Update CHANGELOG.md
        run: git cliff --output CHANGELOG.md

      - name: Commit if changed
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add CHANGELOG.md
          git diff --cached --quiet || git commit -m "chore: update changelog [skip ci]"
          git push

The [skip ci] tag prevents the changelog update commit from triggering another CI run.