UNPKG

dryrun-ci

Version:

DryRun CI - Local GitLab CI/CD pipeline testing tool with Docker execution, performance monitoring, and security sandboxing

487 lines (475 loc) • 20.2 kB
#!/usr/bin/env node "use strict"; 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const chalk_1 = __importDefault(require("chalk")); const figlet_1 = __importDefault(require("figlet")); const boxen_1 = __importDefault(require("boxen")); const yamlParser_1 = require("../utils/yamlParser"); const pipelineExecutor_1 = require("../utils/pipelineExecutor"); const execution_1 = require("../types/execution"); const fileValidator_1 = require("../utils/fileValidator"); const errorHelper_1 = require("../utils/errorHelper"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const child_process_1 = require("child_process"); const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8')); const program = new commander_1.Command(); function showBanner() { console.log(chalk_1.default.cyan(figlet_1.default.textSync('DryRun CI', { font: 'ANSI Shadow', horizontalLayout: 'default', verticalLayout: 'default' }))); console.log(chalk_1.default.gray('Test GitLab CI/CD pipelines locally\n')); } program .command('scan') .description('Analyze project for GitLab CI/CD configurations') .option('-p, --path <path>', 'Project path to scan', process.cwd()) .option('-v, --verbose', 'Show detailed output') .action(async (options) => { showBanner(); console.log(chalk_1.default.blue('šŸ” Scanning project...')); console.log(chalk_1.default.gray(`Path: ${options.path}\n`)); try { const validation = fileValidator_1.FileValidator.validateProject(options.path); console.log((0, boxen_1.default)(`${chalk_1.default.green('āœ… GitLab CI/CD:')} ${validation.gitlabCi.fileExists ? chalk_1.default.green('Found') : chalk_1.default.red('Not found')}\n` + `${chalk_1.default.blue('šŸ“¦ Package.json:')} ${fs.existsSync(path.join(options.path, 'package.json')) ? chalk_1.default.green('Found') : chalk_1.default.gray('Not found')}\n` + `${chalk_1.default.cyan('🐳 Dockerfile:')} ${validation.dockerfile.fileExists ? chalk_1.default.green('Found') : chalk_1.default.gray('Not found')}\n` + `${chalk_1.default.magenta('āš™ļø Nixpacks:')} ${validation.nixpacks.fileExists ? chalk_1.default.green('Found') : chalk_1.default.gray('Not found')}`, { title: 'Project Analysis', padding: 1, borderColor: 'blue', borderStyle: 'round' })); const allResults = [validation.gitlabCi, validation.dockerfile, validation.nixpacks]; const hasIssues = allResults.some(result => result.issues.length > 0); if (hasIssues) { console.log(chalk_1.default.yellow('\nāš ļø Configuration Issues Found:')); const formattedIssues = fileValidator_1.FileValidator.formatValidationResults(allResults); console.log(formattedIssues); } const issueStats = allResults.reduce((stats, result) => { result.issues.forEach(issue => { stats[issue.type] = (stats[issue.type] || 0) + 1; }); return stats; }, {}); const criticalIssues = issueStats.critical || 0; if (validation.gitlabCi.fileExists && criticalIssues === 0) { console.log(chalk_1.default.green('\nāœ… Project is ready for pipeline testing!')); console.log(chalk_1.default.gray('Run "dryrun-ci run" to execute your pipeline')); if (validation.buildSystems.length > 1) { console.log(chalk_1.default.blue('\nšŸ’” Multiple build systems detected:')); console.log(chalk_1.default.gray(' Use --build-with dockerfile or --build-with nixpacks to select')); } } else if (criticalIssues > 0) { console.log(chalk_1.default.red('\nāŒ Critical issues must be fixed before running pipeline')); console.log(chalk_1.default.gray('Fix the issues above and run scan again')); console.log(errorHelper_1.ErrorHelper.formatTroubleshootingTips('yaml-syntax')); } else { console.log(chalk_1.default.yellow('\nāš ļø No GitLab CI configuration found')); console.log(chalk_1.default.gray('Run "dryrun-ci init" to create one')); console.log(errorHelper_1.ErrorHelper.formatPrePushChecklist()); } } catch (error) { console.error(chalk_1.default.red('āŒ Error scanning project:'), error); process.exit(1); } }); program .command('init') .description('Initialize GitLab CI/CD configuration') .option('-t, --template <template>', 'Template to use (basic, docker, nixpacks)') .option('-l, --list', 'List available templates') .option('-f, --force', 'Overwrite existing configuration') .action(async (options) => { showBanner(); if (options.list) { console.log(chalk_1.default.blue('šŸ“‹ Available Templates:\n')); console.log(chalk_1.default.green('• basic') + chalk_1.default.gray(' - Simple Node.js pipeline')); console.log(chalk_1.default.green('• docker') + chalk_1.default.gray(' - Docker build and deployment')); console.log(chalk_1.default.green('• nixpacks') + chalk_1.default.gray(' - Nixpacks-optimized pipeline')); return; } const template = options.template || 'basic'; const outputFile = '.gitlab-ci.yml'; if (fs.existsSync(outputFile) && !options.force) { console.log(chalk_1.default.yellow('āš ļø .gitlab-ci.yml already exists')); console.log(chalk_1.default.gray('Use --force to overwrite')); return; } const templates = { basic: `# GitLab CI/CD Pipeline - Basic Template stages: - build - test - deploy variables: NODE_VERSION: "18" build_job: stage: build image: node:18 script: - echo "Installing dependencies" - npm install - echo "Building application" - npm run build artifacts: paths: - dist/ expire_in: 1 hour test_job: stage: test image: node:18 script: - echo "Running tests" - npm test dependencies: - build_job deploy_job: stage: deploy script: - echo "Deploying application" - echo "Deployment completed" dependencies: - build_job when: manual `, docker: `# GitLab CI/CD Pipeline - Docker Template stages: - build - test - deploy build_image: stage: build image: docker:latest services: - docker:dind script: - docker build -t myapp . - docker tag myapp myapp:$CI_COMMIT_SHA test_app: stage: test image: myapp:$CI_COMMIT_SHA script: - npm test - npm run lint deploy_production: stage: deploy image: docker:latest script: - docker push myapp:$CI_COMMIT_SHA only: - main `, nixpacks: `# GitLab CI/CD Pipeline - Nixpacks Template stages: - build - test - deploy variables: NIXPACKS_BUILD: "true" build_with_nixpacks: stage: build image: nixos/nix script: - nix-env -iA nixpkgs.nixpacks - nixpacks build . --name myapp artifacts: paths: - dist/ expire_in: 1 hour test_app: stage: test image: node:18 script: - npm test dependencies: - build_with_nixpacks deploy_app: stage: deploy script: - echo "Deploying with nixpacks" dependencies: - build_with_nixpacks when: manual ` }; try { const templateContent = templates[template]; if (!templateContent) { console.log(chalk_1.default.red(`āŒ Unknown template: ${template}`)); console.log(chalk_1.default.gray('Use --list to see available templates')); return; } fs.writeFileSync(outputFile, templateContent); console.log(chalk_1.default.green(`āœ… Created ${outputFile} with ${template} template`)); console.log(chalk_1.default.gray('Run "dryrun-ci run --dry-run" to test your pipeline')); } catch (error) { console.error(chalk_1.default.red('āŒ Error creating configuration:'), error); process.exit(1); } }); program .command('run') .description('Execute GitLab CI/CD pipeline') .option('-f, --file <file>', 'CI configuration file', '.gitlab-ci.yml') .option('-j, --job <job>', 'Run specific job only') .option('-s, --stage <stage>', 'Run specific stage only') .option('--build-with <system>', 'Build system to use (dockerfile|nixpacks)', 'auto') .option('-d, --dry-run', 'Show execution plan without running') .option('-v, --verbose', 'Show detailed output') .action(async (options) => { showBanner(); if (!fs.existsSync(options.file)) { console.log(chalk_1.default.red(`āŒ Configuration file not found: ${options.file}`)); console.log(chalk_1.default.gray('Run "dryrun-ci init" to create one')); return; } try { const yamlContent = fs.readFileSync(options.file, 'utf8'); const pipeline = (0, yamlParser_1.parseGitLabYaml)(yamlContent); if (options.dryRun) { console.log(chalk_1.default.blue('šŸ” Pipeline Execution Plan:\n')); console.log(chalk_1.default.cyan('Stages:')); pipeline.stages.forEach((stage, index) => { console.log(` ${index + 1}. ${stage}`); }); console.log(chalk_1.default.cyan('\nJobs:')); Object.entries(pipeline.jobs).forEach(([name, job]) => { const gitLabJob = job; console.log(` • ${chalk_1.default.green(name)} (${gitLabJob.stage})`); if (gitLabJob.script) { gitLabJob.script.slice(0, 2).forEach((script) => { console.log(` ${chalk_1.default.gray('$')} ${script}`); }); if (gitLabJob.script.length > 2) { console.log(` ${chalk_1.default.gray(`... and ${gitLabJob.script.length - 2} more commands`)}`); } } }); console.log(chalk_1.default.green('\nāœ… Pipeline validation successful')); console.log(chalk_1.default.gray('Run without --dry-run to execute')); return; } console.log(chalk_1.default.blue('šŸš€ Starting pipeline execution (Docker-based)...\n')); try { const executionConfig = { security: { level: execution_1.SecurityLevel.STRICT, allowNetwork: false, allowedPaths: [], deniedPaths: [] }, performance: { monitoringInterval: 1000, resourceLimits: { maxCpu: 1, maxMemory: 512, maxProcesses: 10 } }, }; const executor = new pipelineExecutor_1.PipelineExecutor(pipeline, executionConfig); executor.on('security-alert', (alert) => { console.log(chalk_1.default.yellow(`āš ļø Security Alert: ${alert.message}`)); }); executor.on('performance-update', (metrics) => { if (metrics.warnings && metrics.warnings.length > 0) { console.log(chalk_1.default.yellow(`āš ļø Performance Warning: ${metrics.warnings.join(', ')}`)); } }); const result = await executor.execute(); for (const job of result.jobs) { const isFailed = job.status === 'failed'; if (options.verbose || isFailed) { console.log(chalk_1.default.yellow(`\n--- Job: ${job.name} (${job.stage}) ---`)); console.log(chalk_1.default.gray('Output:')); console.log(chalk_1.default.gray('----------------------------------------')); console.log(job.output.join('\n')); console.log(chalk_1.default.gray('----------------------------------------\n')); if (isFailed) { console.log(chalk_1.default.red(`āŒ Job '${job.name}' failed. See output above for details.`)); } else if (options.verbose) { console.log(chalk_1.default.green(`āœ… Job '${job.name}' succeeded.`)); } } else { if (job.status === 'success') { console.log(chalk_1.default.green(`āœ… ${job.name} (${job.stage}) succeeded.`)); } } } if (result.status === 'success') { console.log(chalk_1.default.green('\nāœ… Pipeline completed!')); process.exit(0); } else { console.log(chalk_1.default.red('\nāŒ Pipeline failed!')); process.exit(1); } } catch (error) { console.error(chalk_1.default.red('āŒ Pipeline execution failed:'), error); process.exit(1); } } catch (error) { console.error(chalk_1.default.red('āŒ Pipeline execution failed:'), error); process.exit(1); } }); program .command('web') .description('Start web interface') .option('-p, --port <port>', 'Port to run on', '3000') .action((options) => { showBanner(); console.log(chalk_1.default.blue('🌐 Starting web interface...')); console.log(chalk_1.default.gray(`Port: ${options.port}`)); console.log(chalk_1.default.gray(`URL: http://localhost:${options.port}\n`)); const child = (0, child_process_1.spawn)('npm', ['run', 'dev'], { stdio: 'inherit', env: { ...process.env, PORT: options.port } }); child.on('error', (error) => { console.error(chalk_1.default.red('āŒ Failed to start web interface:'), error); process.exit(1); }); process.on('SIGINT', () => { console.log(chalk_1.default.yellow('\nā¹ļø Shutting down web interface...')); child.kill('SIGINT'); process.exit(0); }); }); program .command('fix') .description('Analyze and suggest fixes for common GitLab CI/CD issues') .option('-f, --file <path>', 'Path to GitLab CI file', '.gitlab-ci.yml') .option('--apply', 'Automatically apply suggested fixes (use with caution)') .action(async (options) => { showBanner(); if (!fs.existsSync(options.file)) { console.log(chalk_1.default.red(`āŒ Configuration file not found: ${options.file}`)); console.log(chalk_1.default.gray('Run "dryrun-ci init" to create one')); return; } try { const yamlContent = fs.readFileSync(options.file, 'utf8'); const pipeline = (0, yamlParser_1.parseGitLabYaml)(yamlContent); console.log(chalk_1.default.blue('šŸ”§ Analyzing GitLab CI/CD configuration...\n')); const fixes = analyzeAndSuggestFixes(pipeline, yamlContent); if (fixes.length === 0) { console.log(chalk_1.default.green('āœ… No issues found! Your configuration looks good.')); return; } console.log(chalk_1.default.yellow(`Found ${fixes.length} potential improvements:\n`)); fixes.forEach((fix, index) => { console.log(chalk_1.default.cyan(`${index + 1}. ${fix.title}`)); console.log(chalk_1.default.gray(` ${fix.description}`)); if (fix.suggestion) { console.log(chalk_1.default.green(` šŸ’” Suggestion: ${fix.suggestion}`)); } console.log(''); }); if (options.apply) { console.log(chalk_1.default.yellow('āš ļø Applying suggested fixes...')); const updatedContent = applyFixes(yamlContent, fixes); fs.writeFileSync(options.file, updatedContent, 'utf8'); console.log(chalk_1.default.green(`āœ… Applied fixes to ${options.file}`)); } else { console.log(chalk_1.default.blue('šŸ’” Run with --apply to automatically apply these fixes')); console.log(chalk_1.default.gray(' Or manually review and apply the suggestions above')); } } catch (error) { console.log(chalk_1.default.red(`āŒ Error analyzing configuration: ${error}`)); } }); program .name('dryrun-ci') .description('DryRun CI - Test GitLab CI/CD pipelines locally') .version(packageJson.version); program.parse(); function analyzeAndSuggestFixes(pipeline, yamlContent) { const fixes = []; if (yamlContent.includes(':latest')) { fixes.push({ title: 'Using "latest" image tags', description: 'Latest tags can cause unpredictable builds', suggestion: 'Replace ":latest" with specific version tags (e.g., ":20", ":3.11-slim")', type: 'warning' }); } if (yamlContent.includes('image: docker:latest') && yamlContent.includes('pip install')) { fixes.push({ title: 'Docker image with Python operations', description: 'docker:latest may have Python environment issues', suggestion: 'Use python:3.11-slim for Python operations', type: 'error' }); } if (yamlContent.includes('curl') && yamlContent.includes('| bash')) { fixes.push({ title: 'Security risk: curl | bash pattern', description: 'Downloading and executing scripts without verification', suggestion: 'Download scripts first, verify checksums, then execute', type: 'error' }); } if (yamlContent.includes('yarn install') && !yamlContent.includes('cache:')) { fixes.push({ title: 'Missing cache configuration', description: 'No cache configured for dependency installation', suggestion: 'Add cache configuration to speed up builds', type: 'improvement' }); } if (yamlContent.includes('yarn build') && !yamlContent.includes('artifacts:')) { fixes.push({ title: 'Missing artifacts configuration', description: 'Build output not preserved as artifacts', suggestion: 'Add artifacts configuration to preserve build output', type: 'improvement' }); } return fixes; } function applyFixes(yamlContent, fixes) { let updatedContent = yamlContent; fixes.forEach(fix => { switch (fix.title) { case 'Using "latest" image tags': updatedContent = updatedContent.replace(/image: docker:latest/g, 'image: docker:20'); updatedContent = updatedContent.replace(/image: node:latest/g, 'image: node:20'); updatedContent = updatedContent.replace(/image: python:latest/g, 'image: python:3.11-slim'); break; case 'Docker image with Python operations': updatedContent = updatedContent.replace(/image: docker:latest/g, 'image: python:3.11-slim'); break; } }); return updatedContent; }