UNPKG

dryrun-ci

Version:

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

554 lines (553 loc) โ€ข 27.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PipelineSimulator = void 0; const performanceMonitor_1 = require("./performanceMonitor"); const react_hot_toast_1 = __importDefault(require("react-hot-toast")); class PipelineSimulator { constructor(config) { this.isRunning = false; this.cancelRequested = false; this.pipeline = config.pipeline; this.containerBuilder = config.containerBuilder; this.onUpdate = config.onUpdate; this.projectContext = config.projectContext; this.performanceMonitor = new performanceMonitor_1.PerformanceMonitor(); this.execution = { id: Math.random().toString(36).substring(7), status: 'pending', startTime: new Date(), jobs: [] }; } createJobExecutions() { return Object.entries(this.pipeline.jobs).map(([name, job]) => ({ name, stage: job.stage, status: 'pending', output: [], startTime: new Date() })); } async runPipeline() { if (this.isRunning) return; this.isRunning = true; this.execution.status = 'running'; this.execution.startTime = new Date(); this.onUpdate(this.execution); try { if (this.projectContext) { this.addProjectContextLogs(); } this.addPipelineValidationLogs(); const stageGroups = this.groupJobsByStage(); for (const stage of this.pipeline.stages) { if (this.cancelRequested) break; const stageJobs = stageGroups[stage] || []; if (stageJobs.length === 0) continue; this.addStageStartLogs(stage, stageJobs.length); await Promise.all(stageJobs.map(job => this.executeJob(job))); const stageHasFailed = stageJobs.some(job => this.execution.jobs.find(j => j.name === job.name)?.status === 'failed'); if (stageHasFailed) { this.execution.status = 'failed'; this.addPipelineFailureLogs(stage); const failedJob = this.execution.jobs.find(j => j.status === 'failed'); react_hot_toast_1.default.error(`Pipeline failed in job "${failedJob?.name}" (${stage} stage)`, { duration: 6000, icon: 'โŒ', }); break; } else { this.addStageCompleteLogs(stage); react_hot_toast_1.default.success(`Stage "${stage}" completed successfully`, { duration: 3000, icon: 'โœ…', }); } } if (!this.cancelRequested && this.execution.status === 'running') { this.execution.status = 'success'; this.addPipelineSuccessLogs(); const duration = this.execution.endTime ? Math.round((this.execution.endTime.getTime() - this.execution.startTime.getTime()) / 1000) : Math.round((Date.now() - this.execution.startTime.getTime()) / 1000); react_hot_toast_1.default.success(`๐ŸŽ‰ Pipeline completed successfully in ${duration}s!`, { duration: 5000, icon: '๐Ÿš€', }); } } catch (error) { this.execution.status = 'failed'; this.addPipelineErrorLogs(error); react_hot_toast_1.default.error(`Pipeline execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { duration: 6000, icon: '๐Ÿ’ฅ', }); } finally { this.execution.endTime = new Date(); this.execution.duration = this.execution.endTime.getTime() - this.execution.startTime.getTime(); this.isRunning = false; this.onUpdate(this.execution); } } addProjectContextLogs() { const firstJob = this.execution.jobs[0]; if (!firstJob || !this.projectContext) return; firstJob.output.push('๐Ÿ” Project Context Information:'); firstJob.output.push(`๐Ÿ“ Project: ${this.projectContext.rootPath}`); if (this.projectContext.nixpacksConfig) { firstJob.output.push('๐Ÿ“ฆ Nixpacks configuration detected'); if (this.projectContext.nixpacksConfig.providers) { firstJob.output.push(` Providers: ${this.projectContext.nixpacksConfig.providers.join(', ')}`); } } if (this.projectContext.packageJson) { firstJob.output.push(`๐Ÿ“„ Package.json: ${this.projectContext.packageJson.name}@${this.projectContext.packageJson.version}`); } if (this.projectContext.dockerfile) { firstJob.output.push('๐Ÿณ Dockerfile detected'); } firstJob.output.push(''); } addPipelineValidationLogs() { const firstJob = this.execution.jobs[0]; if (!firstJob) return; firstJob.output.push('โœ… Pipeline validation successful'); firstJob.output.push(`๐Ÿ“‹ Stages: ${this.pipeline.stages.join(' โ†’ ')}`); firstJob.output.push(`๐Ÿ”ง Jobs: ${Object.keys(this.pipeline.jobs).length}`); if (this.pipeline.variables && Object.keys(this.pipeline.variables).length > 0) { firstJob.output.push(`๐Ÿ”ง Variables: ${Object.keys(this.pipeline.variables).length} defined`); } firstJob.output.push(''); } addStageStartLogs(stage, jobCount) { const firstJobInStage = this.execution.jobs.find(j => this.pipeline.jobs[j.name]?.stage === stage); if (firstJobInStage) { firstJobInStage.output.push(`๐Ÿš€ Starting stage: ${stage}`); firstJobInStage.output.push(`๐Ÿ“Š Jobs in stage: ${jobCount}`); firstJobInStage.output.push(''); } } addStageCompleteLogs(stage) { const lastJobInStage = this.execution.jobs .filter(j => this.pipeline.jobs[j.name]?.stage === stage) .pop(); if (lastJobInStage) { lastJobInStage.output.push(''); lastJobInStage.output.push(`โœ… Stage '${stage}' completed successfully`); } } addPipelineFailureLogs(failedStage) { const failedJob = this.execution.jobs.find(j => j.status === 'failed'); if (failedJob) { failedJob.output.push(''); failedJob.output.push(`โŒ Pipeline failed in stage: ${failedStage}`); failedJob.output.push('๐Ÿ’ก Check the logs above for error details'); } } addPipelineSuccessLogs() { const lastJob = this.execution.jobs[this.execution.jobs.length - 1]; if (lastJob) { lastJob.output.push(''); lastJob.output.push('๐ŸŽ‰ Pipeline completed successfully!'); lastJob.output.push('โœ… All stages and jobs executed without errors'); } } addPipelineErrorLogs(error) { const lastJob = this.execution.jobs[this.execution.jobs.length - 1]; if (lastJob) { lastJob.output.push(''); lastJob.output.push(`โŒ Pipeline execution error: ${error instanceof Error ? error.message : 'Unknown error'}`); } } groupJobsByStage() { const groups = {}; Object.entries(this.pipeline.jobs).forEach(([name, job]) => { const gitLabJob = job; if (!groups[gitLabJob.stage]) { groups[gitLabJob.stage] = []; } groups[gitLabJob.stage].push({ jobName: name, ...gitLabJob }); }); return groups; } async executeJob(job) { const jobExecution = this.execution.jobs.find((j) => j.name === job.name); if (!jobExecution || this.cancelRequested) return; jobExecution.status = 'running'; jobExecution.startTime = new Date(); jobExecution.output = []; this.execution.currentJob = job.name; this.onUpdate(this.execution); try { this.performanceMonitor.start(); if (!job.stage) { throw new Error('Job stage is not defined. Each job must belong to a stage in your .gitlab-ci.yml'); } jobExecution.output.push('โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); jobExecution.output.push(`โ•‘ ๐Ÿ”„ Job: ${job.name}`); jobExecution.output.push(`โ•‘ ๐Ÿ“ Stage: ${job.stage}`); jobExecution.output.push(`โ•‘ โฑ๏ธ Started at: ${jobExecution.startTime.toISOString()}`); jobExecution.output.push('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n'); if (job.image) { const [imageName, imageTag] = job.image.split(':'); if (!imageTag) { jobExecution.output.push('โš ๏ธ Warning: Using latest tag is not recommended for production'); jobExecution.output.push(' Consider specifying a fixed version tag for better reproducibility'); } jobExecution.output.push('๐Ÿ“ฆ Container Configuration:'); jobExecution.output.push(` Image: ${imageName}`); jobExecution.output.push(` Tag: ${imageTag || 'latest'}`); jobExecution.output.push(''); } else { jobExecution.output.push('โš ๏ธ No image specified for this job'); jobExecution.output.push(' Using default image from parent configuration'); jobExecution.output.push(''); } const envVars = this.getEnvironmentVariables(job); const requiredVars = ['CI_JOB_NAME', 'CI_JOB_STAGE', 'CI_PIPELINE_ID']; const missingVars = requiredVars.filter(v => !envVars[v]); if (missingVars.length > 0) { jobExecution.output.push('โš ๏ธ Warning: Missing required environment variables:'); missingVars.forEach(v => jobExecution.output.push(` - ${v}`)); jobExecution.output.push(''); } if (Object.keys(envVars).length > 0) { jobExecution.output.push('๐ŸŒ Environment Variables:'); Object.entries(envVars).forEach(([key, value]) => { const isSensitive = key.toLowerCase().match(/(token|password|secret|key|auth)/i); const displayValue = isSensitive ? '********' : value; if (isSensitive && value.length < 8) { jobExecution.output.push(` โš ๏ธ ${key}=${displayValue} (Warning: Short sensitive value)`); } else { jobExecution.output.push(` ${key}=${displayValue}`); } }); jobExecution.output.push(''); } const securityConfig = { network: true, level: 'BASIC' }; jobExecution.output.push('๐Ÿ”’ Security Configuration:'); if (securityConfig.network) { jobExecution.output.push(' โš ๏ธ Network Access: Enabled (Consider restricting if not needed)'); } else { jobExecution.output.push(' โœ… Network Access: Disabled'); } jobExecution.output.push(` Security Level: ${securityConfig.level}`); jobExecution.output.push(''); if (this.containerBuilder && job.image && !job.image.includes(':')) { jobExecution.output.push('๐Ÿ—๏ธ Building Container Image:'); try { await this.containerBuilder.buildImage(this.projectContext.path || '.', job.image); jobExecution.output.push(' โœ… Image built successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; jobExecution.output.push(' โŒ Image build failed:'); jobExecution.output.push(` Error: ${errorMessage}`); jobExecution.output.push(' Troubleshooting steps:'); jobExecution.output.push(' 1. Check if Docker daemon is running'); jobExecution.output.push(' 2. Verify image name and registry access'); jobExecution.output.push(' 3. Check network connectivity'); jobExecution.output.push(' 4. Review Dockerfile if using custom image'); throw new Error(`Container build failed: ${errorMessage}`); } jobExecution.output.push(''); } const scripts = [ ...(this.pipeline.before_script || []), ...(job.before_script || []), ...(job.script || []), ...(job.after_script || []), ...(this.pipeline.after_script || []) ]; if (scripts.length === 0) { jobExecution.output.push('โš ๏ธ Warning: No scripts defined for this job'); jobExecution.output.push(' Add script: section to your job configuration'); jobExecution.output.push(''); } for (const script of scripts) { if (this.cancelRequested) { jobExecution.output.push('\nโš ๏ธ Job cancelled by user'); break; } jobExecution.output.push(`\n$ ${script}`); this.onUpdate(this.execution); try { const output = await this.simulateCommandOutput(script, job); output.forEach(line => { if (line.match(/(error|fail|fatal)/i)) { jobExecution.output.push(` โŒ ${line}`); } else if (line.match(/(warning|warn)/i)) { jobExecution.output.push(` โš ๏ธ ${line}`); } else if (line.match(/(success|done|completed)/i)) { jobExecution.output.push(` โœ… ${line}`); } else { jobExecution.output.push(` ${line}`); } }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; jobExecution.output.push(` โŒ Command failed: ${script}`); jobExecution.output.push(` Error: ${errorMessage}`); if (errorMessage.includes('not found')) { jobExecution.output.push(' Possible solutions:'); jobExecution.output.push(' 1. Check if the command is installed in the container'); jobExecution.output.push(' 2. Add required packages to your Dockerfile or before_script'); jobExecution.output.push(' 3. Verify PATH environment variable'); } else if (errorMessage.includes('permission denied')) { jobExecution.output.push(' Possible solutions:'); jobExecution.output.push(' 1. Check file permissions'); jobExecution.output.push(' 2. Verify user permissions in container'); jobExecution.output.push(' 3. Use sudo if required (and allowed)'); } else if (errorMessage.includes('network')) { jobExecution.output.push(' Possible solutions:'); jobExecution.output.push(' 1. Check network connectivity'); jobExecution.output.push(' 2. Verify proxy settings if using'); jobExecution.output.push(' 3. Check firewall rules'); } throw error; } } const metrics = this.performanceMonitor.getMetrics(); if (metrics) { jobExecution.output.push('\n๐Ÿ“Š Performance Metrics:'); const cpuUsage = metrics.cpu.usage.toFixed(1); jobExecution.output.push(` CPU Usage: ${cpuUsage}%${parseFloat(cpuUsage) > 80 ? ' โš ๏ธ High CPU usage detected' : ''}`); const memoryUsed = this.formatBytes(metrics.memory.heapUsed); const memoryTotal = this.formatBytes(metrics.memory.heapTotal); const memoryPercentage = (metrics.memory.heapUsed / metrics.memory.heapTotal) * 100; jobExecution.output.push(` Memory: ${memoryUsed} / ${memoryTotal}${memoryPercentage > 80 ? ' โš ๏ธ High memory usage detected' : ''}`); if (metrics.gc) { jobExecution.output.push(` GC: ${metrics.gc.type} (${metrics.gc.duration}ms)${metrics.gc.duration > 1000 ? ' โš ๏ธ Long GC pause detected' : ''}`); } jobExecution.metrics = metrics; } jobExecution.status = 'success'; jobExecution.endTime = new Date(); const duration = jobExecution.endTime.getTime() - jobExecution.startTime.getTime(); jobExecution.output.push('\nโœ… Job completed successfully!'); jobExecution.output.push(`โฑ๏ธ Duration: ${this.formatDuration(duration)}`); if (duration > 300000) { jobExecution.output.push('\nโš ๏ธ Performance Recommendations:'); jobExecution.output.push(' 1. Consider splitting this job into smaller tasks'); jobExecution.output.push(' 2. Review if all steps are necessary'); jobExecution.output.push(' 3. Check if caching could improve performance'); } } catch (error) { jobExecution.status = 'failed'; jobExecution.endTime = new Date(); jobExecution.output.push('\nโŒ Job failed!'); jobExecution.output.push('Error details:'); if (error instanceof Error) { jobExecution.output.push(` Message: ${error.message}`); if (error.stack) { jobExecution.output.push(' Stack trace:'); jobExecution.output.push(...error.stack.split('\n').map(line => ` ${line.trim()}`)); } jobExecution.output.push('\n๐Ÿ’ก Troubleshooting Guide:'); if (error.message.includes('timeout')) { jobExecution.output.push(' Job Timeout Issues:'); jobExecution.output.push(' 1. Check if the job duration is within pipeline limits'); jobExecution.output.push(' 2. Consider optimizing long-running tasks'); jobExecution.output.push(' 3. Split the job into smaller chunks if possible'); jobExecution.output.push(' 4. Review if there are any infinite loops or hanging processes'); } else if (error.message.includes('memory')) { jobExecution.output.push(' Memory Issues:'); jobExecution.output.push(' 1. Check if the job requires more memory than allocated'); jobExecution.output.push(' 2. Look for memory leaks in your scripts'); jobExecution.output.push(' 3. Consider using smaller Docker images'); jobExecution.output.push(' 4. Add memory limits to your job configuration'); } else if (error.message.includes('permission')) { jobExecution.output.push(' Permission Issues:'); jobExecution.output.push(' 1. Verify file and directory permissions'); jobExecution.output.push(' 2. Check if the job has necessary access rights'); jobExecution.output.push(' 3. Review Docker image user configuration'); jobExecution.output.push(' 4. Ensure secrets and tokens are properly configured'); } else { jobExecution.output.push(' General Troubleshooting:'); jobExecution.output.push(' 1. Check script syntax and commands'); jobExecution.output.push(' 2. Verify all required dependencies are installed'); jobExecution.output.push(' 3. Review environment variables and configuration'); jobExecution.output.push(' 4. Test the commands locally in a similar environment'); } jobExecution.output.push('\n๐Ÿ” To Debug Locally:'); jobExecution.output.push(' 1. Copy the failing commands to a local shell'); jobExecution.output.push(' 2. Use the same Docker image locally:'); jobExecution.output.push(` docker run -it ${job.image || 'default-image'} /bin/sh`); jobExecution.output.push(' 3. Set up the same environment variables'); jobExecution.output.push(' 4. Run the commands step by step to identify the issue'); } else { jobExecution.output.push(` Unknown error type: ${error}`); jobExecution.output.push(' Please report this issue if it persists'); } } finally { this.performanceMonitor.stop(); this.onUpdate(this.execution); } } getEnvironmentVariables(job) { const envVars = { CI_JOB_NAME: job.name, CI_JOB_STAGE: job.stage, CI_PIPELINE_ID: this.execution.id || '', CI_COMMIT_SHA: 'simulation', CI_COMMIT_REF_NAME: 'simulation', }; if (job.variables) { Object.entries(job.variables).forEach(([key, value]) => { envVars[key] = typeof value === 'object' ? value.value : value; }); } return envVars; } async simulateCommandOutput(command, job) { if (command.includes('echo')) { return [command.replace('echo ', '').replace(/['"]/g, '')]; } if (command.includes('npm install') || command.includes('yarn install')) { return [ '๐Ÿ“ฆ Installing dependencies...', 'npm WARN deprecated package@1.0.0: This package is deprecated', 'added 1337 packages from 420 contributors in 12.345s', 'โœ… Dependencies installed successfully' ]; } if (command.includes('npm test') || command.includes('yarn test')) { return [ '๐Ÿงช Running tests...', 'PASS src/components/App.test.js', 'PASS src/utils/helpers.test.js', 'Test Suites: 2 passed, 2 total', 'Tests: 15 passed, 15 total', 'โœ… All tests passed' ]; } if (command.includes('npm run build') || command.includes('yarn build')) { return [ '๐Ÿ”จ Building application...', 'Creating an optimized production build...', 'Compiled successfully in 45.67s', 'File sizes after gzip:', ' 142.36 KB build/static/js/main.js', ' 1.78 KB build/static/css/main.css', 'โœ… Build completed successfully' ]; } if (command.includes('nixpacks')) { return [ '๐Ÿ“ฆ Running nixpacks build...', 'Detecting providers... Node.js', 'Setting up build environment...', 'Installing system dependencies...', 'โœ… Nixpacks build completed' ]; } if (command.includes('docker')) { return [ '๐Ÿณ Docker command executed', 'Successfully built image', 'โœ… Docker operation completed' ]; } if (command.includes('lint')) { return [ '๐Ÿ” Running linter...', 'โœ… No linting errors found' ]; } if (job.variables && Object.keys(job.variables).length > 0) { return [ 'โœ… Command executed successfully', `๐Ÿ“ Using ${Object.keys(job.variables).length} job variables` ]; } return ['โœ… Command executed successfully']; } shouldSimulateFailure(command, job) { if (command.includes('echo') || command.includes('ls') || command.includes('pwd')) { return false; } if (job.allow_failure) { return false; } return Math.random() < 0.03; } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } cancelPipeline() { this.cancelRequested = true; if (this.execution.status === 'running') { this.execution.status = 'canceled'; this.execution.jobs.forEach(job => { if (job.status === 'running' || job.status === 'pending') { const wasRunning = job.status === 'running'; job.status = 'canceled'; if (wasRunning) { job.output.push(''); job.output.push('โน๏ธ Job canceled by user'); } } }); this.execution.endTime = new Date(); this.execution.duration = this.execution.endTime.getTime() - this.execution.startTime.getTime(); this.onUpdate(this.execution); react_hot_toast_1.default.error('Pipeline execution canceled by user', { duration: 3000, icon: 'โน๏ธ', }); } } getExecution() { return this.execution; } formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; } formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } } exports.PipelineSimulator = PipelineSimulator;