UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

451 lines (376 loc) 15.8 kB
#!/usr/bin/env node /** * 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); });