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