wifi-densepose/vendor/sublinear-time-solver/crates/psycho-symbolic-reasoner/scripts/release.js

395 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* Release Script for Psycho-Symbolic Reasoner
*
* This script automates the release process:
* 1. Version validation and bumping
* 2. Changelog generation
* 3. Build validation
* 4. Testing
* 5. Git tagging
* 6. NPM publishing
* 7. GitHub release creation
*/
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import { join, resolve } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename, '..');
const rootDir = resolve(__dirname, '..');
// Colors for console output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
};
function log(color, prefix, message) {
console.log(`${color}${prefix}${colors.reset} ${message}`);
}
function info(message) {
log(colors.blue, '[INFO]', message);
}
function success(message) {
log(colors.green, '[SUCCESS]', message);
}
function warn(message) {
log(colors.yellow, '[WARN]', message);
}
function error(message) {
log(colors.red, '[ERROR]', message);
}
async function runCommand(command, args = [], options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: options.silent ? 'pipe' : 'inherit',
cwd: options.cwd || rootDir,
...options
});
let stdout = '';
let stderr = '';
if (options.silent) {
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
}
child.on('close', (code) => {
if (code === 0) {
resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() });
} else {
reject(new Error(`Command failed: ${command} ${args.join(' ')}\\n${stderr}`));
}
});
child.on('error', reject);
});
}
async function getCurrentVersion() {
const packageJson = JSON.parse(
await fs.readFile(resolve(rootDir, 'package.json'), 'utf8')
);
return packageJson.version;
}
async function validateWorkingDirectory() {
info('Validating working directory...');
try {
const { stdout } = await runCommand('git', ['status', '--porcelain'], { silent: true });
if (stdout.trim()) {
error('Working directory is not clean. Please commit or stash your changes.');
console.log('Uncommitted changes:');
console.log(stdout);
process.exit(1);
}
} catch (error) {
error(`Failed to check git status: ${error.message}`);
process.exit(1);
}
// Check if we're on main or develop branch
try {
const { stdout } = await runCommand('git', ['branch', '--show-current'], { silent: true });
const currentBranch = stdout.trim();
if (!['main', 'develop'].includes(currentBranch)) {
warn(`You are on branch '${currentBranch}'. Releases should typically be made from 'main' or 'develop'.`);
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise(resolve => {
rl.question('Continue anyway? (y/N): ', resolve);
});
rl.close();
if (answer.toLowerCase() !== 'y') {
info('Release cancelled.');
process.exit(0);
}
}
} catch (error) {
warn(`Could not determine current branch: ${error.message}`);
}
success('Working directory validated');
}
async function runTests() {
info('Running tests...');
try {
await runCommand('npm', ['test']);
success('All tests passed');
} catch (error) {
error(`Tests failed: ${error.message}`);
process.exit(1);
}
}
async function runLinting() {
info('Running linter...');
try {
await runCommand('npm', ['run', 'lint']);
success('Linting passed');
} catch (error) {
error(`Linting failed: ${error.message}`);
process.exit(1);
}
}
async function buildProject() {
info('Building project...');
try {
await runCommand('npm', ['run', 'build']);
success('Build completed');
} catch (error) {
error(`Build failed: ${error.message}`);
process.exit(1);
}
}
async function bumpVersion(type) {
info(`Bumping version: ${type}`);
try {
const { stdout } = await runCommand('npm', ['version', type, '--no-git-tag-version'], { silent: true });
const newVersion = stdout.trim();
success(`Version bumped to ${newVersion}`);
return newVersion.replace('v', '');
} catch (error) {
error(`Failed to bump version: ${error.message}`);
process.exit(1);
}
}
async function updateChangelog(version) {
info('Updating CHANGELOG.md...');
const changelogPath = resolve(rootDir, 'CHANGELOG.md');
let changelog;
try {
changelog = await fs.readFile(changelogPath, 'utf8');
} catch (error) {
warn('CHANGELOG.md not found, creating new one');
changelog = '# Changelog\\n\\nAll notable changes to this project will be documented in this file.\\n\\n';
}
const today = new Date().toISOString().split('T')[0];
const unreleased = '## [Unreleased]';
const newVersionSection = `## [${version}] - ${today}\\n\\n### Added\\n- Release ${version}\\n\\n`;
if (changelog.includes(unreleased)) {
changelog = changelog.replace(unreleased, `${unreleased}\\n\\n${newVersionSection}`);
} else {
const lines = changelog.split('\\n');
lines.splice(3, 0, '', newVersionSection);
changelog = lines.join('\\n');
}
await fs.writeFile(changelogPath, changelog);
success('CHANGELOG.md updated');
}
async function commitAndTag(version) {
info(`Creating git commit and tag for version ${version}...`);
try {
// Add all changes
await runCommand('git', ['add', '.']);
// Commit
await runCommand('git', ['commit', '-m', `chore(release): ${version}`]);
// Create tag
await runCommand('git', ['tag', `v${version}`]);
success(`Committed and tagged version ${version}`);
} catch (error) {
error(`Failed to commit and tag: ${error.message}`);
process.exit(1);
}
}
async function publishToNpm(tag = 'latest') {
info(`Publishing to NPM with tag: ${tag}`);
try {
// Check if user is logged in
try {
await runCommand('npm', ['whoami'], { silent: true });
} catch {
error('You are not logged in to NPM. Please run: npm login');
process.exit(1);
}
// Publish
const args = ['publish'];
if (tag !== 'latest') {
args.push('--tag', tag);
}
await runCommand('npm', args);
success(`Published to NPM with tag: ${tag}`);
} catch (error) {
error(`Failed to publish to NPM: ${error.message}`);
process.exit(1);
}
}
async function createGitHubRelease(version) {
info(`Creating GitHub release for version ${version}...`);
try {
// Check if gh CLI is available
await runCommand('gh', ['--version'], { silent: true });
// Create release
await runCommand('gh', [
'release', 'create',
`v${version}`,
'--title', `Release ${version}`,
'--notes', `Release notes for version ${version}. See CHANGELOG.md for details.`,
'--verify-tag'
]);
success(`GitHub release created for version ${version}`);
} catch (error) {
warn(`Could not create GitHub release: ${error.message}`);
warn('You may need to install GitHub CLI (gh) or create the release manually');
}
}
async function pushToRemote() {
info('Pushing to remote repository...');
try {
await runCommand('git', ['push']);
await runCommand('git', ['push', '--tags']);
success('Pushed to remote repository');
} catch (error) {
error(`Failed to push to remote: ${error.message}`);
process.exit(1);
}
}
async function main() {
const startTime = Date.now();
// Parse command line arguments
const args = process.argv.slice(2);
const versionType = args[0] || 'patch';
const npmTag = args.includes('--beta') ? 'beta' : 'latest';
const skipTests = args.includes('--skip-tests');
const dryRun = args.includes('--dry-run');
if (!['patch', 'minor', 'major', 'prerelease'].includes(versionType)) {
error('Invalid version type. Use: patch, minor, major, or prerelease');
process.exit(1);
}
try {
log(colors.cyan, '[RELEASE]', `Starting release process for ${versionType} version...`);
if (dryRun) {
warn('DRY RUN MODE - No changes will be made');
}
const currentVersion = await getCurrentVersion();
info(`Current version: ${currentVersion}`);
await validateWorkingDirectory();
if (!skipTests) {
await runLinting();
await runTests();
}
await buildProject();
if (!dryRun) {
const newVersion = await bumpVersion(versionType);
await updateChangelog(newVersion);
await commitAndTag(newVersion);
await pushToRemote();
await publishToNpm(npmTag);
await createGitHubRelease(newVersion);
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
success(`Release ${newVersion} completed successfully in ${duration}s`);
console.log('\\n📦 Release Summary:');
console.log(` Version: ${newVersion}`);
console.log(` NPM Tag: ${npmTag}`);
console.log(` Duration: ${duration}s`);
console.log('\\n🚀 Your package is now available:');
console.log(` npm install psycho-symbolic-reasoner@${newVersion}`);
console.log(` npx psycho-symbolic-reasoner@${newVersion}`);
} else {
success('Dry run completed - no changes made');
}
} catch (error) {
error(`Release failed: ${error.message}`);
process.exit(1);
}
}
// Handle command line arguments
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Usage: node scripts/release.js [version-type] [options]
Version Types:
patch Increment patch version (1.0.0 -> 1.0.1)
minor Increment minor version (1.0.0 -> 1.1.0)
major Increment major version (1.0.0 -> 2.0.0)
prerelease Increment prerelease version (1.0.0 -> 1.0.1-0)
Options:
--help, -h Show this help message
--beta Publish with 'beta' tag instead of 'latest'
--skip-tests Skip running tests
--dry-run Simulate the release without making changes
Examples:
node scripts/release.js patch # Patch release
node scripts/release.js minor --beta # Minor beta release
node scripts/release.js major --dry-run # Simulate major release
`);
process.exit(0);
}
main().catch(console.error);