Files
moko-platform/lib/Enterprise/RepositoryHealthChecker.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

368 lines
10 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
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/RepositoryHealthChecker.php
* BRIEF: Repository health checking enterprise library
*/
declare(strict_types=1);
namespace MokoEnterprise;
/**
* Repository Health Checker
*
* Enterprise library for performing comprehensive repository health checks
* with scoring system and category-based validation.
*
* @since 04.00.00
*/
class RepositoryHealthChecker
{
private AuditLogger $logger;
private MetricsCollector $metrics;
private array $results = [
'categories' => [],
'checks' => [],
'score' => 0,
'max_score' => 100,
'percentage' => 0.0,
'level' => 'unknown',
];
/**
* Constructor
*/
public function __construct(
?AuditLogger $logger = null,
?MetricsCollector $metrics = null,
?UnifiedValidator $validator = null
) {
$this->logger = $logger ?? new AuditLogger('repo_health_checker');
$this->metrics = $metrics ?? new MetricsCollector();
}
/**
* Check repository health
*
* @param string $path Repository path to check
* @return array Health check results
*/
public function check(string $path): array
{
$this->logger->logInfo("Starting health check for: {$path}");
$this->resetResults();
// Run all check categories
$this->runStructureChecks($path);
$this->runDocumentationChecks($path);
$this->runWorkflowChecks($path);
$this->runSecurityChecks($path);
// Calculate final scores
$this->calculateScore();
// Record metrics
$this->metrics->setGauge('repo_health_score', $this->results['percentage']);
$this->metrics->setGauge(
'repo_health_checks_passed',
count(array_filter($this->results['checks'], fn($c) => $c['passed']))
);
$this->logger->logInfo("Health check complete: {$this->results['percentage']}% ({$this->results['level']})");
return $this->results;
}
/**
* Reset results for new check
*/
private function resetResults(): void
{
$this->results = [
'categories' => [],
'checks' => [],
'score' => 0,
'max_score' => 100,
'percentage' => 0.0,
'level' => 'unknown',
];
}
/**
* Run repository structure checks
*/
private function runStructureChecks(string $path): void
{
$category = 'structure';
$this->results['categories'][$category] = [
'name' => 'Repository Structure',
'max_points' => 30,
'earned_points' => 0,
'checks_passed' => 0,
'checks_failed' => 0,
];
// Check README exists
$this->addCheck(
$category,
'README.md exists',
file_exists("{$path}/README.md"),
10
);
// Check LICENSE exists
$this->addCheck(
$category,
'LICENSE file exists',
file_exists("{$path}/LICENSE"),
10
);
// Check .gitignore exists
$this->addCheck(
$category,
'.gitignore exists',
file_exists("{$path}/.gitignore"),
5
);
// Check CHANGELOG exists
$this->addCheck(
$category,
'CHANGELOG.md exists',
file_exists("{$path}/CHANGELOG.md"),
5
);
}
/**
* Run documentation checks
*/
private function runDocumentationChecks(string $path): void
{
$category = 'documentation';
$this->results['categories'][$category] = [
'name' => 'Documentation',
'max_points' => 25,
'earned_points' => 0,
'checks_passed' => 0,
'checks_failed' => 0,
];
// Check docs directory exists
$this->addCheck(
$category,
'docs/ directory exists',
is_dir("{$path}/docs"),
10
);
// Check README has content
if (file_exists("{$path}/README.md")) {
$content = file_get_contents("{$path}/README.md");
$this->addCheck(
$category,
'README has substantial content',
strlen($content) > 500,
10
);
}
// Check for code of conduct
$this->addCheck(
$category,
'CODE_OF_CONDUCT.md exists',
file_exists("{$path}/CODE_OF_CONDUCT.md"),
5
);
}
/**
* Run workflow checks
*/
private function runWorkflowChecks(string $path): void
{
$category = 'workflows';
$this->results['categories'][$category] = [
'name' => 'CI/CD Workflows',
'max_points' => 20,
'earned_points' => 0,
'checks_passed' => 0,
'checks_failed' => 0,
];
// Check both .github/workflows and .gitea/workflows
$githubDir = "{$path}/.github/workflows";
$giteaDir = "{$path}/.mokogitea/workflows";
$hasWorkflowDir = is_dir($githubDir) || is_dir($giteaDir);
$workflowDir = is_dir($giteaDir) ? $giteaDir : $githubDir;
// Check workflows directory exists
$this->addCheck(
$category,
'Workflows directory exists',
$hasWorkflowDir,
10
);
// Check for CI workflow
if ($hasWorkflowDir) {
$hasCI = !empty(glob("{$workflowDir}/ci*.yml")) || !empty(glob("{$workflowDir}/ci*.yaml"));
$this->addCheck(
$category,
'CI workflow exists',
$hasCI,
10
);
}
}
/**
* Run security checks
*/
private function runSecurityChecks(string $path): void
{
$category = 'security';
$this->results['categories'][$category] = [
'name' => 'Security',
'max_points' => 25,
'earned_points' => 0,
'checks_passed' => 0,
'checks_failed' => 0,
];
// Check for SECURITY.md
$this->addCheck(
$category,
'SECURITY.md exists',
file_exists("{$path}/SECURITY.md") ||
file_exists("{$path}/.github/SECURITY.md"),
10
);
// Check for security scanning workflow (CodeQL on GitHub, Trivy on Gitea)
$githubWf = "{$path}/.github/workflows";
$giteaWf = "{$path}/.mokogitea/workflows";
$hasSecurityScan = false;
if (is_dir($githubWf)) {
$hasSecurityScan = !empty(glob("{$githubWf}/*codeql*.yml")) || !empty(glob("{$githubWf}/*codeql*.yaml"));
}
if (!$hasSecurityScan && is_dir($giteaWf)) {
$hasSecurityScan = !empty(glob("{$giteaWf}/*trivy*.yml")) || !empty(glob("{$giteaWf}/*trivy*.yaml"));
}
$this->addCheck(
$category,
'Security scanning workflow exists',
$hasSecurityScan,
10
);
// Check for dependency management (Dependabot on GitHub, Renovate on Gitea)
$this->addCheck(
$category,
'Dependency management configured',
file_exists("{$path}/.github/dependabot.yml") ||
file_exists("{$path}/.github/dependabot.yaml") ||
file_exists("{$path}/renovate.json") ||
file_exists("{$path}/.renovaterc.json"),
5
);
}
/**
* Add a check result
*/
private function addCheck(string $category, string $name, bool $passed, int $points): void
{
$this->results['checks'][] = [
'category' => $category,
'name' => $name,
'passed' => $passed,
'points' => $points,
];
if ($passed) {
$this->results['categories'][$category]['earned_points'] += $points;
$this->results['categories'][$category]['checks_passed']++;
} else {
$this->results['categories'][$category]['checks_failed']++;
}
}
/**
* Calculate overall score and health level
*/
private function calculateScore(): void
{
$totalEarned = 0;
$maxScore = 0;
foreach ($this->results['categories'] as $category) {
$totalEarned += $category['earned_points'];
$maxScore += $category['max_points'];
}
$this->results['score'] = $totalEarned;
$this->results['max_score'] = $maxScore;
$this->results['percentage'] = $maxScore > 0 ? ($totalEarned / $maxScore * 100) : 0;
// Determine health level
$pct = $this->results['percentage'];
if ($pct >= 90) {
$this->results['level'] = 'excellent';
} elseif ($pct >= 80) {
$this->results['level'] = 'good';
} elseif ($pct >= 70) {
$this->results['level'] = 'fair';
} elseif ($pct >= 60) {
$this->results['level'] = 'poor';
} else {
$this->results['level'] = 'critical';
}
}
/**
* Get failed checks
*
* @return array Array of failed checks
*/
public function getFailedChecks(): array
{
return array_filter($this->results['checks'], fn($c) => !$c['passed']);
}
/**
* Get passed checks
*
* @return array Array of passed checks
*/
public function getPassedChecks(): array
{
return array_filter($this->results['checks'], fn($c) => $c['passed']);
}
/**
* Check if repository meets threshold
*
* @param float $threshold Minimum percentage required
* @return bool True if meets threshold
*/
public function meetsThreshold(float $threshold): bool
{
return $this->results['percentage'] >= $threshold;
}
}