Files
moko-platform/lib/Enterprise/ProjectConfigValidator.php
T
Jonathan Miller 07ea171af9
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 43s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
feat: release promotion pipeline, 5 new CLI tools, workflow refactoring
New CLI tools:
- manifest_element.php — extract element/type/prefix from any platform manifest
- release_create.php — create/overwrite Gitea releases with proper naming
- release_package.php — build ZIP+tar.gz, SHA-256, upload assets
- release_promote.php — promote releases between channels (dev→RC→stable)
- version_reset_dev.php — reset platform version on dev branch after release

Updated CLI tools:
- version_bump.php — now writes to manifests, Dolibarr mod, composer.json (not just README)
- release_cascade.php — added --version for version-aware deletion of stale releases
- release_validate.php — auto-detect platform, --github-output, source dir check

Workflow changes (auto-release.yml):
- Draft PR to main → auto-promote highest pre-release to RC
- Merged PR to main → promote RC to stable (skip rebuild when RC exists)
- Removed paths filter for Go/Node/generic repo compatibility
- Fixed cascade --api-base parameter bug

Workflow changes (pre-release.yml):
- Auto-trigger development pre-release on feature branch merge to dev
- Removed paths filter

Infrastructure:
- RepositorySynchronizer: fixed template repo names, .mokogitea/workflows path,
  universal workflow cascade (Template-Generic → other templates)
- bulk_sync.php: syncs universal workflows to templates before repo sync
- PHPDoc added to 4 classes missing class-level docs
- Version bump 09.00.00 → 09.01.00

Closes #152 #153 #154 #155 #156 #157 #158 #159 #161 #162

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 14:29:32 -05:00

358 lines
11 KiB
PHP

