UNPKG

dryrun-ci

Version:

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

422 lines (421 loc) • 17.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 (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; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileValidator = void 0; const yaml = __importStar(require("js-yaml")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const yamlParser_1 = require("./yamlParser"); const errorHelper_1 = require("./errorHelper"); class FileValidator { static validateGitLabCI(filePath) { const result = { isValid: false, issues: [], fileType: 'gitlab-ci', fileExists: false }; if (!fs.existsSync(filePath)) { result.issues.push({ file: filePath, type: 'critical', message: '.gitlab-ci.yml file not found', suggestion: 'Create a .gitlab-ci.yml file in your project root', code: 'GITLAB_CI_MISSING' }); return result; } result.fileExists = true; try { const content = fs.readFileSync(filePath, 'utf8'); result.content = content; try { yaml.load(content); } catch (yamlError) { result.issues.push({ file: filePath, line: yamlError.mark?.line + 1, column: yamlError.mark?.column + 1, type: 'critical', message: `YAML syntax error: ${yamlError.message}`, suggestion: 'Fix YAML syntax. Use proper indentation (spaces, not tabs)', code: 'YAML_SYNTAX_ERROR' }); return result; } try { const pipeline = (0, yamlParser_1.parseGitLabYaml)(content); result.isValid = true; this.checkGitLabCIIssues(pipeline, result, filePath); } catch (pipelineError) { result.issues.push({ file: filePath, type: 'critical', message: `GitLab CI validation error: ${pipelineError.message}`, suggestion: 'Check GitLab CI documentation for proper configuration', code: 'GITLAB_CI_VALIDATION_ERROR' }); } } catch (error) { result.issues.push({ file: filePath, type: 'critical', message: `Failed to read file: ${error.message}`, code: 'FILE_READ_ERROR' }); } return result; } static validateDockerfile(filePath) { const result = { isValid: false, issues: [], fileType: 'dockerfile', fileExists: false }; if (!fs.existsSync(filePath)) { result.issues.push({ file: filePath, type: 'warning', message: 'Dockerfile not found', suggestion: 'Create a Dockerfile if you need custom container images', code: 'DOCKERFILE_MISSING' }); return result; } result.fileExists = true; try { const content = fs.readFileSync(filePath, 'utf8'); result.content = content; const lines = content.split('\n'); let hasFrom = false; let hasWorkdir = false; lines.forEach((line, index) => { const trimmed = line.trim(); const lineNumber = index + 1; if (trimmed.toUpperCase().startsWith('FROM')) { hasFrom = true; if (trimmed.includes(':latest') || (!trimmed.includes(':') && !trimmed.includes('@'))) { result.issues.push({ file: filePath, line: lineNumber, type: 'warning', message: 'Using latest tag or no tag specified', suggestion: 'Use specific version tags for reproducible builds', code: 'DOCKERFILE_LATEST_TAG' }); } } if (trimmed.toUpperCase().startsWith('WORKDIR')) { hasWorkdir = true; } if (trimmed.toUpperCase().includes('USER ROOT') || trimmed.toUpperCase().includes('USER 0')) { result.issues.push({ file: filePath, line: lineNumber, type: 'warning', message: 'Running as root user', suggestion: 'Create and use a non-root user for security', code: 'DOCKERFILE_ROOT_USER' }); } if (trimmed.includes('apt-get') && !trimmed.includes('--no-cache') && !trimmed.includes('rm -rf /var/lib/apt/lists/*')) { result.issues.push({ file: filePath, line: lineNumber, type: 'info', message: 'Package manager cache not cleaned', suggestion: 'Clean package manager cache to reduce image size', code: 'DOCKERFILE_CACHE_CLEANUP' }); } }); if (!hasFrom) { result.issues.push({ file: filePath, type: 'critical', message: 'Dockerfile must start with FROM instruction', suggestion: 'Add FROM instruction to specify base image', code: 'DOCKERFILE_MISSING_FROM' }); } else { result.isValid = true; } if (!hasWorkdir) { result.issues.push({ file: filePath, type: 'info', message: 'No WORKDIR instruction found', suggestion: 'Consider setting WORKDIR for better organization', code: 'DOCKERFILE_NO_WORKDIR' }); } } catch (error) { result.issues.push({ file: filePath, type: 'critical', message: `Failed to read Dockerfile: ${error.message}`, code: 'FILE_READ_ERROR' }); } return result; } static validateNixpacks(filePath) { const result = { isValid: false, issues: [], fileType: 'nixpacks', fileExists: false }; if (!fs.existsSync(filePath)) { result.issues.push({ file: filePath, type: 'info', message: 'nixpacks.toml not found', suggestion: 'Create nixpacks.toml for Nixpacks-based builds', code: 'NIXPACKS_MISSING' }); return result; } result.fileExists = true; try { const content = fs.readFileSync(filePath, 'utf8'); result.content = content; try { const lines = content.split('\n'); let hasProvider = false; let hasStart = false; lines.forEach((line, index) => { const trimmed = line.trim(); const lineNumber = index + 1; if (trimmed.startsWith('#') || trimmed === '') return; if (trimmed.includes('provider')) { hasProvider = true; } if (trimmed.includes('start')) { hasStart = true; } if (trimmed.includes('=') && !trimmed.match(/^\w+\s*=\s*["']?[\w\s\-./]*["']?$/)) { if (trimmed.includes('=') && !(trimmed.includes('"') || trimmed.includes("'"))) { result.issues.push({ file: filePath, line: lineNumber, type: 'warning', message: 'String values should be quoted', suggestion: 'Wrap string values in quotes', code: 'NIXPACKS_UNQUOTED_STRING' }); } } }); if (!hasProvider) { result.issues.push({ file: filePath, type: 'info', message: 'No provider specified', suggestion: 'Consider specifying a provider for better control', code: 'NIXPACKS_NO_PROVIDER' }); } if (!hasStart) { result.issues.push({ file: filePath, type: 'info', message: 'No start command specified', suggestion: 'Consider specifying a start command', code: 'NIXPACKS_NO_START' }); } result.isValid = true; } catch (tomlError) { result.issues.push({ file: filePath, type: 'critical', message: `TOML syntax error: ${tomlError.message}`, suggestion: 'Fix TOML syntax', code: 'TOML_SYNTAX_ERROR' }); } } catch (error) { result.issues.push({ file: filePath, type: 'critical', message: `Failed to read nixpacks.toml: ${error.message}`, code: 'FILE_READ_ERROR' }); } return result; } static validateProject(projectPath) { const gitlabCiPath = path.join(projectPath, '.gitlab-ci.yml'); const dockerfilePath = path.join(projectPath, 'Dockerfile'); const nixpacksPath = path.join(projectPath, 'nixpacks.toml'); const gitlabCi = this.validateGitLabCI(gitlabCiPath); const dockerfile = this.validateDockerfile(dockerfilePath); const nixpacks = this.validateNixpacks(nixpacksPath); const buildSystems = []; if (dockerfile.fileExists) buildSystems.push('dockerfile'); if (nixpacks.fileExists) buildSystems.push('nixpacks'); return { gitlabCi, dockerfile, nixpacks, buildSystems }; } static checkGitLabCIIssues(pipeline, result, filePath) { if (Object.keys(pipeline.jobs).length === 0) { result.issues.push({ file: filePath, type: 'critical', message: 'No jobs defined in pipeline', suggestion: 'Add at least one job to your pipeline', code: 'GITLAB_CI_NO_JOBS' }); } Object.entries(pipeline.jobs).forEach(([jobName, job]) => { if (!job.script && !job.trigger && !job.extends) { result.issues.push({ file: filePath, type: 'warning', message: `Job '${jobName}' has no script, trigger, or extends`, suggestion: 'Add script commands or use extends/trigger', code: 'GITLAB_CI_JOB_NO_SCRIPT' }); } if (job.privileged) { result.issues.push({ file: filePath, type: 'warning', message: `Job '${jobName}' runs in privileged mode`, suggestion: 'Avoid privileged mode unless absolutely necessary', code: 'GITLAB_CI_PRIVILEGED_MODE' }); } if (job.image && (job.image.includes(':latest') || !job.image.includes(':'))) { result.issues.push({ file: filePath, type: 'warning', message: `Job '${jobName}' uses latest tag or no tag`, suggestion: 'Use specific version tags for reproducible builds', code: 'GITLAB_CI_LATEST_TAG' }); } if (job.variables) { Object.entries(job.variables).forEach(([varName, varValue]) => { if (typeof varValue === 'string' && (varName.toLowerCase().includes('password') || varName.toLowerCase().includes('secret') || varName.toLowerCase().includes('token') || varName.toLowerCase().includes('key'))) { result.issues.push({ file: filePath, type: 'warning', message: `Job '${jobName}' may contain secrets in variables`, suggestion: 'Use GitLab CI variables with protected/masked flags', code: 'GITLAB_CI_POTENTIAL_SECRET' }); } }); } }); if (!pipeline.stages || pipeline.stages.length === 0) { result.issues.push({ file: filePath, type: 'info', message: 'No stages defined, using default stages', suggestion: 'Define explicit stages for better organization', code: 'GITLAB_CI_NO_STAGES' }); } const usedStages = new Set(Object.values(pipeline.jobs).map((job) => job.stage)); const definedStages = new Set(pipeline.stages || []); definedStages.forEach(stage => { if (!usedStages.has(stage)) { result.issues.push({ file: filePath, type: 'info', message: `Stage '${stage}' is defined but not used`, suggestion: 'Remove unused stages or add jobs to them', code: 'GITLAB_CI_UNUSED_STAGE' }); } }); } static formatValidationResults(results) { let output = ''; results.forEach(result => { if (result.issues.length > 0) { const fileIssues = {}; result.issues.forEach(issue => { if (!fileIssues[issue.file]) { fileIssues[issue.file] = []; } fileIssues[issue.file].push(issue); }); Object.entries(fileIssues).forEach(([file, issues]) => { output += `\nšŸ“„ ${file}:\n`; issues.forEach(issue => { if (issue.type === 'critical') { output += errorHelper_1.ErrorHelper.formatError(issue.message, { file: issue.file, line: issue.line, column: issue.column, suggestion: issue.suggestion }); } else if (issue.type === 'warning') { output += errorHelper_1.ErrorHelper.formatWarning(issue.message, { file: issue.file, line: issue.line, column: issue.column, suggestion: issue.suggestion }); } else { output += errorHelper_1.ErrorHelper.formatInfo(issue.message, { file: issue.file, line: issue.line, column: issue.column, suggestion: issue.suggestion }); } }); }); } }); return output; } } exports.FileValidator = FileValidator;