xrelease
Version:
xrelease (pronounced cross-release) helps you setup automated releases for your project for any language
75 lines (66 loc) • 2.55 kB
text/typescript
import { execa } from 'execa';
import fs from 'fs/promises';
export type ChangelogTemplate = 'conventional' | 'simple';
export async function generateChangelog(version: string, template: ChangelogTemplate = 'conventional'): Promise<void> {
try {
// Try to get the last tag
let commits: string;
try {
const { stdout: lastTag } = await execa('git', ['describe', '--tags', '--abbrev=0']);
// Get commits since last tag
const { stdout } = await execa('git', ['log', '--oneline', '--no-merges', `${lastTag}..HEAD`]);
commits = stdout;
} catch (error) {
// If no tags exist, get all commits
const { stdout } = await execa('git', ['log', '--oneline', '--no-merges']);
commits = stdout;
}
// Generate changelog content
const date = new Date().toISOString().split('T')[0];
const header = `## [${version}] - ${date}\n\n`;
let content = '';
if (!commits.trim()) {
content = '* No changes\n\n';
} else if (template === 'conventional') {
content = formatConventionalCommits(commits);
} else {
content = formatSimpleCommits(commits);
}
const changelogContent = header + content;
try {
// Try to read existing changelog
const existingChangelog = await fs.readFile('CHANGELOG.md', 'utf-8');
const updatedChangelog = existingChangelog.replace(/^(# Changelog\n\n)?/, ''); // Remove existing header if present
await fs.writeFile('CHANGELOG.md', '# Changelog\n\n' + changelogContent + updatedChangelog);
} catch (error) {
// If file doesn't exist, create new one
await fs.writeFile('CHANGELOG.md', '# Changelog\n\n' + changelogContent);
}
} catch (error) {
if (error instanceof Error && error.message.includes('ENOENT')) {
throw new Error('Failed to update changelog file');
}
throw error;
}
}
function formatConventionalCommits(commits: string): string {
const lines = commits.split('\n').filter(Boolean);
const formatted = lines.map((line) => {
const match = line.match(/^[a-f0-9]+ (feat|fix|docs|style|refactor|test|chore)(\([^)]+\))?: (.+)$/);
if (match) {
const [, type, , message] = match;
return `* ${type}: ${message}`;
}
return `* ${line.replace(/^[a-f0-9]+ /, '')}`;
});
return formatted.join('\n') + '\n\n';
}
function formatSimpleCommits(commits: string): string {
return (
commits
.split('\n')
.filter(Boolean)
.map((line) => `* ${line.replace(/^[a-f0-9]+ /, '')}`)
.join('\n') + '\n\n'
);
}