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
JavaScript
;
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;