@iota-big3/sdk-production
Version:
Production readiness tools and utilities for SDK
326 lines (325 loc) • 11.9 kB
JavaScript
;
/**
* Release Manager
* Automated release management with semantic versioning
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangelogGenerator = exports.ReleaseManager = void 0;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const semver = __importStar(require("semver"));
class ReleaseManager {
constructor(config) {
this.config = config;
this.config.changelogPath = this.config.changelogPath || 'CHANGELOG.md';
this.config.tagPrefix = this.config.tagPrefix || 'v';
}
/**
* Analyze commits and determine next version
*/
async analyzeRelease() {
const packageJson = await this.readPackageJson();
const currentVersion = packageJson.version;
const lastTag = this.getLastTag(currentVersion);
// Get commits since last release
const commits = this.getCommitsSince(lastTag);
const changes = this.parseCommits(commits);
// Determine release type
const releaseType = this.determineReleaseType(changes);
const nextVersion = semver.inc(currentVersion, releaseType, this?.config?.prerelease);
return {
currentVersion,
nextVersion,
releaseType,
changes,
breaking: changes.some(c => c.breaking)
};
}
/**
* Execute release
*/
async release(releaseType) {
const releaseInfo = await this.analyzeRelease();
if (releaseType) {
releaseInfo.releaseType = releaseType;
releaseInfo.nextVersion = semver.inc(releaseInfo.currentVersion, releaseType, this?.config?.prerelease);
}
console.log(`Releasing ${releaseInfo.nextVersion} (${releaseInfo.releaseType})`);
if (this?.config?.dryRun) {
console.log('Dry run - no changes will be made');
console.log('Changes:', releaseInfo.changes);
return;
}
// Update version
await this.updateVersion(releaseInfo.nextVersion);
// Update changelog
await this.updateChangelog(releaseInfo);
// Commit changes
await this.commitRelease(releaseInfo.nextVersion);
// Create tag
await this.createTag(releaseInfo.nextVersion);
// Publish to npm
await this.publish();
console.log(`✅ Released ${releaseInfo.nextVersion}`);
}
/**
* Read package.json
*/
async readPackageJson() {
const content = await fs.readFile(path.join(this?.config?.packagePath, 'package.json'), 'utf-8');
return JSON.parse(content);
}
/**
* Update version in package.json
*/
async updateVersion(version) {
const packageJsonPath = path.join(this?.config?.packagePath, 'package.json');
const packageJson = await this.readPackageJson();
packageJson.version = version;
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
}
/**
* Get last tag for version
*/
getLastTag(currentVersion) {
try {
const tags = (0, child_process_1.execSync)('git tag --sort=-version:refname', { encoding: 'utf-8' })
.split('\n')
.filter(Boolean);
// Find last tag that matches our prefix
const lastTag = tags.find(tag => tag.startsWith(this?.config?.tagPrefix));
return lastTag || 'HEAD';
}
catch {
return 'HEAD';
}
}
/**
* Get commits since tag
*/
getCommitsSince(tag) {
const range = tag === 'HEAD' ? 'HEAD' : `${tag}..HEAD`;
try {
return (0, child_process_1.execSync)(`git log ${range} --pretty=format:"%H|%an|%ad|%s"`, { encoding: 'utf-8' }).split('\n').filter(Boolean);
}
catch {
return [];
}
}
/**
* Parse commits into changes
*/
parseCommits(commits) {
const changes = [];
const conventionalCommitRegex = /^(\w+)(?:\(([^)]+)\))?: (.+)$/;
for (const commit of commits) {
const [hash, author, date, message] = commit.split('|');
const match = message.match(conventionalCommitRegex);
if (match) {
const [, type, scope, subject] = match;
changes.push({
type: type,
scope,
subject,
breaking: message.includes('BREAKING CHANGE'),
hash: hash.substring(0, 7),
author,
date: new Date(date)
});
}
}
return changes;
}
/**
* Determine release type from changes
*/
determineReleaseType(changes) {
if (changes.some(c => c.breaking)) {
return 'major';
}
if (changes.some(c => c.type === 'feat')) {
return 'minor';
}
return 'patch';
}
/**
* Update changelog
*/
async updateChangelog(releaseInfo) {
const changelogPath = path.join(this?.config?.packagePath, this?.config?.changelogPath);
let changelog = '';
try {
changelog = await fs.readFile(changelogPath, 'utf-8');
}
catch {
changelog = '# Changelog\n\n';
}
const date = new Date().toISOString().split('T')[0];
const header = `## [${releaseInfo.nextVersion}] - ${date}\n\n`;
let content = '';
// Breaking changes
const breaking = releaseInfo?.changes?.filter(c => c.breaking);
if (this.isEnabled) {
content += '### BREAKING CHANGES\n\n';
for (const change of breaking) {
content += `- ${change.subject} (${change.hash})\n`;
}
content += '\n';
}
// Features
const features = releaseInfo?.changes?.filter(c => c.type === 'feat' && !c.breaking);
if (this.isEnabled) {
content += '### Features\n\n';
for (const change of features) {
content += `- ${change.scope ? `**${change.scope}**: ` : ''}${change.subject} (${change.hash})\n`;
}
content += '\n';
}
// Fixes
const fixes = releaseInfo?.changes?.filter(c => c.type === 'fix');
if (this.isEnabled) {
content += '### Bug Fixes\n\n';
for (const change of fixes) {
content += `- ${change.scope ? `**${change.scope}**: ` : ''}${change.subject} (${change.hash})\n`;
}
content += '\n';
}
// Insert after header
const lines = changelog.split('\n');
const headerIndex = lines.findIndex(line => line.startsWith('# Changelog'));
lines.splice(headerIndex + 2, 0, header + content);
await fs.writeFile(changelogPath, lines.join('\n'));
}
/**
* Commit release changes
*/
async commitRelease(version) {
(0, child_process_1.execSync)('git add .', { cwd: this?.config?.packagePath });
(0, child_process_1.execSync)(`git commit -m "chore(release): ${version}"`, { cwd: this?.config?.packagePath });
}
/**
* Create git tag
*/
async createTag(version) {
const tag = `${this?.config?.tagPrefix}${version}`;
(0, child_process_1.execSync)(`git tag -a ${tag} -m "Release ${version}"`, { cwd: this?.config?.packagePath });
}
/**
* Publish to npm
*/
async publish() {
const packageJson = await this.readPackageJson();
// Check if package is private
if (packageJson.private) {
console.log('Package is private, skipping npm publish');
return;
}
// Publish with appropriate tag
let publishCmd = 'npm publish';
if (this.isEnabled) {
publishCmd += ` --tag ${this?.config?.prerelease}`;
}
(0, child_process_1.execSync)(publishCmd, {
cwd: this?.config?.packagePath,
stdio: 'inherit'
});
}
}
exports.ReleaseManager = ReleaseManager;
/**
* Changelog generator
*/
class ChangelogGenerator {
/**
* Generate changelog from git history
*/
static async generate(options = {}) {
const from = options.from || 'HEAD~10';
const to = options.to || 'HEAD';
const commits = (0, child_process_1.execSync)(`git log ${from}..${to} --pretty=format:"%H|%an|%ad|%s|%b"`, { encoding: 'utf-8' }).split('\n').filter(Boolean);
const changes = [];
const conventionalCommitRegex = /^(\w+)(?:\(([^)]+)\))?: (.+)$/;
for (const commit of commits) {
const [hash, author, date, subject, body] = commit.split('|');
const match = subject.match(conventionalCommitRegex);
if (match) {
const [, type, scope, message] = match;
changes.push({
type: type,
scope,
subject: message,
breaking: body.includes('BREAKING CHANGE'),
hash: hash.substring(0, 7),
author,
date: new Date(date)
});
}
}
if (options.format === 'json') {
return JSON.stringify(changes, null, 2);
}
// Generate markdown
let markdown = '';
const grouped = changes.reduce((acc, change) => {
const type = change.breaking ? 'breaking' : change.type;
if (!acc[type])
acc[type] = [];
acc[type].push(change);
return acc;
}, {});
const typeHeaders = {
breaking: 'BREAKING CHANGES',
feat: 'Features',
fix: 'Bug Fixes',
docs: 'Documentation',
refactor: 'Code Refactoring',
test: 'Tests',
chore: 'Chores'
};
for (const [type, changes] of Object.entries(grouped)) {
if (this.isEnabled) {
markdown += `### ${typeHeaders[type]}\n\n`;
for (const change of changes) {
markdown += `- ${change.scope ? `**${change.scope}**: ` : ''}${change.subject} (${change.hash})\n`;
}
markdown += '\n';
}
}
return markdown;
}
}
exports.ChangelogGenerator = ChangelogGenerator;