213 lines
6.5 KiB
JavaScript
Executable File
213 lines
6.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Version Bumping Script
|
|
*
|
|
* Bumps the version based on conventional commits since last release.
|
|
* Supports:
|
|
* - major: Breaking changes (! or BREAKING CHANGE)
|
|
* - minor: Features (feat:)
|
|
* - patch: Fixes (fix:) and other changes
|
|
*/
|
|
|
|
const { execSync } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
const forcedVersion = args[0]; // e.g., "0.2.0" or "patch/minor/major"
|
|
const skipConfirm = args.includes('--yes') || args.includes('-y'); // Skip confirmation prompt
|
|
|
|
// Paths
|
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
|
|
|
|
// Load current version from package.json
|
|
function getCurrentVersion() {
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
return packageJson.version;
|
|
}
|
|
|
|
// Parse version string into components
|
|
function parseVersion(version) {
|
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
return { major, minor, patch };
|
|
}
|
|
|
|
// Format version string
|
|
function formatVersion(major, minor, patch) {
|
|
return `${major}.${minor}.${patch}`;
|
|
}
|
|
|
|
// Get commits since last tag
|
|
function getCommitsSinceLastTag() {
|
|
try {
|
|
const tags = execSync('git tag --sort=-version:refname', { encoding: 'utf8' }).trim();
|
|
const latestTag = tags.split('\n')[0];
|
|
|
|
if (!latestTag) {
|
|
// No tags yet, get all commits
|
|
return execSync('git log --oneline --format=%s', { encoding: 'utf8' }).trim().split('\n');
|
|
}
|
|
|
|
const commits = execSync(`git log ${latestTag}..HEAD --oneline --format=%s`, { encoding: 'utf8' }).trim();
|
|
return commits ? commits.split('\n') : [];
|
|
} catch (error) {
|
|
// If no commits since tag or other error, get recent commits
|
|
return execSync('git log --oneline --format=%s -n 20', { encoding: 'utf8' }).trim().split('\n');
|
|
}
|
|
}
|
|
|
|
// Analyze commits to determine version bump type
|
|
function analyzeCommits(commits) {
|
|
let bumpType = 'patch'; // default
|
|
|
|
for (const commit of commits) {
|
|
if (!commit) continue;
|
|
|
|
// Breaking change indicators
|
|
if (commit.includes('!') || commit.includes('BREAKING CHANGE')) {
|
|
bumpType = 'major';
|
|
break; // Major takes precedence
|
|
}
|
|
|
|
// Feature indicators
|
|
if (commit.startsWith('feat:') || commit.startsWith('feat(')) {
|
|
if (bumpType === 'patch') {
|
|
bumpType = 'minor';
|
|
}
|
|
}
|
|
|
|
// Fix indicators (already patch by default)
|
|
}
|
|
|
|
return bumpType;
|
|
}
|
|
|
|
// Generate new version based on bump type
|
|
function generateNewVersion(currentVersion, bumpType) {
|
|
const { major, minor, patch } = parseVersion(currentVersion);
|
|
|
|
switch (bumpType) {
|
|
case 'major':
|
|
return formatVersion(major + 1, 0, 0);
|
|
case 'minor':
|
|
return formatVersion(major, minor + 1, 0);
|
|
case 'patch':
|
|
default:
|
|
return formatVersion(major, minor, patch + 1);
|
|
}
|
|
}
|
|
|
|
// Update package.json version
|
|
function updatePackageJson(newVersion) {
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
packageJson.version = newVersion;
|
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
console.log(`✓ Updated package.json to version ${newVersion}`);
|
|
}
|
|
|
|
// Generate changelog entry
|
|
function generateChangelogEntry(newVersion, commits, bumpType) {
|
|
const date = new Date().toISOString().split('T')[0];
|
|
const relevantCommits = commits.filter(commit => {
|
|
if (!commit) return false;
|
|
// Filter out non-user-facing commits
|
|
if (commit.startsWith('chore:') && !commit.includes('BREAKING')) return false;
|
|
return true;
|
|
});
|
|
|
|
const changelogEntry = `## [${newVersion}] - ${date}\n\n` +
|
|
`### ${bumpType.charAt(0).toUpperCase() + bumpType.slice(1)} Changes\n\n` +
|
|
relevantCommits.map(commit => `- ${commit}`).join('\n') +
|
|
'\n\n';
|
|
|
|
return changelogEntry;
|
|
}
|
|
|
|
// Update CHANGELOG.md
|
|
function updateChangelog(newVersion, commits, bumpType) {
|
|
const changelogEntry = generateChangelogEntry(newVersion, commits, bumpType);
|
|
|
|
if (fs.existsSync(changelogPath)) {
|
|
const content = fs.readFileSync(changelogPath, 'utf8');
|
|
const newContent = changelogEntry + content;
|
|
fs.writeFileSync(changelogPath, newContent);
|
|
} else {
|
|
fs.writeFileSync(changelogPath, `# Changelog\n\n${changelogEntry}`);
|
|
}
|
|
|
|
console.log(`✓ Updated CHANGELOG.md`);
|
|
}
|
|
|
|
// Main function
|
|
function main() {
|
|
console.log('🔄 Version Bumping Tool\n');
|
|
|
|
const currentVersion = getCurrentVersion();
|
|
console.log(`Current version: ${currentVersion}`);
|
|
|
|
let newVersion;
|
|
let bumpType;
|
|
|
|
if (forcedVersion) {
|
|
// Check if it's a bump type or explicit version
|
|
if (['patch', 'minor', 'major'].includes(forcedVersion.toLowerCase())) {
|
|
bumpType = forcedVersion.toLowerCase();
|
|
newVersion = generateNewVersion(currentVersion, bumpType);
|
|
console.log(`Forced bump type: ${bumpType}`);
|
|
} else {
|
|
// Assume it's an explicit version
|
|
newVersion = forcedVersion;
|
|
bumpType = 'custom';
|
|
console.log(`Forced version: ${newVersion}`);
|
|
}
|
|
} else {
|
|
// Analyze commits to determine bump type
|
|
const commits = getCommitsSinceLastTag();
|
|
console.log(`\nAnalyzing ${commits.length} commits since last release...`);
|
|
|
|
bumpType = analyzeCommits(commits);
|
|
newVersion = generateNewVersion(currentVersion, bumpType);
|
|
|
|
console.log(`\nDetected bump type: ${bumpType}`);
|
|
console.log(`New version: ${currentVersion} → ${newVersion}`);
|
|
}
|
|
|
|
// Confirm with user
|
|
const readline = require('readline');
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
});
|
|
|
|
rl.question(`\nApply version ${newVersion}? (y/N) `, (answer) => {
|
|
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
// Update package.json
|
|
updatePackageJson(newVersion);
|
|
|
|
// Update changelog
|
|
if (bumpType !== 'custom') {
|
|
const commits = getCommitsSinceLastTag();
|
|
updateChangelog(newVersion, commits, bumpType);
|
|
}
|
|
|
|
console.log(`\n✅ Version bumped to ${newVersion}`);
|
|
console.log(`\nNext steps:`);
|
|
console.log(` 1. Review changes: git diff`);
|
|
console.log(` 2. Commit changes: git commit -am "chore: bump version to v${newVersion}"`);
|
|
console.log(` 3. Create tag: git tag -a v${newVersion} -m "Release v${newVersion}"`);
|
|
console.log(` 4. Push changes: git push origin main`);
|
|
console.log(` 5. Push tag: git push origin v${newVersion}`);
|
|
} else {
|
|
console.log('❌ Version bump cancelled');
|
|
}
|
|
|
|
rl.close();
|
|
});
|
|
}
|
|
|
|
// Run main function
|
|
main();
|