<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Enterprise.ProjectTypes
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/ProjectConfigValidator.php
* BRIEF: Enterprise library for validating project configurations
*/
declare(strict_types=1);
namespace MokoEnterprise;
/**
* Project Config Validator
*
* Enterprise library for validating project configurations against
* project type templates and standards.
*
* @since 04.00.00
*/
class ProjectConfigValidator
{
private AuditLogger $logger;
private MetricsCollector $metrics;
private ProjectTypeDetector $detector;
private array $validationResults = [];
private int $errorsCount = 0;
private int $warningsCount = 0;
private const VALIDATION_RULES = [
'nodejs' => [
'required_files' => ['package.json'],
'recommended_files' => ['README.md', '.gitignore', 'tsconfig.json'],
'required_fields' => ['name', 'version', 'description'],
],
'python' => [
'required_files' => ['setup.py|pyproject.toml'],
'recommended_files' => ['README.md', 'requirements.txt', '.gitignore'],
'required_fields' => ['name', 'version'],
],
'terraform' => [
'required_files' => ['*.tf'],
'recommended_files' => ['README.md', 'variables.tf', 'outputs.tf'],
'required_fields' => [],
],
'wordpress' => [
'required_files' => ['*.php'],
'recommended_files' => ['README.md', 'readme.txt'],
'required_fields' => ['Plugin Name|Theme Name', 'Version'],
],
'mobile' => [
'required_files' => ['package.json|pubspec.yaml'],
'recommended_files' => ['README.md', '.gitignore'],
'required_fields' => ['name', 'version'],
],
'api' => [
'required_files' => [],
'recommended_files' => ['README.md', 'openapi.yaml|swagger.yaml', 'Dockerfile'],
'required_fields' => [],
],
];
/**
* Constructor
*/
public function __construct(
?AuditLogger $logger = null,
?MetricsCollector $metrics = null,
?ProjectTypeDetector $detector = null
) {
$this->logger = $logger ?? new AuditLogger('project_config_validator');
$this->metrics = $metrics ?? new MetricsCollector();
$this->detector = $detector ?? new ProjectTypeDetector($this->logger, $this->metrics);
}
/**
* Validate project configuration
*
* @param string $repoPath Path to repository
* @param string|null $projectType Optional project type (auto-detect if null)
* @return array Validation results
*/
public function validate(string $repoPath, ?string $projectType = null): array
{
$this->logger->logInfo("Validating project configuration: {$repoPath}");
$this->resetResults();
// Detect project type if not provided
if ($projectType === null) {
$detection = $this->detector->detect($repoPath);
$projectType = $detection['type'];
$this->logger->logInfo("Auto-detected project type: {$projectType}");
}
// Get validation rules for project type
$rules = self::VALIDATION_RULES[$projectType] ?? [];
if (empty($rules)) {
$this->addWarning('No validation rules for project type: ' . $projectType);
return $this->getResults();
}
// Run validations
$this->validateRequiredFiles($repoPath, $rules['required_files'] ?? []);
$this->validateRecommendedFiles($repoPath, $rules['recommended_files'] ?? []);
$this->validateProjectFields($repoPath, $projectType, $rules['required_fields'] ?? []);
// Record metrics
$this->metrics->setGauge('validation_errors', $this->errorsCount);
$this->metrics->setGauge('validation_warnings', $this->warningsCount);
$this->logger->logInfo("Validation complete: {$this->errorsCount} errors, {$this->warningsCount} warnings");
return $this->getResults();
}
/**
* Check if validation passed (no errors)
*/
public function passed(): bool
{
return $this->errorsCount === 0;
}
/**
* Get validation results
*/
public function getResults(): array
{
return [
'passed' => $this->passed(),
'errors' => $this->errorsCount,
'warnings' => $this->warningsCount,
'results' => $this->validationResults,
];
}
private function resetResults(): void
{
$this->validationResults = [];
$this->errorsCount = 0;
$this->warningsCount = 0;
}
private function validateRequiredFiles(string $path, array $files): void
{
foreach ($files as $filePattern) {
$found = false;
// Handle OR patterns (file1|file2)
if (strpos($filePattern, '|') !== false) {
$patterns = explode('|', $filePattern);
foreach ($patterns as $pattern) {
if ($this->filePatternExists($path, trim($pattern))) {
$found = true;
break;
}
}
} else {
$found = $this->filePatternExists($path, $filePattern);
}
if (!$found) {
$this->addError("Required file missing: {$filePattern}");
} else {
$this->addSuccess("Required file found: {$filePattern}");
}
}
}
private function validateRecommendedFiles(string $path, array $files): void
{
foreach ($files as $filePattern) {
$found = false;
// Handle OR patterns
if (strpos($filePattern, '|') !== false) {
$patterns = explode('|', $filePattern);
foreach ($patterns as $pattern) {
if ($this->filePatternExists($path, trim($pattern))) {
$found = true;
break;
}
}
} else {
$found = $this->filePatternExists($path, $filePattern);
}
if (!$found) {
$this->addWarning("Recommended file missing: {$filePattern}");
} else {
$this->addSuccess("Recommended file found: {$filePattern}");
}
}
}
private function validateProjectFields(string $path, string $projectType, array $fields): void
{
if (empty($fields)) {
return;
}
// Validate based on project type
switch ($projectType) {
case 'nodejs':
$this->validateNodeJSFields($path, $fields);
break;
case 'python':
$this->validatePythonFields($path, $fields);
break;
case 'wordpress':
$this->validateWordPressFields($path, $fields);
break;
default:
$this->logger->logInfo("No field validation for project type: {$projectType}");
}
}
private function validateNodeJSFields(string $path, array $fields): void
{
$packageFile = "{$path}/package.json";
if (!file_exists($packageFile)) {
$this->addError("Cannot validate fields: package.json not found");
return;
}
$package = json_decode(file_get_contents($packageFile), true);
if (!$package) {
$this->addError("Cannot parse package.json");
return;
}
foreach ($fields as $field) {
if (!isset($package[$field])) {
$this->addError("Required field missing in package.json: {$field}");
} else {
$this->addSuccess("Required field found in package.json: {$field}");
}
}
}
private function validatePythonFields(string $path, array $fields): void
{
$setupFile = "{$path}/setup.py";
$pyprojectFile = "{$path}/pyproject.toml";
if (!file_exists($setupFile) && !file_exists($pyprojectFile)) {
$this->addError("Cannot validate fields: setup.py or pyproject.toml not found");
return;
}
// Basic validation - check if fields appear in file content
$content = '';
if (file_exists($setupFile)) {
$content = file_get_contents($setupFile);
} elseif (file_exists($pyprojectFile)) {
$content = file_get_contents($pyprojectFile);
}
foreach ($fields as $field) {
if (stripos($content, $field) === false) {
$this->addWarning("Field may be missing: {$field}");
} else {
$this->addSuccess("Field appears to be present: {$field}");
}
}
}
private function validateWordPressFields(string $path, array $fields): void
{
$phpFiles = glob("{$path}/*.php");
if (empty($phpFiles)) {
$this->addError("No PHP files found for WordPress validation");
return;
}
$content = '';
foreach ($phpFiles as $file) {
$content .= file_get_contents($file);
}
foreach ($fields as $field) {
// Handle OR patterns
if (strpos($field, '|') !== false) {
$patterns = explode('|', $field);
$found = false;
foreach ($patterns as $pattern) {
if (stripos($content, trim($pattern)) !== false) {
$found = true;
break;
}
}
if (!$found) {
$this->addError("Required header field missing: {$field}");
} else {
$this->addSuccess("Required header field found");
}
} else {
if (stripos($content, $field) === false) {
$this->addError("Required header field missing: {$field}");
} else {
$this->addSuccess("Required header field found: {$field}");
}
}
}
}
private function filePatternExists(string $path, string $pattern): bool
{
// Handle wildcard patterns
if (strpos($pattern, '*') !== false) {
$files = glob("{$path}/{$pattern}");
return !empty($files);
}
return file_exists("{$path}/{$pattern}");
}
private function addError(string $message): void
{
$this->validationResults[] = [
'level' => 'error',
'message' => $message,
];
$this->errorsCount++;
$this->logger->logError($message);
}
private function addWarning(string $message): void
{
$this->validationResults[] = [
'level' => 'warning',
'message' => $message,
];
$this->warningsCount++;
$this->logger->logWarning($message);
}
private function addSuccess(string $message): void
{
$this->validationResults[] = [
'level' => 'success',
'message' => $message,
];
}
}