Skip to content

feat(changelog): add CHANGELOG.md for automated version history trackingΒ #1062

@beatrizsmerino

Description

@beatrizsmerino

feat(changelog): add CHANGELOG.md for automated version history tracking

⏱️ Estimate πŸ“Š Priority πŸ“ Size πŸ“… Start πŸ“… End
4h P1 M 26-01-2026 26-01-2026

πŸ“Έ Screenshots

Current Expected
N/A β€” This change has no visual impact. N/A β€” This change has no visual impact.

πŸ“ Summary

  • Install conventional-changelog-cli and standard-version packages
  • Create .changelogrc.cjs configuration file with emoji categories
  • Create .versionrc.cjs configuration file for standard-version
  • Create version.sbt file for version tracking
  • Create GitFlow automation script
  • Add npm scripts for changelog generation and release automation
  • Generate CHANGELOG.md from commit history

πŸ’‘ Why this change?

  • Project currently has no changelog to track version history
  • Automated changelog generation from Conventional Commits ensures consistency
  • The package.json tracks a version but there's no documentation of changes
  • The standard-version automates the entire release flow (version bump + changelog + git tag)
  • GitFlow script automates merge to master/develop and push
  • Provides clear documentation of all changes for users and contributors

βœ… Benefits

  • Automated changelog generation from commit messages
  • Consistent format with emoji categories for easy reading
  • Links to commits and issues in GitHub
  • Follows Conventional Commits standard
  • Automatic version bump based on commit types (feat β†’ minor, fix β†’ patch)
  • Automatic git tag creation
  • Full GitFlow automation with single command

πŸ“‹ Steps

Phase 1: Install dependencies

npm install -D conventional-changelog-cli standard-version

Phase 2: Create .changelogrc.cjs file

  • Create .changelogrc.cjs file in project root with the following content:
const compareFunc = require("compare-func");
const path = require("path");
const fs = require("fs");

// Load project name from package.json
let projectName = "Vue Users";
try {
  const pkgPath = path.join(process.cwd(), "package.json");
  if (fs.existsSync(pkgPath)) {
    const pkgContent = fs.readFileSync(pkgPath, "utf8");
    const pkg = JSON.parse(pkgContent);
    projectName = pkg.name || projectName;
  }
} catch (e) {
  // Use default name if package.json doesn't exist or is invalid
}

