e8da1a30ff
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Successful in 10s
Generic: Repo Health / Scripts governance (push) Successful in 9s
Generic: Repo Health / Repository health (push) Successful in 17s
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 8s
Universal: PR Check / Build RC Package (pull_request) Successful in 5s
Generic: Repo Health / Repository health (pull_request) Successful in 19s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 1m16s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 1m29s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 41s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 6s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 1m38s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 1m40s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 1m41s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 1m40s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 1m20s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Successful in 1m24s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 1m24s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Successful in 1m26s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Successful in 1m30s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 12s
Removed 13 write-only properties and unused code. Remaining 41 baselined items are defensive patterns (null coalesce on API responses, boolean safety checks) that are intentional. PHPStan level 4: 0 errors with baseline. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
366 lines
10 KiB
PHP
366 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.
|
|
*/
|
|
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;
|
|
}
|
|
}
|