dryrun-ci
Version:
DryRun CI - Local GitLab CI/CD pipeline testing tool with Docker execution, performance monitoring, and security sandboxing
465 lines (464 loc) ⢠18.9 kB
JavaScript
"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 {
/**
* Validate GitLab CI YAML file
*/
static validateGitLabCI(filePath) {
const result = {
isValid: false,
issues: [],
fileType: 'gitlab-ci',
fileExists: false
};
// Check if file exists
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;
// Basic YAML syntax validation
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;
}
// GitLab CI specific validation
try {
const pipeline = (0, yamlParser_1.parseGitLabYaml)(content);
result.isValid = true;
// Check for common issues
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;
}
/**
* Validate Dockerfile
*/
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;
// Check for FROM instruction
if (trimmed.toUpperCase().startsWith('FROM')) {
hasFrom = true;
// Check for latest tag
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'
});
}
}
// Check for WORKDIR
if (trimmed.toUpperCase().startsWith('WORKDIR')) {
hasWorkdir = true;
}
// Check for security issues
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'
});
}
// Check for package manager cache
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'
});
}
});
// Check required instructions
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;
}
/**
* Validate nixpacks.toml file
*/
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;
// Basic TOML syntax validation
try {
// Simple TOML validation - check for basic syntax issues
const lines = content.split('\n');
let hasProvider = false;
let hasStart = false;
lines.forEach((line, index) => {
const trimmed = line.trim();
const lineNumber = index + 1;
// Skip comments and empty lines
if (trimmed.startsWith('#') || trimmed === '')
return;
// Check for provider
if (trimmed.includes('provider')) {
hasProvider = true;
}
// Check for start command
if (trimmed.includes('start')) {
hasStart = true;
}
// Check for common syntax issues
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;
}
/**
* Validate multiple files in a project
*/
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);
// Determine available build systems
const buildSystems = [];
if (dockerfile.fileExists)
buildSystems.push('dockerfile');
if (nixpacks.fileExists)
buildSystems.push('nixpacks');
return {
gitlabCi,
dockerfile,
nixpacks,
buildSystems
};
}
/**
* Check GitLab CI specific issues
*/
static checkGitLabCIIssues(pipeline, result, filePath) {
// Check for empty jobs
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'
});
}
// Check for jobs without scripts
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'
});
}
// Check for security issues
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'
});
}
// Check for latest image tags
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'
});
}
// Check for secrets in variables
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'
});
}
});
}
});
// Check for missing stages
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'
});
}
// Check for unused 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'
});
}
});
}
/**
* Format validation results for terminal output
*/
static formatValidationResults(results) {
let output = '';
results.forEach(result => {
if (result.issues.length > 0) {
// Group issues by file
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;