Skip to content

Commit bc5b410

Browse files
committed
feat(changelog): add changelog generation
- Added ChangelogService to generate changelog - Integrated changelog generation into workflow - Updated version bump process to include changelog - Uses git log to get commits since last tag - Categorizes commits (Added, Changed, Fixed)
1 parent 599cb29 commit bc5b410

6 files changed

Lines changed: 291 additions & 5 deletions

File tree

dist/index.js

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/**
2-
* universal-version-bump v0.9.0
2+
* universal-version-bump v0.9.1
33
* Universal Version Bump
44
*
55
* Description: A GitHub Action to automatically bump versions across any app (Node, Python, PHP, Docker, etc.)
66
* Author: Taj <[email protected]>
77
* Homepage: https://github.com/taj54/universal-version-bump#readme
88
* License: MIT
9-
* Generated on Mon, 25 Aug 2025 10:58:18 GMT
9+
* Generated on Tue, 26 Aug 2025 08:40:56 GMT
1010
*/
1111
require('./sourcemap-register.js');/******/ (() => { // webpackBootstrap
1212
/******/ var __webpack_modules__ = ({
@@ -32756,6 +32756,7 @@ var __importStar = (this && this.__importStar) || (function () {
3275632756
})();
3275732757
Object.defineProperty(exports, "__esModule", ({ value: true }));
3275832758
const services_1 = __nccwpck_require__(5234);
32759+
const utils_1 = __nccwpck_require__(9499);
3275932760
const updaters_1 = __nccwpck_require__(1384);
3276032761
const registry_1 = __nccwpck_require__(8378);
3276132762
const errors_1 = __nccwpck_require__(4830);
@@ -32775,10 +32776,17 @@ async function run() {
3277532776
updaterRegistry.registerUpdater(new updaters_1.PHPUpdater());
3277632777
const updaterService = new services_1.UpdaterService(updaterRegistry);
3277732778
const gitService = new services_1.GitService();
32779+
const fileHandler = new utils_1.FileHandler();
32780+
const changelogService = new services_1.ChangelogService(fileHandler);
3277832781
const platform = updaterService.getPlatform(targetPlatform);
3277932782
core.info(`Detected platform: ${platform}`);
3278032783
const version = updaterService.updateVersion(platform, releaseType);
3278132784
core.setOutput('new_version', version);
32785+
// Generate and update changelog
32786+
const latestTag = await changelogService.getLatestTag();
32787+
const commits = await changelogService.getCommitsSinceTag(latestTag);
32788+
const changelogContent = changelogService.generateChangelog(commits, version);
32789+
await changelogService.updateChangelog(changelogContent);
3278232790
// Git Commit & Tag
3278332791
const gitTag = config_1.GIT_TAG;
3278432792
await gitService.configureGitUser();
@@ -32867,6 +32875,111 @@ class UpdaterRegistry {
3286732875
exports.UpdaterRegistry = UpdaterRegistry;
3286832876

3286932877

32878+
/***/ }),
32879+
32880+
/***/ 5417:
32881+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
32882+
32883+
"use strict";
32884+
32885+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
32886+
if (k2 === undefined) k2 = k;
32887+
var desc = Object.getOwnPropertyDescriptor(m, k);
32888+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
32889+
desc = { enumerable: true, get: function() { return m[k]; } };
32890+
}
32891+
Object.defineProperty(o, k2, desc);
32892+
}) : (function(o, m, k, k2) {
32893+
if (k2 === undefined) k2 = k;
32894+
o[k2] = m[k];
32895+
}));
32896+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32897+
Object.defineProperty(o, "default", { enumerable: true, value: v });
32898+
}) : function(o, v) {
32899+
o["default"] = v;
32900+
});
32901+
var __importStar = (this && this.__importStar) || (function () {
32902+
var ownKeys = function(o) {
32903+
ownKeys = Object.getOwnPropertyNames || function (o) {
32904+
var ar = [];
32905+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32906+
return ar;
32907+
};
32908+
return ownKeys(o);
32909+
};
32910+
return function (mod) {
32911+
if (mod && mod.__esModule) return mod;
32912+
var result = {};
32913+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32914+
__setModuleDefault(result, mod);
32915+
return result;
32916+
};
32917+
})();
32918+
Object.defineProperty(exports, "__esModule", ({ value: true }));
32919+
exports.ChangelogService = void 0;
32920+
const exec = __importStar(__nccwpck_require__(8872));
32921+
class ChangelogService {
32922+
constructor(fileHandler) {
32923+
this.fileHandler = fileHandler;
32924+
}
32925+
async getLatestTag() {
32926+
let latestTag = '';
32927+
const options = {
32928+
listeners: {
32929+
stdout: (data) => {
32930+
latestTag += data.toString();
32931+
},
32932+
},
32933+
};
32934+
await exec.exec('git', ['describe', '--tags', '--abbrev=0'], options);
32935+
return latestTag.trim();
32936+
}
32937+
async getCommitsSinceTag(tag) {
32938+
let commits = '';
32939+
const options = {
32940+
listeners: {
32941+
stdout: (data) => {
32942+
commits += data.toString();
32943+
},
32944+
},
32945+
};
32946+
await exec.exec('git', ['log', `${tag}..HEAD`, '--oneline'], options);
32947+
return commits.split('\n').filter(Boolean);
32948+
}
32949+
generateChangelog(commits, newVersion) {
32950+
const changelogDate = new Date().toISOString().split('T')[0];
32951+
let changelogContent = `## v${newVersion} ${changelogDate}\n\n`;
32952+
const categorizedCommits = { Added: [], Changed: [], Fixed: [] };
32953+
for (const commit of commits) {
32954+
const commitMessage = commit.split(' ').slice(1).join(' ');
32955+
if (commitMessage.startsWith('feat') || commitMessage.startsWith('Added')) {
32956+
categorizedCommits.Added.push(`- ${commitMessage}`);
32957+
}
32958+
else if (commitMessage.startsWith('fix') || commitMessage.startsWith('Fixed')) {
32959+
categorizedCommits.Fixed.push(`- ${commitMessage}`);
32960+
}
32961+
else {
32962+
categorizedCommits.Changed.push(`- ${commitMessage}`);
32963+
}
32964+
}
32965+
for (const category in categorizedCommits) {
32966+
if (categorizedCommits[category].length > 0) {
32967+
changelogContent += `### ${category}\n\n`;
32968+
changelogContent += categorizedCommits[category].join('\n') + '\n\n';
32969+
}
32970+
}
32971+
return changelogContent;
32972+
}
32973+
async updateChangelog(changelogContent) {
32974+
const changelogPath = 'CHANGELOG.md';
32975+
const existingChangelog = await this.fileHandler.readFile(changelogPath);
32976+
const newChangelog = changelogContent + existingChangelog;
32977+
await this.fileHandler.writeFile(changelogPath, newChangelog);
32978+
}
32979+
}
32980+
exports.ChangelogService = ChangelogService;
32981+
32982+
3287032983
/***/ }),
3287132984

