UNPKG

@iota-big3/sdk-production

Version:

Production readiness tools and utilities for SDK

326 lines (325 loc) 11.9 kB
"use strict"; /** * 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;