cbfa23c4c4
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Repository health (push) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 45s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: PR Check / Build RC Package (pull_request) Successful in 2s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 44s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 48s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 48s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 48s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 50s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 1m13s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 5s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 42s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Failing after 45s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 44s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Failing after 47s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Failing after 49s
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Real bugs found and fixed: - bulk_joomla_template: $org undefined in heredoc (missing parameter) - RepositorySynchronizer: $root undefined (should be $repoRoot), duplicate array key - RepositoryHealthChecker: wrong class name (UnifiedValidation → UnifiedValidator) - scan_drift: missing $adapter property declaration - auto_detect_platform: wrong method name (detectProjectType → detect) - EnterpriseReadinessValidator: void return used as value - check_client_theme: extra parameter to printSummary() - ApiClient: unused constructor parameter now stored - GitPlatformAdapter: added listBranches/getCloneUrl/cloneRepo to interface - MokoGiteaAdapter/GitHubAdapter: implemented new interface methods 3 legacy CLIApp scripts excluded (need migration to CliFramework): repo_cleanup.php, push_files.php, joomla_release.php Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
534 lines
15 KiB
PHP
534 lines
15 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.Plugins
|
|
* INGROUP: MokoStandards
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /lib/Enterprise/Plugins/GenericPlugin.php
|
|
* BRIEF: Enterprise plugin for generic projects
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MokoEnterprise\Plugins;
|
|
|
|
use MokoEnterprise\AbstractProjectPlugin;
|
|
|
|
/**
|
|
* Generic Project Plugin
|
|
*
|
|
* Provides validation, metrics, and management capabilities for
|
|
* generic projects that don't fit specific technology categories.
|
|
*/
|
|
class GenericPlugin extends AbstractProjectPlugin
|
|
{
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getProjectType(): string
|
|
{
|
|
return 'generic';
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getPluginName(): string
|
|
{
|
|
return 'Generic Project Plugin';
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function validateProject(array $config, string $projectPath): array
|
|
{
|
|
$errors = [];
|
|
$warnings = [];
|
|
|
|
// Check for README
|
|
if (
|
|
!$this->fileExists($projectPath, 'README.md') &&
|
|
!$this->fileExists($projectPath, 'README') &&
|
|
!$this->fileExists($projectPath, 'README.txt')
|
|
) {
|
|
$errors[] = 'No README file found';
|
|
}
|
|
|
|
// Check for LICENSE
|
|
if (
|
|
!$this->fileExists($projectPath, 'LICENSE') &&
|
|
!$this->fileExists($projectPath, 'LICENSE.md') &&
|
|
!$this->fileExists($projectPath, 'COPYING')
|
|
) {
|
|
$warnings[] = 'No LICENSE file found';
|
|
}
|
|
|
|
// Check for version control ignore file
|
|
if (
|
|
!$this->fileExists($projectPath, '.gitignore') &&
|
|
!$this->fileExists($projectPath, '.hgignore')
|
|
) {
|
|
$warnings[] = 'No version control ignore file found';
|
|
}
|
|
|
|
// Check for CI/CD configuration
|
|
$hasCICD = $this->fileExists($projectPath, '.mokogitea/workflows') ||
|
|
$this->fileExists($projectPath, '.mokogitea/workflows') ||
|
|
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
|
|
$this->fileExists($projectPath, '.travis.yml') ||
|
|
$this->fileExists($projectPath, 'Jenkinsfile') ||
|
|
$this->fileExists($projectPath, '.circleci');
|
|
|
|
if (!$hasCICD) {
|
|
$warnings[] = 'No CI/CD configuration found';
|
|
}
|
|
|
|
// Check for security policy
|
|
if (!$this->fileExists($projectPath, 'SECURITY.md')) {
|
|
$warnings[] = 'No SECURITY.md file found';
|
|
}
|
|
|
|
// Check for contributing guidelines
|
|
if (!$this->fileExists($projectPath, 'CONTRIBUTING.md')) {
|
|
$warnings[] = 'No CONTRIBUTING.md file found';
|
|
}
|
|
|
|
// Check for code of conduct
|
|
if (!$this->fileExists($projectPath, 'CODE_OF_CONDUCT.md')) {
|
|
$warnings[] = 'No CODE_OF_CONDUCT.md file found';
|
|
}
|
|
|
|
$this->log(
|
|
'Generic project validation completed',
|
|
'info',
|
|
['errors' => count($errors), 'warnings' => count($warnings)]
|
|
);
|
|
|
|
return [
|
|
'valid' => empty($errors),
|
|
'errors' => $errors,
|
|
'warnings' => $warnings,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function collectMetrics(string $projectPath, array $config): array
|
|
{
|
|
$metrics = [
|
|
'total_files' => $this->countAllFiles($projectPath),
|
|
'has_readme' => $this->hasReadme($projectPath),
|
|
'has_license' => $this->hasLicense($projectPath),
|
|
'has_cicd' => $this->hasCICD($projectPath),
|
|
'has_tests' => $this->hasTests($projectPath),
|
|
'has_documentation' => $this->hasDocumentation($projectPath),
|
|
'language_detected' => $this->detectPrimaryLanguage($projectPath),
|
|
];
|
|
|
|
// Count by file extension
|
|
$extensions = $this->countByExtension($projectPath);
|
|
$metrics['file_types'] = $extensions;
|
|
$metrics['dominant_type'] = $this->getDominantFileType($extensions);
|
|
|
|
// Directory structure depth
|
|
$metrics['max_depth'] = $this->getDirectoryDepth($projectPath);
|
|
|
|
// Count lines
|
|
$metrics['total_lines'] = $this->countTotalLines($projectPath);
|
|
|
|
// Record metrics
|
|
$this->recordMetric('generic', 'total_files', $metrics['total_files']);
|
|
$this->recordMetric('generic', 'total_lines', $metrics['total_lines']);
|
|
|
|
$this->log('Collected generic project metrics', 'info', $metrics);
|
|
|
|
return $metrics;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function healthCheck(string $projectPath, array $config): array
|
|
{
|
|
$issues = [];
|
|
$score = 100;
|
|
|
|
// Check README
|
|
if (!$this->hasReadme($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'Missing README file',
|
|
];
|
|
$score -= 15;
|
|
}
|
|
|
|
// Check LICENSE
|
|
if (!$this->hasLicense($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'Missing LICENSE file',
|
|
];
|
|
$score -= 15;
|
|
}
|
|
|
|
// Check version control
|
|
if (
|
|
!$this->fileExists($projectPath, '.git') &&
|
|
!$this->fileExists($projectPath, '.hg')
|
|
) {
|
|
$issues[] = [
|
|
'severity' => 'info',
|
|
'message' => 'Not under version control',
|
|
];
|
|
$score -= 10;
|
|
}
|
|
|
|
// Check .gitignore
|
|
if (
|
|
$this->fileExists($projectPath, '.git') &&
|
|
!$this->fileExists($projectPath, '.gitignore')
|
|
) {
|
|
$issues[] = [
|
|
'severity' => 'warning',
|
|
'message' => 'Missing .gitignore file',
|
|
];
|
|
$score -= 10;
|
|
}
|
|
|
|
// Check for documentation
|
|
if (!$this->hasDocumentation($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'info',
|
|
'message' => 'No documentation directory found',
|
|
];
|
|
$score -= 5;
|
|
}
|
|
|
|
// Check for tests
|
|
if (!$this->hasTests($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'info',
|
|
'message' => 'No test directory found',
|
|
];
|
|
$score -= 10;
|
|
}
|
|
|
|
// Check CI/CD
|
|
if (!$this->hasCICD($projectPath)) {
|
|
$issues[] = [
|
|
'severity' => 'info',
|
|
'message' => 'No CI/CD configuration found',
|
|
];
|
|
$score -= 10;
|
|
}
|
|
|
|
// Check for security policy
|
|
if (!$this->fileExists($projectPath, 'SECURITY.md')) {
|
|
$issues[] = [
|
|
'severity' => 'info',
|
|
'message' => 'No SECURITY.md file found',
|
|
];
|
|
$score -= 5;
|
|
}
|
|
|
|
// Check for changelog
|
|
if (
|
|
!$this->fileExists($projectPath, 'CHANGELOG.md') &&
|
|
!$this->fileExists($projectPath, 'CHANGELOG')
|
|
) {
|
|
$issues[] = [
|
|
'severity' => 'info',
|
|
'message' => 'No CHANGELOG file found',
|
|
];
|
|
$score -= 5;
|
|
}
|
|
|
|
$score = max(0, $score);
|
|
|
|
$this->log('Generic project health check completed', 'info', [
|
|
'score' => $score,
|
|
'issues_count' => count($issues),
|
|
]);
|
|
|
|
return [
|
|
'healthy' => $score >= 60,
|
|
'score' => $score,
|
|
'issues' => $issues,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getRequiredFiles(): array
|
|
{
|
|
return [
|
|
'README.md or README',
|
|
'LICENSE or COPYING',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getRecommendedFiles(): array
|
|
{
|
|
return [
|
|
'.gitignore',
|
|
'CHANGELOG.md',
|
|
'CONTRIBUTING.md',
|
|
'CODE_OF_CONDUCT.md',
|
|
'SECURITY.md',
|
|
'.mokogitea/workflows/* or .gitea/workflows/* or .gitlab-ci.yml',
|
|
'docs/ or documentation/',
|
|
'tests/ or test/',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getConfigSchema(): array
|
|
{
|
|
return [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'project_name' => [
|
|
'type' => 'string',
|
|
'description' => 'Project name',
|
|
],
|
|
'primary_language' => [
|
|
'type' => 'string',
|
|
'description' => 'Primary programming language',
|
|
],
|
|
'requires_build' => [
|
|
'type' => 'boolean',
|
|
'description' => 'Project requires build step',
|
|
],
|
|
'has_dependencies' => [
|
|
'type' => 'boolean',
|
|
'description' => 'Project has external dependencies',
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getBestPractices(): array
|
|
{
|
|
return [
|
|
'Include comprehensive README with project description',
|
|
'Add clear LICENSE file with appropriate license',
|
|
'Maintain CHANGELOG for version history',
|
|
'Provide CONTRIBUTING guidelines for contributors',
|
|
'Include CODE_OF_CONDUCT for community standards',
|
|
'Add SECURITY policy for vulnerability reporting',
|
|
'Use .gitignore to exclude generated files',
|
|
'Implement CI/CD for automated testing and deployment',
|
|
'Organize code in logical directory structure',
|
|
'Include documentation in docs/ directory',
|
|
'Add unit and integration tests',
|
|
'Use semantic versioning for releases',
|
|
'Keep dependencies up to date',
|
|
'Document installation and usage instructions',
|
|
'Add examples and tutorials where applicable',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Check if project has README
|
|
*/
|
|
private function hasReadme(string $projectPath): bool
|
|
{
|
|
return $this->fileExists($projectPath, 'README.md') ||
|
|
$this->fileExists($projectPath, 'README') ||
|
|
$this->fileExists($projectPath, 'README.txt');
|
|
}
|
|
|
|
/**
|
|
* Check if project has LICENSE
|
|
*/
|
|
private function hasLicense(string $projectPath): bool
|
|
{
|
|
return $this->fileExists($projectPath, 'LICENSE') ||
|
|
$this->fileExists($projectPath, 'LICENSE.md') ||
|
|
$this->fileExists($projectPath, 'COPYING') ||
|
|
$this->fileExists($projectPath, 'LICENSE.txt');
|
|
}
|
|
|
|
/**
|
|
* Check if project has CI/CD
|
|
*/
|
|
private function hasCICD(string $projectPath): bool
|
|
{
|
|
return $this->fileExists($projectPath, '.mokogitea/workflows') ||
|
|
$this->fileExists($projectPath, '.mokogitea/workflows') ||
|
|
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
|
|
$this->fileExists($projectPath, '.travis.yml') ||
|
|
$this->fileExists($projectPath, 'Jenkinsfile') ||
|
|
$this->fileExists($projectPath, '.circleci/config.yml');
|
|
}
|
|
|
|
/**
|
|
* Check if project has tests
|
|
*/
|
|
private function hasTests(string $projectPath): bool
|
|
{
|
|
return $this->fileExists($projectPath, 'tests') ||
|
|
$this->fileExists($projectPath, 'test') ||
|
|
$this->fileExists($projectPath, '__tests__') ||
|
|
$this->fileExists($projectPath, 'spec');
|
|
}
|
|
|
|
/**
|
|
* Check if project has documentation
|
|
*/
|
|
private function hasDocumentation(string $projectPath): bool
|
|
{
|
|
return $this->fileExists($projectPath, 'docs') ||
|
|
$this->fileExists($projectPath, 'doc') ||
|
|
$this->fileExists($projectPath, 'documentation');
|
|
}
|
|
|
|
/**
|
|
* Count all files
|
|
*/
|
|
private function countAllFiles(string $projectPath): int
|
|
{
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($projectPath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::LEAVES_ONLY
|
|
);
|
|
|
|
$count = 0;
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile()) {
|
|
$count++;
|
|
}
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Count files by extension
|
|
*/
|
|
private function countByExtension(string $projectPath): array
|
|
{
|
|
$extensions = [];
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($projectPath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::LEAVES_ONLY
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile()) {
|
|
$ext = strtolower($file->getExtension());
|
|
$extensions[$ext] = ($extensions[$ext] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
arsort($extensions);
|
|
return array_slice($extensions, 0, 10);
|
|
}
|
|
|
|
/**
|
|
* Get dominant file type
|
|
*/
|
|
private function getDominantFileType(array $extensions): string
|
|
{
|
|
if (empty($extensions)) {
|
|
return 'unknown';
|
|
}
|
|
|
|
reset($extensions);
|
|
return key($extensions);
|
|
}
|
|
|
|
/**
|
|
* Get directory depth
|
|
*/
|
|
private function getDirectoryDepth(string $projectPath): int
|
|
{
|
|
$maxDepth = 0;
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($projectPath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
$depth = $iterator->getDepth();
|
|
if ($depth > $maxDepth) {
|
|
$maxDepth = $depth;
|
|
}
|
|
}
|
|
|
|
return $maxDepth;
|
|
}
|
|
|
|
/**
|
|
* Count total lines
|
|
*/
|
|
private function countTotalLines(string $projectPath): int
|
|
{
|
|
$totalLines = 0;
|
|
$textExtensions = ['php', 'js', 'py', 'java', 'c', 'cpp', 'h', 'cs', 'go', 'rb', 'ts', 'tsx', 'jsx'];
|
|
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($projectPath, \RecursiveDirectoryIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::LEAVES_ONLY
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile()) {
|
|
$ext = strtolower($file->getExtension());
|
|
if (in_array($ext, $textExtensions)) {
|
|
$totalLines += count(file($file->getPathname()));
|
|
}
|
|
}
|
|
}
|
|
|
|
return $totalLines;
|
|
}
|
|
|
|
/**
|
|
* Detect primary language
|
|
*/
|
|
private function detectPrimaryLanguage(string $projectPath): string
|
|
{
|
|
$extensions = $this->countByExtension($projectPath);
|
|
$languageMap = [
|
|
'php' => 'PHP',
|
|
'js' => 'JavaScript',
|
|
'ts' => 'TypeScript',
|
|
'py' => 'Python',
|
|
'java' => 'Java',
|
|
'c' => 'C',
|
|
'cpp' => 'C++',
|
|
'cs' => 'C#',
|
|
'go' => 'Go',
|
|
'rb' => 'Ruby',
|
|
'rs' => 'Rust',
|
|
];
|
|
|
|
foreach ($extensions as $ext => $count) {
|
|
if (isset($languageMap[$ext])) {
|
|
return $languageMap[$ext];
|
|
}
|
|
}
|
|
|
|
return 'Unknown';
|
|
}
|
|
}
|