3287232985
/***/ 8743:
@@ -32995,6 +33108,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
3299533108
Object.defineProperty(exports, "__esModule", ({ value: true }));
3299633109
__exportStar(__nccwpck_require__(9104), exports);
3299733110
__exportStar(__nccwpck_require__(8743), exports);
33111+
__exportStar(__nccwpck_require__(5417), exports);
3299833112

3299933113

3300033114
/***/ }),
@@ -33105,7 +33219,7 @@ class GoUpdater {
3310533219
if (!this.manifestPath)
3310633220
return null;
3310733221
return this.manifestParser.getVersion(this.manifestPath, 'regex', {
33108-
regex: /module\s+.*\n.*v(\d+\.\d+\.\d+)/,
33222+
regex: /^module\s+[^\s]+\s+v?(\d+\.\d+\.\d+)/m,
3310933223
});
3311033224
}
3311133225
bumpVersion(releaseType) {

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { UpdaterService, GitService } from './services';
1+
import { UpdaterService, GitService, ChangelogService } from './services';
2+
import { FileHandler } from './utils';
23
import {
34
DockerUpdater,
45
GoUpdater,
@@ -33,13 +34,21 @@ async function run() {
3334

3435
const updaterService = new UpdaterService(updaterRegistry);
3536
const gitService = new GitService();
37+
const fileHandler = new FileHandler();
38+
const changelogService = new ChangelogService(fileHandler);
3639

3740
const platform = updaterService.getPlatform(targetPlatform);
3841
core.info(`Detected platform: ${platform}`);
3942

4043
const version = updaterService.updateVersion(platform, releaseType);
4144
core.setOutput('new_version', version);
4245

46+
// Generate and update changelog
47+
const latestTag = await changelogService.getLatestTag();
48+
const commits = await changelogService.getCommitsSinceTag(latestTag);
49+
const changelogContent = changelogService.generateChangelog(commits, version);
50+
await changelogService.updateChangelog(changelogContent);
51+
4352
// Git Commit & Tag
4453
const gitTag = GIT_TAG;
4554
await gitService.configureGitUser();

src/services/changelogService.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as exec from '@actions/exec';
2+
import { FileHandler } from '../utils/fileHandler';
3+
4+
/**
5+
* Service for managing the changelog file.
6+
*/
7+
export class ChangelogService {
8+
constructor(private fileHandler: FileHandler) {}
9+
10+
/**
11+
* Get the latest git tag.
12+
* @returns The latest tag as a string.
13+
*/
14+
async getLatestTag(): Promise<string> {
15+
let latestTag = '';
16+
const options = {
17+
listeners: {
18+
stdout: (data: Buffer) => {
19+
latestTag += data.toString();
20+
},
21+
},
22+
};
23+
await exec.exec('git', ['describe', '--tags', '--abbrev=0'], options);
24+
return latestTag.trim();
25+
}
26+
27+
/**
28+
* Get the commits since a specific tag.
29+
* @param tag The tag to get commits since.
30+
* @returns An array of commit messages.
31+
*/
32+
async getCommitsSinceTag(tag: string): Promise<string[]> {
33+
let commits = '';
34+
const options = {
35+
listeners: {
36+
stdout: (data: Buffer) => {
37+
commits += data.toString();
38+
},
39+
},
40+
};
41+
await exec.exec('git', ['log', `${tag}..HEAD`, '--oneline'], options);
42+
return commits.split('\n').filter(Boolean);
43+
}
44+
45+
/**
46+
* Generate a changelog from the given commits.
47+
* @param commits The list of commits to include in the changelog.
48+
* @param newVersion The new version number.
49+
* @returns The generated changelog as a string.
50+
*/
51+
generateChangelog(commits: string[], newVersion: string): string {
52+
const changelogDate = new Date().toISOString().split('T')[0];
53+
let changelogContent = `## v${newVersion} ${changelogDate}\n\n`;
54+
const categorizedCommits: { [key: string]: string[] } = { Added: [], Changed: [], Fixed: [] };
55+
for (const commit of commits) {
56+
const commitMessage = commit.split(' ').slice(1).join(' ');
57+
const messageParts = commitMessage.split(':');
58+
const message =
59+
messageParts.length > 1 ? messageParts.slice(1).join(':').trim() : commitMessage;
60+
if (commitMessage.startsWith('feat') || commitMessage.startsWith('Added')) {
61+
categorizedCommits.Added.push(`- ${message}`);
62+
} else if (commitMessage.startsWith('fix') || commitMessage.startsWith('Fixed')) {
63+
categorizedCommits.Fixed.push(`- ${message}`);
64+
} else {
65+
categorizedCommits.Changed.push(`- ${message}`);
66+
}
67+
}
68+
for (const category in categorizedCommits) {
69+
if (categorizedCommits[category].length > 0) {
70+
changelogContent += `### ${category}\n\n`;
71+
changelogContent += categorizedCommits[category].join('\n') + '\n\n';
72+
}
73+
}
74+
return changelogContent;
75+
}
76+
77+
/**
78+
* Update the changelog file with the new content.
79+
* @param changelogContent The new changelog content to add.
80+
*/
81+
async updateChangelog(changelogContent: string): Promise<void> {
82+
const changelogPath = 'CHANGELOG.md';
83+
const existingChangelog = await this.fileHandler.readFile(changelogPath);
84+
const newChangelog = changelogContent + existingChangelog;
85+
await this.fileHandler.writeFile(changelogPath, newChangelog);
86+
}
87+
}

src/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './updaterService';
22
export * from './gitService';
3+
export * from './changelogService';
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { ChangelogService } from '../../src/services/changelogService';
3+
import { FileHandler } from '../../src/utils/fileHandler';
4+
import * as exec from '@actions/exec';
5+
6+
vi.mock('@actions/exec');
7+
vi.mock('../../src/utils/fileHandler');
8+
9+
describe('ChangelogService', () => {
10+
let changelogService: ChangelogService;
11+
let fileHandler: FileHandler;
12+
13+
beforeEach(() => {
14+
fileHandler = new FileHandler();
15+
changelogService = new ChangelogService(fileHandler);
16+
});
17+
18+
it('should get the latest tag', async () => {
19+
const execSpy = vi.spyOn(exec, 'exec').mockImplementation((command, args, options) => {
20+
options.listeners.stdout(Buffer.from('v1.0.0'));
21+
return Promise.resolve(0);
22+
});
23+
24+
const latestTag = await changelogService.getLatestTag();
25+
26+
expect(execSpy).toHaveBeenCalledWith(
27+
'git',
28+
['describe', '--tags', '--abbrev=0'],
29+
expect.any(Object),
30+
);
31+
expect(latestTag).toBe('v1.0.0');
32+
});
33+
34+
it('should get commits since a tag', async () => {
35+
const execSpy = vi.spyOn(exec, 'exec').mockImplementation((command, args, options) => {
36+
options.listeners.stdout(Buffer.from('commit1\ncommit2'));
37+
return Promise.resolve(0);
38+
});
39+
40+
const commits = await changelogService.getCommitsSinceTag('v1.0.0');
41+
42+
expect(execSpy).toHaveBeenCalledWith(
43+
'git',
44+
['log', 'v1.0.0..HEAD', '--oneline'],
45+
expect.any(Object),
46+
);
47+
expect(commits).toEqual(['commit1', 'commit2']);
48+
});
49+
50+
it('should generate a changelog', () => {
51+
const commits = ['hash1 feat: new feature', 'hash2 fix: a bug fix', 'hash3 chore: maintenance'];
52+
const newVersion = '1.1.0';
53+
54+
const changelog = changelogService.generateChangelog(commits, newVersion);
55+
console.log(changelog);
56+
57+
expect(changelog).toContain('## v1.1.0');
58+
expect(changelog).toContain('### Added');
59+
expect(changelog).toContain('- new feature');
60+
expect(changelog).toContain('### Fixed');
61+
expect(changelog).toContain('- a bug fix');
62+
expect(changelog).toContain('### Changed');
63+
expect(changelog).toContain('- maintenance');
64+
});
65+
66+
it('should update the changelog file', async () => {
67+
const readFileSpy = vi.spyOn(fileHandler, 'readFile').mockResolvedValue('existing content');
68+
const writeFileSpy = vi.spyOn(fileHandler, 'writeFile').mockResolvedValue();
69+
70+
await changelogService.updateChangelog('new content');
71+
72+
expect(readFileSpy).toHaveBeenCalledWith('CHANGELOG.md');
73+
expect(writeFileSpy).toHaveBeenCalledWith('CHANGELOG.md', 'new contentexisting content');
74+
});
75+
});

0 commit comments

Comments
 (0)