-
Notifications
You must be signed in to change notification settings - Fork 5
Open
Labels
documentationDocumentation changesDocumentation changesenhancementNew feature or requestNew feature or request
Milestone
Description
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-cliandstandard-versionpackages - Create
.changelogrc.cjsconfiguration file with emoji categories - Create
.versionrc.cjsconfiguration file forstandard-version - Create
version.sbtfile for version tracking - Create GitFlow automation script
- Add npm scripts for changelog generation and release automation
- Generate
CHANGELOG.mdfrom commit history
π‘ Why this change?
- Project currently has no changelog to track version history
- Automated changelog generation from Conventional Commits ensures consistency
- The
package.jsontracks a version but there's no documentation of changes - The
standard-versionautomates 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
- Install packages as
devDependencies:
npm install -D conventional-changelog-cli standard-versionPhase 2: Create .changelogrc.cjs file
- Create
.changelogrc.cjsfile 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.cjsfile 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.sbtfile 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.jsfile:
module.exports.readVersion = contents => contents.match(/"(?<version>.*)"/u).groups.version;
module.exports.writeVersion = (_, version) => `ThisBuild / version := "${version}"\n`;- Create
bin/standard-version-updater-gitflow.shfile:
#!/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
fiPhase 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.mdis 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.sbtis 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.jsonandversion.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
releasebranch - Runs
changelog:update(standard-version) - Detects if
release(major/minor) orhotfix(patch) - Renames branch to
release/X.X.Xorhotfix/X.X.X - Merges to master with
--no-ff - Pushes master with tags
- Merges to develop
- Pushes develop
- Deletes
release/hotfixbranch - Returns to develop or master
π References
Files to create
.changelogrc.cjs.versionrc.cjsversion.sbtbin/standard-version-updater.jsbin/standard-version-updater-gitflow.shCHANGELOG.md
Files to modify
package.json
Documentation
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
documentationDocumentation changesDocumentation changesenhancementNew feature or requestNew feature or request