UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

926 lines (922 loc) 35.5 kB
"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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.CICDPipelineTesting = void 0; exports.createTestSuite = createTestSuite; exports.createCICDJob = createCICDJob; exports.createJobStep = createJobStep; exports.testCICDPipelines = testCICDPipelines; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const yaml = __importStar(require("js-yaml")); class CICDPipelineTesting extends events_1.EventEmitter { constructor(config) { super(); this.results = []; this.generatedConfigs = []; this.config = { parallel: true, maxConcurrency: 4, generateConfigs: true, validateConfigs: true, simulateRuns: false, generateReport: true, outputPath: './cicd-test-output', ...config }; } async run() { this.emit('cicd:start', { providers: this.config.providers.length, suites: this.config.testSuites.length }); const startTime = Date.now(); try { await this.setup(); if (this.config.generateConfigs) { await this.generateConfigurations(); } if (this.config.validateConfigs) { await this.validateConfigurations(); } if (this.config.simulateRuns) { await this.simulateRuns(); } const report = this.generateReport(Date.now() - startTime); if (this.config.generateReport) { await this.saveReport(report); } this.emit('cicd:complete', report); return report; } catch (error) { this.emit('cicd:error', error); throw error; } } async setup() { await fs.ensureDir(this.config.outputPath); this.emit('setup:complete'); } async generateConfigurations() { this.emit('configs:start'); const enabledProviders = this.config.providers.filter(p => p.enabled !== false); for (const provider of enabledProviders) { for (const suite of this.config.testSuites) { const config = await this.generateProviderConfig(provider, suite); this.generatedConfigs.push(config); } } this.emit('configs:complete', { count: this.generatedConfigs.length }); } async generateProviderConfig(provider, suite) { this.emit('config:generate', { provider: provider.name, suite: suite.name }); const config = { provider: provider.name, path: this.getConfigPath(provider.name, suite.name), content: '', valid: false, errors: [], warnings: [] }; try { switch (provider.name) { case 'github': config.content = this.generateGitHubConfig(provider, suite); break; case 'gitlab': config.content = this.generateGitLabConfig(provider, suite); break; case 'jenkins': config.content = this.generateJenkinsConfig(provider, suite); break; case 'circleci': config.content = this.generateCircleCIConfig(provider, suite); break; case 'azure': config.content = this.generateAzureConfig(provider, suite); break; case 'bitbucket': config.content = this.generateBitbucketConfig(provider, suite); break; default: config.content = this.generateCustomConfig(provider, suite); } // Save config to file const fullPath = path.join(this.config.outputPath, config.path); await fs.ensureDir(path.dirname(fullPath)); await fs.writeFile(fullPath, config.content); config.valid = true; this.emit('config:generated', config); } catch (error) { config.errors.push(error.message); this.emit('config:error', { config, error }); } return config; } generateGitHubConfig(provider, suite) { const workflow = { name: suite.name, on: provider.config.triggers || ['push', 'pull_request'], env: provider.config.environment || {}, jobs: {} }; for (const job of suite.jobs) { const ghJob = { 'runs-on': 'ubuntu-latest', timeout: job.timeout || provider.config.timeout || 30, steps: [] }; // Add matrix if specified if (job.matrix || provider.matrix) { const matrix = { ...provider.matrix, ...job.matrix }; ghJob.strategy = { matrix: this.buildMatrix(matrix) }; } // Add services if specified if (job.services) { ghJob.services = this.buildGitHubServices(job.services); } // Add steps for (const step of job.steps) { const ghStep = this.buildGitHubStep(step); ghJob.steps.push(ghStep); } workflow.jobs[job.name] = ghJob; } return yaml.dump(workflow, { indent: 2 }); } generateGitLabConfig(provider, suite) { const pipeline = { stages: suite.jobs.map(job => job.name), variables: provider.config.environment || {} }; // Add cache configuration if (provider.config.caching?.enabled) { pipeline.cache = { paths: provider.config.caching.paths || ['node_modules/'], key: provider.config.caching.key || 'npm-cache' }; } for (const job of suite.jobs) { const glJob = { stage: job.name, timeout: job.timeout || provider.config.timeout || '30m', script: [] }; // Add matrix as parallel jobs if (job.matrix || provider.matrix) { const matrix = { ...provider.matrix, ...job.matrix }; glJob.parallel = { matrix: this.buildGitLabMatrix(matrix) }; } // Add services if (job.services) { glJob.services = job.services.map(s => s.image); } // Add job steps as script for (const step of job.steps) { if (step.command) { glJob.script.push(step.command); } else if (step.script) { glJob.script.push(...step.script); } } // Add artifacts if (provider.config.artifacts) { glJob.artifacts = { paths: provider.config.artifacts.map(a => a.path), expire_in: '1 week' }; } pipeline[job.name] = glJob; } return yaml.dump(pipeline, { indent: 2 }); } generateJenkinsConfig(provider, suite) { const pipeline = { pipeline: { agent: 'any', environment: provider.config.environment || {}, stages: suite.jobs.map(job => ({ stage: job.name, steps: job.steps.map(step => ({ script: step.command || step.script?.join('\n') || '' })) })) } }; return `pipeline { agent any environment { ${Object.entries(provider.config.environment || {}) .map(([key, value]) => ` ${key} = '${value}'`) .join('\n')} } stages { ${suite.jobs.map(job => ` stage('${job.name}') { steps { ${job.steps.map(step => ` script { ${step.command || step.script?.join('\n ') || ''} }`).join('\n')} } }`).join('\n')} } }`; } generateCircleCIConfig(provider, suite) { const config = { version: '2.1', jobs: {}, workflows: { [suite.name]: { jobs: suite.jobs.map(job => job.name) } } }; for (const job of suite.jobs) { const ccJob = { docker: [{ image: 'cimg/node:18.17' }], steps: [] }; // Add matrix as parameters if (job.matrix || provider.matrix) { const matrix = { ...provider.matrix, ...job.matrix }; ccJob.parameters = this.buildCircleCIParameters(matrix); } for (const step of job.steps) { if (step.command) { ccJob.steps.push({ run: step.command }); } else if (step.script) { ccJob.steps.push({ run: { command: step.script.join('\n') } }); } } config.jobs[job.name] = ccJob; } return yaml.dump(config, { indent: 2 }); } generateAzureConfig(provider, suite) { const pipeline = { trigger: provider.config.triggers || ['main'], pool: { vmImage: 'ubuntu-latest' }, variables: provider.config.environment || {}, stages: [] }; const stage = { stage: suite.name, jobs: [] }; for (const job of suite.jobs) { const azJob = { job: job.name, timeoutInMinutes: Math.ceil((job.timeout || 30000) / 60000), steps: [] }; // Add matrix if (job.matrix || provider.matrix) { const matrix = { ...provider.matrix, ...job.matrix }; azJob.strategy = { matrix: this.buildAzureMatrix(matrix) }; } for (const step of job.steps) { if (step.command) { azJob.steps.push({ script: step.command, displayName: step.name }); } else if (step.script) { azJob.steps.push({ script: step.script.join('\n'), displayName: step.name }); } } stage.jobs.push(azJob); } pipeline.stages.push(stage); return yaml.dump(pipeline, { indent: 2 }); } generateBitbucketConfig(provider, suite) { const pipeline = { pipelines: { default: suite.jobs.map(job => ({ step: { name: job.name, image: 'node:18', caches: provider.config.caching?.enabled ? ['node'] : undefined, script: job.steps .map(step => step.command || step.script?.join('\n')) .filter(Boolean) } })) } }; return yaml.dump(pipeline, { indent: 2 }); } generateCustomConfig(provider, suite) { return `# Custom CI/CD configuration for ${provider.name} # Suite: ${suite.name} # Jobs: ${suite.jobs.map(j => j.name).join(', ')} # This is a placeholder for custom provider configurations # Implement specific logic based on your custom CI/CD system `; } buildMatrix(matrix) { const result = {}; if (matrix.os) result.os = matrix.os; if (matrix.nodeVersion) result['node-version'] = matrix.nodeVersion; if (matrix.packageManager) result['package-manager'] = matrix.packageManager; if (matrix.variables) { Object.assign(result, matrix.variables); } return result; } buildGitLabMatrix(matrix) { const combinations = []; // Simple matrix generation for GitLab const os = matrix.os || ['ubuntu-latest']; const nodeVersions = matrix.nodeVersion || ['18']; for (const osValue of os) { for (const nodeValue of nodeVersions) { combinations.push({ OS: osValue, NODE_VERSION: nodeValue }); } } return combinations; } buildCircleCIParameters(matrix) { const params = {}; if (matrix.nodeVersion) { params.node_version = { type: 'string', default: matrix.nodeVersion[0] }; } return params; } buildAzureMatrix(matrix) { const result = {}; if (matrix.os) { result.vmImage = matrix.os; } if (matrix.nodeVersion) { result.node_version = matrix.nodeVersion; } return result; } buildGitHubServices(services) { const result = {}; for (const service of services) { result[service.name] = { image: service.image, ports: service.ports, env: service.env }; } return result; } buildGitHubStep(step) { const ghStep = { name: step.name }; if (step.uses) { ghStep.uses = step.uses; if (step.with) { ghStep.with = step.with; } } else if (step.command) { ghStep.run = step.command; } else if (step.script) { ghStep.run = step.script.join('\n'); } if (step.env) { ghStep.env = step.env; } if (step.condition) { ghStep.if = step.condition; } if (step.continueOnError) { ghStep['continue-on-error'] = true; } return ghStep; } getConfigPath(provider, suite) { const filename = `${suite}-${provider}`; switch (provider) { case 'github': return `.github/workflows/${filename}.yml`; case 'gitlab': return `.gitlab-ci-${filename}.yml`; case 'jenkins': return `Jenkinsfile-${filename}`; case 'circleci': return `.circleci/config-${filename}.yml`; case 'azure': return `azure-pipelines-${filename}.yml`; case 'bitbucket': return `bitbucket-pipelines-${filename}.yml`; default: return `${provider}-${filename}.yml`; } } async validateConfigurations() { this.emit('validation:start'); for (const config of this.generatedConfigs) { await this.validateConfig(config); } this.emit('validation:complete'); } async validateConfig(config) { try { switch (config.provider) { case 'github': case 'gitlab': case 'circleci': case 'azure': case 'bitbucket': // Validate YAML syntax yaml.load(config.content); break; case 'jenkins': // Basic Jenkinsfile validation if (!config.content.includes('pipeline {')) { config.warnings.push('Jenkinsfile should start with pipeline block'); } break; } // Additional validation rules this.validateCommonPatterns(config); } catch (error) { config.valid = false; config.errors.push(`Validation failed: ${error.message}`); } } validateCommonPatterns(config) { const content = config.content; // Check for security issues if (content.includes('password') || content.includes('secret')) { config.warnings.push('Potential hardcoded secrets detected'); } // Check for performance issues if (!content.includes('cache') && !content.includes('Cache')) { config.warnings.push('No caching configuration found - consider adding caching for better performance'); } // Check for timeout configuration if (!content.includes('timeout')) { config.warnings.push('No timeout configuration - jobs may hang indefinitely'); } } async simulateRuns() { this.emit('simulation:start'); for (const provider of this.config.providers.filter(p => p.enabled !== false)) { for (const suite of this.config.testSuites) { for (const job of suite.jobs) { const result = await this.simulateJob(provider, suite, job); this.results.push(result); } } } this.emit('simulation:complete'); } async simulateJob(provider, suite, job) { this.emit('job:simulate', { provider: provider.name, suite: suite.name, job: job.name }); const result = { provider: provider.name, suite: suite.name, job: job.name, success: true, duration: 0, steps: [], artifacts: [], errors: [], warnings: [], timestamp: new Date() }; const startTime = Date.now(); try { for (const step of job.steps) { const stepResult = await this.simulateStep(step, provider); result.steps.push(stepResult); if (!stepResult.success && !step.continueOnError) { result.success = false; break; } } result.duration = Date.now() - startTime; } catch (error) { result.success = false; result.errors.push({ type: 'execution', message: error.message }); result.duration = Date.now() - startTime; } return result; } async simulateStep(step, provider) { // Simulate step execution with random delays and occasional failures const duration = Math.random() * 5000 + 1000; // 1-6 seconds await new Promise(resolve => setTimeout(resolve, duration)); const success = Math.random() > 0.1; // 90% success rate return { name: step.name, success, duration, output: success ? 'Step completed successfully' : undefined, error: success ? undefined : 'Simulated step failure', exitCode: success ? 0 : 1 }; } generateReport(duration) { const summary = this.generateSummary(duration); const analysis = this.analyzeResults(); const recommendations = this.generateRecommendations(analysis); const bestPractices = this.generateBestPractices(); return { summary, results: this.results, configs: this.generatedConfigs, analysis, recommendations, bestPractices, timestamp: new Date() }; } generateSummary(duration) { const totalSteps = this.results.reduce((sum, r) => sum + r.steps.length, 0); const passedSteps = this.results.reduce((sum, r) => sum + r.steps.filter(s => s.success).length, 0); return { totalProviders: this.config.providers.length, totalSuites: this.config.testSuites.length, totalJobs: this.results.length, totalSteps, passed: this.results.filter(r => r.success).length, failed: this.results.filter(r => !r.success).length, warnings: this.generatedConfigs.reduce((sum, c) => sum + c.warnings.length, 0), duration: duration / 1000, coverage: totalSteps > 0 ? (passedSteps / totalSteps) * 100 : 0 }; } analyzeResults() { const compatibility = this.analyzeCompatibility(); const performance = this.analyzePerformance(); const reliability = this.analyzeReliability(); const security = this.analyzeSecurity(); const optimization = this.generateOptimizations(); return { compatibility, performance, reliability, security, optimization }; } analyzeCompatibility() { const providers = this.config.providers.map(p => p.name); const features = ['caching', 'matrix', 'services', 'artifacts', 'parallel']; const support = []; const gaps = []; for (const feature of features) { const row = []; const unsupportedProviders = []; for (const provider of providers) { const supported = this.checkFeatureSupport(provider, feature); row.push(supported); if (!supported) { unsupportedProviders.push(provider); } } support.push(row); if (unsupportedProviders.length > 0) { gaps.push({ feature, providers: unsupportedProviders, impact: unsupportedProviders.length > providers.length / 2 ? 'high' : 'medium' }); } } return { providers, features, support, gaps }; } checkFeatureSupport(provider, feature) { // Simplified feature support matrix const supportMatrix = { github: ['caching', 'matrix', 'services', 'artifacts', 'parallel'], gitlab: ['caching', 'matrix', 'services', 'artifacts', 'parallel'], jenkins: ['caching', 'parallel'], circleci: ['caching', 'matrix', 'services', 'artifacts', 'parallel'], azure: ['caching', 'matrix', 'artifacts', 'parallel'], bitbucket: ['caching', 'parallel'] }; return supportMatrix[provider]?.includes(feature) ?? false; } analyzePerformance() { const jobDurations = this.results.map(r => r.duration); const averageJobDuration = jobDurations.length > 0 ? jobDurations.reduce((a, b) => a + b, 0) / jobDurations.length : 0; const slowestJobs = this.results .sort((a, b) => b.duration - a.duration) .slice(0, 5) .map(r => ({ job: `${r.suite}/${r.job}`, duration: r.duration })); return { averageJobDuration, slowestJobs, parallelizationOpportunities: this.identifyParallelizationOpportunities(), cacheEffectiveness: this.calculateCacheEffectiveness() }; } identifyParallelizationOpportunities() { const opportunities = []; // Check for sequential jobs that could be parallel for (const suite of this.config.testSuites) { const independentJobs = suite.jobs.filter(job => !job.dependencies?.length); if (independentJobs.length > 1) { opportunities.push(`Suite "${suite.name}" has ${independentJobs.length} independent jobs that could run in parallel`); } } return opportunities; } calculateCacheEffectiveness() { const cachingConfigs = this.config.providers.filter(p => p.config.caching?.enabled); return (cachingConfigs.length / this.config.providers.length) * 100; } analyzeReliability() { const total = this.results.length; const successful = this.results.filter(r => r.success).length; const failed = total - successful; const timeouts = this.results.filter(r => r.errors.some(e => e.type === 'timeout')).length; return { successRate: total > 0 ? (successful / total) * 100 : 0, errorRate: total > 0 ? (failed / total) * 100 : 0, timeoutRate: total > 0 ? (timeouts / total) * 100 : 0, retryRate: 0, // Would be calculated from actual retry data flakyTests: [] // Would be identified from multiple runs }; } analyzeSecurity() { const secretsExposed = []; const permissionsIssues = []; const vulnerabilities = []; const recommendations = []; // Analyze configurations for security issues for (const config of this.generatedConfigs) { if (config.content.includes('password') || config.content.includes('token')) { secretsExposed.push(`${config.provider}: Potential secrets in configuration`); } if (config.content.includes('chmod 777') || config.content.includes('sudo')) { permissionsIssues.push(`${config.provider}: Overly permissive operations detected`); } } if (secretsExposed.length > 0) { recommendations.push('Use secure secret management instead of hardcoded values'); } if (permissionsIssues.length > 0) { recommendations.push('Review and minimize required permissions'); } return { secretsExposed, permissionsIssues, vulnerabilities, recommendations }; } generateOptimizations() { const optimizations = []; // Cache optimization const noCacheProviders = this.config.providers.filter(p => !p.config.caching?.enabled); if (noCacheProviders.length > 0) { optimizations.push({ type: 'cache', description: 'Enable dependency caching to speed up builds', impact: 'high', implementation: 'Add cache configuration to CI/CD pipelines', estimatedSavings: '30-50% build time reduction' }); } // Parallel optimization const sequentialSuites = this.config.testSuites.filter(s => s.jobs.length > 1 && !s.jobs.some(j => j.matrix)); if (sequentialSuites.length > 0) { optimizations.push({ type: 'parallel', description: 'Run independent jobs in parallel', impact: 'medium', implementation: 'Configure job dependencies and parallel execution', estimatedSavings: '20-40% total execution time' }); } // Matrix optimization const noMatrixJobs = this.config.testSuites .flatMap(s => s.jobs) .filter(j => !j.matrix); if (noMatrixJobs.length > 0) { optimizations.push({ type: 'matrix', description: 'Use build matrices for multi-environment testing', impact: 'medium', implementation: 'Configure test matrices for OS, Node.js versions, etc.', estimatedSavings: 'Better test coverage with organized execution' }); } return optimizations; } generateRecommendations(analysis) { const recommendations = []; if (analysis.reliability.successRate < 90) { recommendations.push(`Improve pipeline reliability - current success rate: ${analysis.reliability.successRate.toFixed(1)}%`); } if (analysis.performance.cacheEffectiveness < 50) { recommendations.push('Enable caching on more providers to improve build performance'); } if (analysis.compatibility.gaps.length > 0) { const highImpactGaps = analysis.compatibility.gaps.filter(g => g.impact === 'high'); if (highImpactGaps.length > 0) { recommendations.push(`Address high-impact compatibility gaps: ${highImpactGaps.map(g => g.feature).join(', ')}`); } } recommendations.push(...analysis.security.recommendations); return recommendations; } generateBestPractices() { return [ { category: 'performance', title: 'Use Dependency Caching', description: 'Cache dependencies between builds to reduce installation time', implementation: 'Configure cache paths and keys in your CI/CD pipeline', providers: ['github', 'gitlab', 'circleci', 'azure'] }, { category: 'reliability', title: 'Set Appropriate Timeouts', description: 'Prevent jobs from hanging indefinitely', implementation: 'Set reasonable timeout values for jobs and steps', providers: ['github', 'gitlab', 'jenkins', 'circleci', 'azure', 'bitbucket'] }, { category: 'security', title: 'Use Secret Management', description: 'Store sensitive data in secure secret stores', implementation: 'Use provider-specific secret management features', providers: ['github', 'gitlab', 'jenkins', 'circleci', 'azure', 'bitbucket'] }, { category: 'maintainability', title: 'Use Build Matrices', description: 'Test across multiple environments systematically', implementation: 'Configure test matrices for different OS, runtime versions', providers: ['github', 'gitlab', 'circleci', 'azure'] } ]; } async saveReport(report) { const reportPath = path.join(this.config.outputPath, 'cicd-test-report.json'); await fs.writeJson(reportPath, report, { spaces: 2 }); const summaryPath = path.join(this.config.outputPath, 'cicd-test-summary.md'); await fs.writeFile(summaryPath, this.formatMarkdownReport(report)); this.emit('report:saved', { json: reportPath, markdown: summaryPath }); } formatMarkdownReport(report) { const lines = [ '# CI/CD Pipeline Testing Report', '', `**Date:** ${report.timestamp.toISOString()}`, `**Duration:** ${report.summary.duration.toFixed(2)}s`, '', '## Summary', '', `- **Providers:** ${report.summary.totalProviders}`, `- **Test Suites:** ${report.summary.totalSuites}`, `- **Jobs:** ${report.summary.totalJobs}`, `- **Steps:** ${report.summary.totalSteps}`, `- **Success Rate:** ${((report.summary.passed / report.summary.totalJobs) * 100).toFixed(1)}%`, `- **Coverage:** ${report.summary.coverage.toFixed(1)}%`, '', '## Generated Configurations', '' ]; const configsByProvider = new Map(); for (const config of report.configs) { if (!configsByProvider.has(config.provider)) { configsByProvider.set(config.provider, []); } configsByProvider.get(config.provider).push(config); } for (const [provider, configs] of configsByProvider) { lines.push(`### ${provider.charAt(0).toUpperCase() + provider.slice(1)}`); lines.push(''); for (const config of configs) { const status = config.valid ? '✅' : '❌'; lines.push(`- ${status} \`${config.path}\``); if (config.errors.length > 0) { config.errors.forEach(error => { lines.push(` - ❌ ${error}`); }); } if (config.warnings.length > 0) { config.warnings.forEach(warning => { lines.push(` - ⚠️ ${warning}`); }); } } lines.push(''); } lines.push('## Compatibility Analysis', ''); const matrix = report.analysis.compatibility; lines.push('| Feature | ' + matrix.providers.join(' | ') + ' |'); lines.push('|---------|' + matrix.providers.map(() => '---').join('|') + '|'); for (let i = 0; i < matrix.features.length; i++) { const feature = matrix.features[i]; const support = matrix.support[i].map(s => s ? '✅' : '❌').join(' | '); lines.push(`| ${feature} | ${support} |`); } lines.push('', '## Recommendations', ''); report.recommendations.forEach(rec => { lines.push(`- ${rec}`); }); if (report.analysis.optimization.length > 0) { lines.push('', '## Optimization Opportunities', ''); report.analysis.optimization.forEach(opt => { lines.push(`### ${opt.type.charAt(0).toUpperCase() + opt.type.slice(1)}`); lines.push(`- **Impact:** ${opt.impact}`); lines.push(`- **Description:** ${opt.description}`); lines.push(`- **Implementation:** ${opt.implementation}`); if (opt.estimatedSavings) { lines.push(`- **Estimated Savings:** ${opt.estimatedSavings}`); } lines.push(''); }); } return lines.join('\n'); } } exports.CICDPipelineTesting = CICDPipelineTesting; // Export utility functions function createTestSuite(name, jobs, options) { return { name, jobs, ...options }; } function createCICDJob(name, steps, options) { return { name, steps, ...options }; } function createJobStep(name, type, options) { return { name, type, ...options }; } async function testCICDPipelines(config) { const tester = new CICDPipelineTesting(config); return tester.run(); }