ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
451 lines (376 loc) • 15.8 kB
JavaScript
/**
* ctrl.shift.left v1.4.0 Release Preparation Script
*
* This script automates the preparation for release:
* 1. Verifies all tests pass
* 2. Updates version numbers in all necessary files
* 3. Generates final documentation
* 4. Creates a git tag
* 5. Prepares npm release
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { PathUtils, FileUtils } = require('./utils/platformUtils');
const { errorHandler } = require('./utils/errorHandler');
// Configuration
const VERSION = '1.4.0';
const RELEASE_DATE = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const TEMP_DIR = path.join(process.cwd(), 'release-prep');
// Colors for terminal output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
magenta: '\x1b[35m'
};
// Print a section header
function printHeader(text) {
console.log(`\n${colors.bright}${colors.cyan}=== ${text} ===${colors.reset}\n`);
}
// Print a success message
function printSuccess(text) {
console.log(`${colors.green}✓ ${text}${colors.reset}`);
}
// Print a warning message
function printWarning(text) {
console.log(`${colors.yellow}! ${text}${colors.reset}`);
}
// Print an error message
function printError(text) {
console.log(`${colors.red}✗ ${text}${colors.reset}`);
}
// Verify that all tests are passing
async function verifyTests() {
printHeader("Verifying Tests");
try {
// Run unit tests
console.log("Running unit tests...");
execSync('npm test', { stdio: 'inherit' });
printSuccess("Unit tests passed");
// Run integration tests
console.log("\nRunning integration tests...");
execSync('node ./scripts/integration-test.js', { stdio: 'inherit' });
printSuccess("Integration tests passed");
// Run cross-platform tests
console.log("\nRunning cross-platform tests...");
execSync('node ./scripts/test-cross-platform.js', { stdio: 'inherit' });
printSuccess("Cross-platform tests passed");
return true;
} catch (error) {
printError(`Test verification failed: ${error.message}`);
return false;
}
}
// Update version numbers in all necessary files
async function updateVersions() {
printHeader("Updating Version Numbers");
try {
// Update package.json
console.log("Updating package.json...");
const packageJsonPath = path.join(process.cwd(), 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const oldVersion = packageJson.version;
packageJson.version = VERSION;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
printSuccess(`Updated package.json version from ${oldVersion} to ${VERSION}`);
// Update package-lock.json if it exists
const packageLockPath = path.join(process.cwd(), 'package-lock.json');
if (fs.existsSync(packageLockPath)) {
console.log("Updating package-lock.json...");
const packageLock = JSON.parse(fs.readFileSync(packageLockPath, 'utf8'));
packageLock.version = VERSION;
if (packageLock.packages && packageLock.packages['']) {
packageLock.packages[''].version = VERSION;
}
fs.writeFileSync(packageLockPath, JSON.stringify(packageLock, null, 2));
printSuccess(`Updated package-lock.json to version ${VERSION}`);
}
// Update CLI version constant
console.log("Updating CLI version constant...");
const cliPath = path.join(process.cwd(), 'bin', 'ctrlshiftleft');
let cliContent = fs.readFileSync(cliPath, 'utf8');
// Replace VERSION constant
cliContent = cliContent.replace(
/const VERSION = ['"].*['"]/,
`const VERSION = '${VERSION}'`
);
fs.writeFileSync(cliPath, cliContent);
printSuccess(`Updated CLI version constant to ${VERSION}`);
// Also update AI-enhanced CLI if it exists
const aiCliPath = path.join(process.cwd(), 'bin', 'ctrlshiftleft-ai');
if (fs.existsSync(aiCliPath)) {
console.log("Updating AI-enhanced CLI version...");
let aiCliContent = fs.readFileSync(aiCliPath, 'utf8');
// Replace VERSION constant
aiCliContent = aiCliContent.replace(
/const VERSION = ['"].*['"]/,
`const VERSION = '${VERSION}'`
);
fs.writeFileSync(aiCliPath, aiCliContent);
printSuccess(`Updated AI-enhanced CLI version to ${VERSION}`);
}
// Update README if it has version references
const readmePath = path.join(process.cwd(), 'README.md');
if (fs.existsSync(readmePath)) {
let readmeContent = fs.readFileSync(readmePath, 'utf8');
// Only update if it contains version references (to avoid false updates)
if (readmeContent.includes('## Version') || readmeContent.includes('version:')) {
console.log("Updating README version references...");
readmeContent = readmeContent.replace(
/ctrl\.shift\.left@\d+\.\d+\.\d+/g,
`ctrl.shift.left@${VERSION}`
);
fs.writeFileSync(readmePath, readmeContent);
printSuccess(`Updated README version references to ${VERSION}`);
}
}
return true;
} catch (error) {
printError(`Version update failed: ${error.message}`);
return false;
}
}
// Generate final documentation
async function generateDocs() {
printHeader("Generating Final Documentation");
try {
// Ensure docs directory exists
const docsDir = path.join(process.cwd(), 'docs');
PathUtils.ensureDir(docsDir);
// Update release date in CHANGELOG.md
console.log("Updating CHANGELOG release date...");
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
if (fs.existsSync(changelogPath)) {
let changelogContent = fs.readFileSync(changelogPath, 'utf8');
// Replace the release date placeholder
changelogContent = changelogContent.replace(
/## \[1\.4\.0\] - \d{4}-\d{2}-\d{2}/,
`## [1.4.0] - ${RELEASE_DATE}`
);
fs.writeFileSync(changelogPath, changelogContent);
printSuccess(`Updated CHANGELOG release date to ${RELEASE_DATE}`);
} else {
printWarning("CHANGELOG.md not found, skipping release date update");
}
// Create a release notes file specifically for this version
console.log("Creating release notes...");
const releaseNotesPath = path.join(docsDir, 'RELEASE_NOTES_1.4.0.md');
// Extract the 1.4.0 section from the CHANGELOG
let changelogContent = fs.readFileSync(changelogPath, 'utf8');
const versionRegex = /## \[1\.4\.0\].+?(?=## \[|$)/s;
const versionMatch = changelogContent.match(versionRegex);
if (versionMatch) {
const releaseNotes = `# ctrl.shift.left v1.4.0 Release Notes\n\nRelease Date: ${RELEASE_DATE}\n\n${versionMatch[0].trim()}`;
fs.writeFileSync(releaseNotesPath, releaseNotes);
printSuccess(`Created release notes at ${releaseNotesPath}`);
} else {
printWarning("Could not extract version info from CHANGELOG, creating basic release notes");
const basicReleaseNotes = `# ctrl.shift.left v1.4.0 Release Notes\n\nRelease Date: ${RELEASE_DATE}\n\nSee CHANGELOG.md for full details.`;
fs.writeFileSync(releaseNotesPath, basicReleaseNotes);
}
// Make sure essential documentation is available
const essentialDocs = [
{ name: 'V1_4_0_FEATURES.md', title: 'v1.4.0 Features' },
{ name: 'UPGRADING_TO_V1_4.md', title: 'Upgrade Guide' },
{ name: 'AI_SECURITY_GUIDE.md', title: 'AI Security Guide' }
];
for (const doc of essentialDocs) {
const docPath = path.join(docsDir, doc.name);
if (!fs.existsSync(docPath)) {
printWarning(`Essential documentation ${doc.name} not found. Creating placeholder...`);
const placeholderContent = `# ${doc.title}\n\nThis documentation will be completed before final release.\n`;
fs.writeFileSync(docPath, placeholderContent);
printSuccess(`Created placeholder for ${doc.name}`);
} else {
printSuccess(`Documentation ${doc.name} exists`);
}
}
return true;
} catch (error) {
printError(`Documentation generation failed: ${error.message}`);
return false;
}
}
// Create a git tag for the release
async function createGitTag() {
printHeader("Creating Git Tag");
try {
// Check if we're in a git repository
try {
execSync('git status', { stdio: 'pipe' });
} catch (gitError) {
printWarning("Not in a git repository, skipping git tag creation");
return true;
}
// Check if there are uncommitted changes
const status = execSync('git status --porcelain').toString();
if (status.trim() !== '') {
printWarning("There are uncommitted changes:");
console.log(status);
// Prompt for confirmation
console.log(`\n${colors.yellow}Do you want to commit all changes with message "Release v${VERSION}"? (y/N)${colors.reset}`);
const readlineSync = require('readline-sync');
const answer = readlineSync.question('> ');
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
// Commit all changes
execSync(`git add .`, { stdio: 'inherit' });
execSync(`git commit -m "Release v${VERSION}"`, { stdio: 'inherit' });
printSuccess("Committed all changes");
} else {
printWarning("Not committing changes, please commit manually before tagging");
return false;
}
}
// Check if the tag already exists
try {
const tags = execSync('git tag').toString();
if (tags.split('\n').includes(`v${VERSION}`)) {
printWarning(`Tag v${VERSION} already exists. Overwrite? (y/N)`);
const readlineSync = require('readline-sync');
const answer = readlineSync.question('> ');
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
execSync(`git tag -d v${VERSION}`, { stdio: 'inherit' });
printSuccess(`Deleted existing tag v${VERSION}`);
} else {
printWarning(`Not overwriting existing tag v${VERSION}`);
return false;
}
}
} catch (tagError) {
// Ignore tag checking errors, proceed with tag creation
}
// Create tag with release notes
const tagMessage = `Release v${VERSION}`;
execSync(`git tag -a v${VERSION} -m "${tagMessage}"`, { stdio: 'inherit' });
printSuccess(`Created git tag v${VERSION}`);
// Remind about pushing the tag
printWarning(`Remember to push the tag with: git push origin v${VERSION}`);
return true;
} catch (error) {
printError(`Git tag creation failed: ${error.message}`);
return false;
}
}
// Prepare npm release
async function prepareNpmRelease() {
printHeader("Preparing NPM Release");
try {
// Create temporary directory for release preparation
if (fs.existsSync(TEMP_DIR)) {
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
}
fs.mkdirSync(TEMP_DIR, { recursive: true });
// Pack the npm package
console.log("Creating npm package...");
execSync('npm pack --pack-destination ' + TEMP_DIR, { stdio: 'inherit' });
// Get the package filename
const packageFilename = fs.readdirSync(TEMP_DIR)
.find(file => file.endsWith('.tgz'));
if (!packageFilename) {
throw new Error("Could not find packed npm package");
}
const packagePath = path.join(TEMP_DIR, packageFilename);
printSuccess(`Created npm package: ${packagePath}`);
// Extract the package to inspect its contents
console.log("Extracting package to verify contents...");
execSync(`tar -xzf ${packagePath} -C ${TEMP_DIR}`, { stdio: 'pipe' });
// Check for essential files
const essentialFiles = [
'package.json',
'README.md',
'bin/ctrlshiftleft',
'src/utils/reactSecurityPatterns.ts',
'src/utils/apiRouteAnalyzer.ts',
'src/utils/configLoader.ts',
'src/utils/performanceTracker.ts',
'src/utils/platformUtils.ts',
'src/utils/errorHandler.ts'
];
const packageContentsDir = path.join(TEMP_DIR, 'package');
const missingFiles = [];
for (const file of essentialFiles) {
const filePath = path.join(packageContentsDir, file);
if (!fs.existsSync(filePath)) {
missingFiles.push(file);
}
}
if (missingFiles.length > 0) {
printWarning("The following essential files are missing from the package:");
missingFiles.forEach(file => console.log(`- ${file}`));
printWarning("Please check your .npmignore file and ensure these files are included");
} else {
printSuccess("All essential files are included in the package");
}
// Check that the package.json has the correct version
const packageJsonPath = path.join(packageContentsDir, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.version !== VERSION) {
printError(`Package version mismatch: ${packageJson.version} (expected ${VERSION})`);
} else {
printSuccess(`Package version is correct: ${VERSION}`);
}
// Provide instructions for publishing
printHeader("NPM Publish Instructions");
console.log(`To publish this package to npm, run:\n`);
console.log(` npm publish ${packagePath} --access public\n`);
console.log(`Or to publish with a tag (recommended for beta/rc):\n`);
console.log(` npm publish ${packagePath} --tag beta --access public\n`);
return true;
} catch (error) {
printError(`NPM release preparation failed: ${error.message}`);
return false;
}
}
// Main function to run all steps
async function main() {
printHeader(`PREPARING RELEASE: ctrl.shift.left v${VERSION}`);
try {
const testResults = await verifyTests();
if (!testResults) {
printError("Tests failed, aborting release preparation");
process.exit(1);
}
const versionResults = await updateVersions();
if (!versionResults) {
printError("Version updates failed, aborting release preparation");
process.exit(1);
}
const docResults = await generateDocs();
if (!docResults) {
printError("Documentation generation failed, aborting release preparation");
process.exit(1);
}
const gitResults = await createGitTag();
if (!gitResults) {
printWarning("Git tag creation was skipped or failed");
// Continue anyway, this might be intentional
}
const npmResults = await prepareNpmRelease();
if (!npmResults) {
printError("NPM release preparation failed");
// Continue anyway, we've done the main preparation
}
printHeader("RELEASE PREPARATION COMPLETE");
console.log(`ctrl.shift.left v${VERSION} is ready for release!\n`);
// Final instructions
console.log(`${colors.magenta}Final Release Checklist:${colors.reset}`);
console.log(`1. Review CHANGELOG.md and documentation for accuracy`);
console.log(`2. Push git commits and tags: git push && git push --tags`);
console.log(`3. Create a GitHub release with RELEASE_NOTES_1.4.0.md`);
console.log(`4. Publish to npm with appropriate access level`);
console.log(`5. Announce the release to users and team members`);
} catch (error) {
printError(`Release preparation failed: ${error.message}`);
process.exit(1);
}
}
// Run the release preparation
main().catch(error => {
printError(`Unhandled error: ${error}`);
process.exit(1);
});