module.exports = {
  "context": {
    "linkCompare": true,
    "host": "https://github.com",
    "owner": "beatrizsmerino",
    "repository": "vue-users",
  },
  "parserOpts": {
    "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"],
    "referenceActions": [
      "close",
      "closes",
      "closed",
      "fix",
      "fixes",
      "fixed",
      "resolve",
      "resolves",
      "resolved",
    ],
    "issuePrefixes": ["#"],
  },
  "releaseCommitMessageFormat": "build(release): {{currentTag}}",
  "writerOpts": {
    "commitGroupsSort": (a, b) => {
      const order = [
        "✨ Features",
        "πŸ› Fixes",
        "⚑ Performance",
        "πŸš€ Build System",
        "πŸ“ Documentation",
        "🎨 Styles",
        "πŸ”¨ Refactors",
        "πŸ§ͺ Tests",
        "πŸ”§ Continuous Integration",
        "πŸ”„ Reverts",
      ];
      const indexA = order.indexOf(a.title);
      const indexB = order.indexOf(b.title);
      if (indexA === -1 && indexB === -1) return 0;
      if (indexA === -1) return 1;
      if (indexB === -1) return -1;
      return indexA - indexB;
    },
    "commitsSort": compareFunc,
    "finalizeContext": context => {
      if (context.date) {
        const d = new Date(context.date);
        const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
        context.date = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`;
      }
      return context;
    },
    "groupBy": "type",
    "headerPartial": `# Changelog - ${projectName}

All notable changes to this project will be documented in this file.

This changelog is automatically generated from [Conventional Commits](https://www.conventionalcommits.org/).

## Emojis Legend

- ✨ **Features** - New functionality added
- πŸ› **Fixes** - Bug fixes
- ⚑ **Performance** - Performance improvements
- πŸš€ **Build System** - Build system and configuration changes
- πŸ“ **Documentation** - Adding or updating documentation
- 🎨 **Styles** - Code style changes (formatting, missing semi-colons, etc)
- πŸ”¨ **Refactors** - Code restructuring without changing functionality
- πŸ§ͺ **Tests** - Adding or updating tests
- πŸ”§ **Continuous Integration** - CI/CD configuration changes
- πŸ”„ **Reverts** - Revert previous commits
- ⚠️ **Breaking Changes** - Changes that may break existing functionality

---

`,
    "mainTemplate": `{{> header}}
## {{#if @root.linkCompare~}}
  [{{version}}]({{@root.host}}/{{@root.owner}}/{{@root.repository}}/compare/{{previousTag}}...{{currentTag}})
{{~else}}
  {{~version}}
{{~/if}}
{{~#if title}} "{{title}}"
{{~/if}}
{{~#if date}} - {{date}}
{{/if}}

{{#if noteGroups}}
{{#each noteGroups}}

### {{title}}

{{#each notes}}
* {{text}}
{{/each}}
{{/each}}
{{/if}}
{{#each commitGroups}}

{{#if title}}
### {{title}}

{{/if}}
{{#each commits}}
* {{#if scope}}**{{originalType}}({{scope}}):** {{else}}**{{originalType}}:** {{/if}}{{subject}} ([{{shortHash}}]({{@root.host}}/{{@root.owner}}/{{@root.repository}}/commit/{{hash}})){{#if references}}, refs {{#each references}}[#{{issue}}]({{@root.host}}/{{@root.owner}}/{{@root.repository}}/issues/{{issue}}){{#unless @last}} {{/unless}}{{/each}}{{/if}}
{{/each}}
{{/each}}

{{> footer}}
`,
    "noteGroupsSort": "title",
    "notesSort": compareFunc,
    "transform": (commit, context) => {
      if (commit.subject && commit.subject.startsWith("Merge ")) return;

      const issues = [];
      const modifiedCommit = { ...commit };
      modifiedCommit.notes = commit.notes.map(note => ({
        ...note,
        "title": "⚠️ BREAKING CHANGES",
      }));

      const typeMapping = {
        "feat": { "emoji": "✨", "section": "Features" },
        "fix": { "emoji": "πŸ›", "section": "Fixes" },
        "perf": { "emoji": "⚑", "section": "Performance" },
        "build": { "emoji": "πŸš€", "section": "Build System" },
        "docs": { "emoji": "πŸ“", "section": "Documentation" },
        "style": { "emoji": "🎨", "section": "Styles" },
        "refactor": { "emoji": "πŸ”¨", "section": "Refactors" },
        "test": { "emoji": "πŸ§ͺ", "section": "Tests" },
        "ci": { "emoji": "πŸ”§", "section": "Continuous Integration" },
        "revert": { "emoji": "πŸ”„", "section": "Reverts" },
      };

      if (modifiedCommit.type && typeMapping[modifiedCommit.type]) {
        modifiedCommit.originalType = modifiedCommit.type;
        modifiedCommit.type = `${typeMapping[modifiedCommit.type].emoji} ${typeMapping[modifiedCommit.type].section}`;
      } else {
        return;
      }

      if (modifiedCommit.scope === "*") modifiedCommit.scope = "";
      if (modifiedCommit.scope && modifiedCommit.scope.length > 30) {
        modifiedCommit.scope = modifiedCommit.scope.substring(0, 27) + "...";
      }

      if (typeof modifiedCommit.hash === "string") {
        modifiedCommit.shortHash = modifiedCommit.hash.substring(0, 7);
      }

      if (typeof modifiedCommit.subject === "string") {
        let url = context.repository
          ? `${context.host}/${context.owner}/${context.repository}`
          : context.repoUrl;
        if (url) {
          url = `${url}/issues/`;
          modifiedCommit.subject = modifiedCommit.subject.replace(/#(\d+)/g, (_, issue) => {
            issues.push(issue);
            return `[#${issue}](${url}${issue})`;
          });
        }
        if (context.host) {
          modifiedCommit.subject = modifiedCommit.subject.replace(
            /\B@([a-z0-9](?:-?[a-z0-9/]){0,38})/g,
            (_, username) => {
              if (username.includes("/")) return `@${username}`;
              return `[@${username}](${context.host}/${username})`;
            },
          );
        }
      }

      const uniqueReferences = [];
      const seenIssues = new Set(issues);
      modifiedCommit.references.forEach(reference => {
        if (!seenIssues.has(reference.issue)) {
          seenIssues.add(reference.issue);
          uniqueReferences.push(reference);
        }
      });
      uniqueReferences.sort((a, b) => parseInt(a.issue, 10) - parseInt(b.issue, 10));
      modifiedCommit.references = uniqueReferences;

      return modifiedCommit;
    },
  },
};

Phase 3: Create .versionrc.cjs file

  • Create .versionrc.cjs file in project root:
module.exports = {
  "header": "",
  "bumpFiles": [
    {
      "filename": "version.sbt",
      "updater": "bin/standard-version-updater.js"
    },
    {
      "filename": "package.json",
      "type": "json"
    },
    {
      "filename": "package-lock.json",
      "type": "json"
    }
  ],
  "types": [
    { "type": "feat", "section": "✨ Features" },
    { "type": "fix", "section": "πŸ› Fixes" },
    { "type": "perf", "section": "⚑ Performance" },
    { "type": "build", "section": "πŸš€ Build System" },
    { "type": "docs", "section": "πŸ“ Documentation" },
    { "type": "style", "section": "🎨 Styles" },
    { "type": "refactor", "section": "πŸ”¨ Refactors" },
    { "type": "test", "section": "πŸ§ͺ Tests" },
    { "type": "ci", "section": "πŸ”§ Continuous Integration" },
    { "type": "revert", "section": "πŸ”„ Reverts" },
    { "type": "chore", "hidden": true }
  ],
  "releaseCommitMessageFormat": "build(release): {{currentTag}}",
  "tagPrefix": ""
};

Phase 4: Create version.sbt file

  • Create version.sbt file in project root with current version:
ThisBuild / version := "X.X.X"

Phase 5: Create bin/ scripts to automate versioning and GitFlow

  • Create bin/standard-version-updater.js file:
module.exports.readVersion = contents => contents.match(/"(?<version>.*)"/u).groups.version;

module.exports.writeVersion = (_, version) => `ThisBuild / version := "${version}"\n`;
  • Create bin/standard-version-updater-gitflow.sh file:
#!/bin/bash

# Check if there are uncommitted changes in the working directory
if ! git diff-index --quiet HEAD --; then
    echo "Error: You have uncommitted changes. Please commit or stash them."
    exit 1
fi

# Create temporal branch name to store the changes
timestamp=$(date +%Y%m%d%H%M%S)
branchTypeTemp="release"
branchNameTemp="$branchTypeTemp/$timestamp"

# Create and move to new branch
git checkout -b "$branchNameTemp"

# Run standard-version to update the changelog and the version
npm run changelog:update

# Get the new version created by standard-version
versionNew=$(node -p "require('./package.json').version")

# Function to extract major, minor, and patch numbers from a version
extract_version_parts() {
    IFS='.' read -ra PARTS <<< "$1"
    echo "${PARTS[@]}"
}

# Extract parts of the old and new versions
read -ra currentParts <<< $(extract_version_parts $versionCurrent)
read -ra newParts <<< $(extract_version_parts $versionNew)

# Determine the branch type based on version changes
if [ "${currentParts[0]}" != "${newParts[0]}" ] || [ "${currentParts[1]}" != "${newParts[1]}" ]; then
    branchType="release"
else
    branchType="hotfix"
fi

# Define the branch name based on the branch type and new version
branchName="$branchType/$versionNew"

# Check if the branch already exists
if git show-ref --verify --quiet "refs/heads/$branchName"; then
    echo "Error: Branch '$branchName' already exists."
    git checkout develop
    git tag -d "$versionNew"
    git branch -D "$branchNameTemp"
    exit 1
fi

# Rename the current branch with the branch type and the new version
git branch -m "$branchName"

# Function to perform git merge and handle errors
merge_branch() {
    git checkout $1
    git merge --no-ff "$branchName" -m "Merge branch '$branchName' into $1"
    if [ $? -ne 0 ]; then
        echo "Merge failed, please resolve conflicts manually."
        exit 1
    fi
}

# Merge into master
merge_branch master

# Push master and tags to remote
git push origin master --tags

# Merge into develop
merge_branch develop

# Push develop to remote
git push origin develop

# Delete the release/hotfix branch
git branch -D "$branchName"

# Checkout to develop or master based on the branch type
if [ "$branchType" = "release" ]; then
    git checkout develop
elif [ "$branchType" = "hotfix" ]; then
    git checkout master
fi

Phase 6: Update package.json scripts

  • Add scripts to package.json:
{
  "scripts": {
    "changelog:init": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 --config .changelogrc.cjs",
    "changelog:update": "standard-version --releaseCommitMessageFormat 'build(release): {{currentTag}}'",
    "changelog:update:gitflow": "sh ./bin/standard-version-updater-gitflow.sh"
  }
}

Phase 7: Generate CHANGELOG.md

  • Run the changelog script to generate initial file:
npm run changelog:init

πŸ§ͺ Tests

  • Verify CHANGELOG.md is generated correctly:
npm run changelog:init
  • Confirm emoji categories display properly
  • Test that links to commits work
  • Verify links to issues work (if any referenced)
  • Verify version bump works correctly:
npm run changelog:update
  • Ensure version.sbt is updated with the new version
  • Test full GitFlow automation end-to-end:
npm run changelog:update:gitflow

πŸ“Œ Notes

  • This issue must be executed LAST, after all other issues are completed, so the CHANGELOG captures all previous changes

Scripts usage

Script Description
changelog:init Generate changelog from all commits (use for initial setup or regeneration)
changelog:update Bump version + update changelog + create git tag (use for releases)
changelog:update:gitflow Full GitFlow automation: bump + changelog + merge to master/develop + push

How standard-version works

  • Analyzes commits since last tag
  • Determines version bump based on commit types:
    • feat: β†’ minor (1.0.0 β†’ 1.1.0)
    • fix: β†’ patch (1.0.0 β†’ 1.0.1)
    • BREAKING CHANGE β†’ major (1.0.0 β†’ 2.0.0)
  • Updates package.json, package-lock.json and version.sbt
  • Updates CHANGELOG.md
  • Creates commit with message build(release): X.X.X
  • Creates git tag X.X.X

How changelog:update:gitflow works

  • Checks for uncommitted changes
  • Creates temporary release branch
  • Runs changelog:update (standard-version)
  • Detects if release (major/minor) or hotfix (patch)
  • Renames branch to release/X.X.X or hotfix/X.X.X
  • Merges to master with --no-ff
  • Pushes master with tags
  • Merges to develop
  • Pushes develop
  • Deletes release/hotfix branch
  • Returns to develop or master

πŸ”— References

Files to create

  • .changelogrc.cjs
  • .versionrc.cjs
  • version.sbt
  • bin/standard-version-updater.js
  • bin/standard-version-updater-gitflow.sh
  • CHANGELOG.md

Files to modify

  • package.json

Documentation

Metadata

Metadata

Labels

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions