ae2860c3b5
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 6s
Generic: Repo Health / Access control (push) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 22s
Universal: Auto Version Bump / Version Bump (push) Failing after 23s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m13s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m17s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
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
Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
623 lines
22 KiB
PHP
Executable File
623 lines
22 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* DEFGROUP: moko-platform.Validate
|
|
* INGROUP: moko-platform
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /validate/check_repo_health.php
|
|
* BRIEF: Repository health checker — validates against current Moko standards (wiki-first, no docs/)
|
|
*
|
|
* Categories:
|
|
* Required Files (40) — README, LICENSE, CHANGELOG, CONTRIBUTING, SECURITY, CLAUDE.md, .gitignore, Makefile
|
|
* Manifest & Config (20) — .moko-platform, workflows, README quality, CODE_OF_CONDUCT, .gitignore content, CLAUDE.md quality
|
|
* Documentation (15) — wiki-first: docs/ must NOT exist, CHANGELOG [Unreleased]
|
|
* License Headers (15) — Copyright, SPDX, FILE INFORMATION in source files
|
|
* Disallowed (10) — TODO.md, vendor/, node_modules/, .claude/, wiki/, .mcp.json, renovate.json, profile.ps1
|
|
* Workflows (15) — repo-health, sync-roadmap-wiki, CI/deploy
|
|
* Security (20) — SECURITY.md, scanning, no renovate.json, no secrets
|
|
* Rulesets (15) — main protected, dev branch, rulesets
|
|
* Deployment (10) — deploy workflow, build system
|
|
*
|
|
* Note: This file uses curl_init for HTTP requests to the Gitea API.
|
|
* No shell commands or child processes are invoked.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
use MokoEnterprise\{AuditLogger, CliFramework, MetricsCollector, PluginFactory};
|
|
|
|
/**
|
|
* Repository Health Checker
|
|
*
|
|
* Validates repository structure, standards compliance, and configuration
|
|
* against moko-platform definitions. Produces a health score and report.
|
|
*
|
|
* @since 04.00.00
|
|
*/
|
|
class RepoHealthChecker extends CliFramework
|
|
{
|
|
private AuditLogger $logger;
|
|
private MetricsCollector $metrics;
|
|
private string $apiBaseUrl = 'https://git.mokoconsulting.tech/api/v1';
|
|
|
|
private array $results = [
|
|
'categories' => [], 'checks' => [],
|
|
'score' => 0, 'max_score' => 0, 'percentage' => 0.0, 'level' => 'unknown',
|
|
];
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Check repository health against Moko standards');
|
|
$this->addArgument('--path', 'Repository path', '.');
|
|
$this->addArgument('--threshold', 'Minimum health %', '70');
|
|
$this->addArgument('--json', 'JSON output', false);
|
|
$this->addArgument('--create-issue', 'Create Gitea issue', false);
|
|
$this->addArgument('--repo', 'owner/repo', '');
|
|
}
|
|
|
|
protected function initialize(): void
|
|
{
|
|
parent::initialize();
|
|
$this->logger = new AuditLogger('repo_health_checker');
|
|
$this->metrics = new MetricsCollector();
|
|
$config = \MokoEnterprise\Config::load();
|
|
$this->apiBaseUrl = rtrim($config->getString('gitea.url', 'https://git.mokoconsulting.tech'), '/') . '/api/v1';
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$path = $this->getArgument('--path');
|
|
$threshold = (float)$this->getArgument('--threshold');
|
|
$repo = $this->getArgument('--repo');
|
|
|
|
$this->section('Required Files');
|
|
$this->checkRequiredFiles($path);
|
|
$this->section('Manifest & Config');
|
|
$this->checkManifest($path);
|
|
$this->section('Documentation');
|
|
$this->checkDocumentation($path);
|
|
$this->section('License Headers');
|
|
$this->checkLicenseHeaders($path);
|
|
$this->section('Disallowed Items');
|
|
$this->checkDisallowed($path);
|
|
$this->section('Workflows');
|
|
$this->checkWorkflows($path);
|
|
$this->section('Security');
|
|
$this->checkSecurity($path);
|
|
$this->section('Rulesets');
|
|
$this->checkRulesets($repo);
|
|
$this->section('Deployment');
|
|
$this->checkDeployment($path);
|
|
|
|
$this->calculateScore();
|
|
|
|
if ($this->getArgument('--json')) {
|
|
echo json_encode($this->results, JSON_PRETTY_PRINT) . PHP_EOL;
|
|
} else {
|
|
$this->displayResults();
|
|
}
|
|
|
|
if ($this->getArgument('--create-issue') && $repo) {
|
|
$this->createHealthIssue($repo);
|
|
}
|
|
|
|
return $this->results['percentage'] >= $threshold ? 0 : 1;
|
|
}
|
|
|
|
// ── Required Files (40 pts) ──────────────────────────────────────
|
|
|
|
private function checkRequiredFiles(string $p): void
|
|
{
|
|
$cat = 'required_files';
|
|
$this->initCategory($cat, 'Required Files', 40);
|
|
|
|
foreach (
|
|
[
|
|
'README.md' => 8, 'LICENSE' => 8, 'CHANGELOG.md' => 5,
|
|
'CONTRIBUTING.md' => 4, 'SECURITY.md' => 4,
|
|
'CLAUDE.md' => 5, '.gitignore' => 3,
|
|
'Makefile' => 3,
|
|
] as $file => $pts
|
|
) {
|
|
$this->addCheck($cat, "{$file} exists", file_exists("{$p}/{$file}"), $pts);
|
|
}
|
|
|
|
// Negative checks — files that should NOT be committed
|
|
foreach (['.mcp.json', 'TODO.md', 'renovate.json'] as $bad) {
|
|
if (file_exists("{$p}/{$bad}")) {
|
|
$this->addCheck($cat, "{$bad} should not be committed", false, 0);
|
|
}
|
|
}
|
|
if (is_dir("{$p}/.claude")) {
|
|
$this->addCheck($cat, '.claude/ should not be committed', false, 0);
|
|
}
|
|
if (is_dir("{$p}/wiki")) {
|
|
$this->addCheck($cat, 'wiki/ should not be committed', false, 0);
|
|
}
|
|
}
|
|
|
|
// ── Manifest & Config (15 pts) ───────────────────────────────────
|
|
|
|
private function checkManifest(string $p): void
|
|
{
|
|
$cat = 'manifest';
|
|
$this->initCategory($cat, 'Manifest & Config', 15);
|
|
|
|
$this->addCheck(
|
|
$cat,
|
|
'.mokogitea/.moko-platform manifest',
|
|
file_exists("{$p}/.gitea/.moko-platform"),
|
|
5
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'Workflows directory',
|
|
is_dir("{$p}/.gitea/workflows") || is_dir("{$p}/.github/workflows"),
|
|
5
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'README >500 chars',
|
|
file_exists("{$p}/README.md") && strlen(file_get_contents("{$p}/README.md")) > 500,
|
|
3
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'CODE_OF_CONDUCT.md',
|
|
file_exists("{$p}/CODE_OF_CONDUCT.md"),
|
|
2
|
|
);
|
|
|
|
// .gitignore must contain key exclusions
|
|
$gitignoreOk = false;
|
|
if (file_exists("{$p}/.gitignore")) {
|
|
$gi = file_get_contents("{$p}/.gitignore");
|
|
$gitignoreOk = str_contains($gi, '.claude/') && str_contains($gi, 'TODO.md')
|
|
&& str_contains($gi, '*.min.css') && str_contains($gi, '*.min.js')
|
|
&& str_contains($gi, 'wiki/');
|
|
}
|
|
$this->addCheck(
|
|
$cat,
|
|
'.gitignore has .claude/, TODO.md, *.min.css/js, wiki/',
|
|
$gitignoreOk,
|
|
3
|
|
);
|
|
|
|
// CLAUDE.md should have project overview
|
|
$claudeOk = false;
|
|
if (file_exists("{$p}/CLAUDE.md")) {
|
|
$claude = file_get_contents("{$p}/CLAUDE.md");
|
|
$claudeOk = strlen($claude) > 200 && str_contains($claude, 'moko-platform');
|
|
}
|
|
$this->addCheck(
|
|
$cat,
|
|
'CLAUDE.md has project context + moko-platform ref',
|
|
$claudeOk,
|
|
2
|
|
);
|
|
}
|
|
|
|
// ── Documentation: Wiki-First (15 pts) ───────────────────────────
|
|
|
|
private function checkDocumentation(string $p): void
|
|
{
|
|
$cat = 'documentation';
|
|
$this->initCategory($cat, 'Documentation (Wiki-First)', 15);
|
|
|
|
$this->addCheck(
|
|
$cat,
|
|
'No docs/ directory (wiki-first)',
|
|
!is_dir("{$p}/docs"),
|
|
10
|
|
);
|
|
|
|
// CHANGELOG must have [Unreleased] section for release workflow
|
|
$hasUnreleased = false;
|
|
if (file_exists("{$p}/CHANGELOG.md")) {
|
|
$cl = file_get_contents("{$p}/CHANGELOG.md");
|
|
$hasUnreleased = (bool)preg_match('/##\s*\[?Unreleased/i', $cl);
|
|
}
|
|
$this->addCheck(
|
|
$cat,
|
|
'CHANGELOG has [Unreleased] section',
|
|
$hasUnreleased,
|
|
5
|
|
);
|
|
}
|
|
|
|
// ── License Headers (15 pts) ────────────────────────────────────
|
|
|
|
private function checkLicenseHeaders(string $p): void
|
|
{
|
|
$cat = 'license_headers';
|
|
$this->initCategory($cat, 'License Headers', 15);
|
|
|
|
// Collect source files by scanning directories
|
|
$extensions = ['php', 'ts', 'css', 'yml', 'yaml'];
|
|
$files = [];
|
|
$dirs = [$p];
|
|
while ($dirs) {
|
|
$dir = array_pop($dirs);
|
|
$base = basename($dir);
|
|
if (in_array($base, ['vendor', 'node_modules', 'dist', '.git'], true)) {
|
|
continue;
|
|
}
|
|
$items = @scandir($dir);
|
|
if (!$items) {
|
|
continue;
|
|
}
|
|
foreach ($items as $item) {
|
|
if ($item === '.' || $item === '..') {
|
|
continue;
|
|
}
|
|
$full = "{$dir}/{$item}";
|
|
if (is_dir($full)) {
|
|
$dirs[] = $full;
|
|
continue;
|
|
}
|
|
$ext = pathinfo($item, PATHINFO_EXTENSION);
|
|
if (in_array($ext, $extensions, true)) {
|
|
$files[] = $full;
|
|
}
|
|
}
|
|
}
|
|
|
|
$total = count($files);
|
|
$withCopyright = 0;
|
|
$withSpdx = 0;
|
|
$withFileInfo = 0;
|
|
|
|
foreach ($files as $fullPath) {
|
|
$header = '';
|
|
$handle = @fopen($fullPath, 'r');
|
|
if (!$handle) {
|
|
continue;
|
|
}
|
|
for ($j = 0; $j < 20 && !feof($handle); $j++) {
|
|
$header .= (string) fgets($handle);
|
|
}
|
|
fclose($handle);
|
|
|
|
if (str_contains($header, 'Copyright')) {
|
|
$withCopyright++;
|
|
}
|
|
if (str_contains($header, 'SPDX-License-Identifier:')) {
|
|
$withSpdx++;
|
|
}
|
|
if (
|
|
str_contains($header, 'FILE INFORMATION') ||
|
|
str_contains($header, 'DEFGROUP:') ||
|
|
str_contains($header, 'BRIEF:')
|
|
) {
|
|
$withFileInfo++;
|
|
}
|
|
}
|
|
|
|
if ($total === 0) {
|
|
$this->addCheck($cat, 'No source files found', true, 15);
|
|
return;
|
|
}
|
|
|
|
$copyrightPct = $withCopyright / $total * 100;
|
|
$spdxPct = $withSpdx / $total * 100;
|
|
$fileInfoPct = $withFileInfo / $total * 100;
|
|
|
|
$this->addCheck(
|
|
$cat,
|
|
sprintf('Copyright headers (%.0f%% of %d files)', $copyrightPct, $total),
|
|
$copyrightPct >= 80,
|
|
5
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
sprintf('SPDX-License-Identifier (%.0f%%)', $spdxPct),
|
|
$spdxPct >= 80,
|
|
5
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
sprintf('FILE INFORMATION block (%.0f%%)', $fileInfoPct),
|
|
$fileInfoPct >= 70,
|
|
5
|
|
);
|
|
}
|
|
|
|
// ── Disallowed Items (10 pts) ────────────────────────────────────
|
|
|
|
private function checkDisallowed(string $p): void
|
|
{
|
|
$cat = 'disallowed';
|
|
$this->initCategory($cat, 'Disallowed Items', 10);
|
|
|
|
$this->addCheck(
|
|
$cat,
|
|
'No TODO.md (use issues)',
|
|
!file_exists("{$p}/TODO.md"),
|
|
2
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'No vendor/ committed',
|
|
!is_dir("{$p}/vendor") || file_exists("{$p}/vendor/.gitkeep"),
|
|
2
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'No node_modules/',
|
|
!is_dir("{$p}/node_modules"),
|
|
2
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'No .claude/ committed',
|
|
!is_dir("{$p}/.claude"),
|
|
1
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'No .mcp.json committed',
|
|
!file_exists("{$p}/.mcp.json"),
|
|
1
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'No renovate.json',
|
|
!file_exists("{$p}/renovate.json"),
|
|
1
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'No profile.ps1',
|
|
!file_exists("{$p}/profile.ps1"),
|
|
1
|
|
);
|
|
}
|
|
|
|
// ── Workflows (15 pts) ───────────────────────────────────────────
|
|
|
|
private function checkWorkflows(string $p): void
|
|
{
|
|
$cat = 'workflows';
|
|
$this->initCategory($cat, 'Workflows', 15);
|
|
|
|
$wf = is_dir("{$p}/.gitea/workflows") ? "{$p}/.gitea/workflows" : "{$p}/.github/workflows";
|
|
$exists = is_dir($wf);
|
|
|
|
$this->addCheck($cat, 'Workflows directory', $exists, 5);
|
|
$this->addCheck(
|
|
$cat,
|
|
'repo-health.yml',
|
|
$exists && file_exists("{$wf}/repo-health.yml"),
|
|
3
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'sync-roadmap-wiki.yml',
|
|
$exists && file_exists("{$wf}/sync-roadmap-wiki.yml"),
|
|
3
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'CI/deploy workflow',
|
|
$exists && (!empty(glob("{$wf}/ci*.yml")) || !empty(glob("{$wf}/deploy*.yml")) || !empty(glob("{$wf}/build*.yml"))),
|
|
4
|
|
);
|
|
}
|
|
|
|
// ── Security (20 pts) ────────────────────────────────────────────
|
|
|
|
private function checkSecurity(string $p): void
|
|
{
|
|
$cat = 'security';
|
|
$this->initCategory($cat, 'Security', 20);
|
|
|
|
$this->addCheck($cat, 'SECURITY.md', file_exists("{$p}/SECURITY.md"), 5);
|
|
|
|
$wf = is_dir("{$p}/.gitea/workflows") ? "{$p}/.gitea/workflows" : "{$p}/.github/workflows";
|
|
$hasScan = is_dir($wf) && (!empty(glob("{$wf}/*gitleaks*.yml")) || !empty(glob("{$wf}/*trivy*.yml"))
|
|
|| !empty(glob("{$wf}/*security*.yml")));
|
|
$this->addCheck($cat, 'Security scanning workflow', $hasScan, 5);
|
|
|
|
$this->addCheck(
|
|
$cat,
|
|
'No renovate.json (removed from ecosystem)',
|
|
!file_exists("{$p}/renovate.json"),
|
|
5
|
|
);
|
|
|
|
$secrets = false;
|
|
foreach (['.env', '.env.local', 'credentials.json'] as $s) {
|
|
if (file_exists("{$p}/{$s}")) {
|
|
$secrets = true;
|
|
break;
|
|
}
|
|
}
|
|
$this->addCheck($cat, 'No secret files committed', !$secrets, 5);
|
|
}
|
|
|
|
// ── Rulesets (15 pts) ────────────────────────────────────────────
|
|
|
|
private function checkRulesets(string $repo): void
|
|
{
|
|
$cat = 'rulesets';
|
|
$this->initCategory($cat, 'Branch Rulesets', 15);
|
|
|
|
if (!$repo) {
|
|
$this->addCheck($cat, 'Main branch protected', false, 5);
|
|
$this->addCheck($cat, 'Dev branch exists', false, 5);
|
|
$this->addCheck($cat, 'Branch rulesets configured', false, 5);
|
|
return;
|
|
}
|
|
|
|
$token = getenv('GH_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
|
|
if (!$token) {
|
|
$this->addCheck($cat, 'Main branch protected', false, 5);
|
|
$this->addCheck($cat, 'Dev branch exists', false, 5);
|
|
$this->addCheck($cat, 'Branch rulesets configured', false, 5);
|
|
return;
|
|
}
|
|
|
|
// Gitea uses /branch_protections endpoint (not /branches/main/protection)
|
|
$protections = $this->apiFetch("repos/{$repo}/branch_protections", $token);
|
|
$mainProtected = false;
|
|
foreach ($protections as $bp) {
|
|
if (($bp['branch_name'] ?? '') === 'main') {
|
|
$mainProtected = true;
|
|
break;
|
|
}
|
|
}
|
|
$this->addCheck($cat, 'Main branch protected', $mainProtected, 5);
|
|
|
|
$this->addCheck(
|
|
$cat,
|
|
'Dev branch exists',
|
|
$this->apiCheck("repos/{$repo}/branches/dev", $token),
|
|
5
|
|
);
|
|
|
|
$this->addCheck($cat, 'Branch protections configured', count($protections) > 0, 5);
|
|
}
|
|
|
|
// ── Deployment (10 pts) ──────────────────────────────────────────
|
|
|
|
private function checkDeployment(string $p): void
|
|
{
|
|
$cat = 'deployment';
|
|
$this->initCategory($cat, 'Deployment', 10);
|
|
|
|
$wf = is_dir("{$p}/.gitea/workflows") ? "{$p}/.gitea/workflows" : "{$p}/.github/workflows";
|
|
$this->addCheck(
|
|
$cat,
|
|
'Deploy workflow',
|
|
is_dir($wf) && !empty(glob("{$wf}/deploy*.yml")),
|
|
5
|
|
);
|
|
$this->addCheck(
|
|
$cat,
|
|
'Build system',
|
|
file_exists("{$p}/Makefile") || file_exists("{$p}/package.json") || file_exists("{$p}/composer.json"),
|
|
5
|
|
);
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
|
|
private function initCategory(string $key, string $name, int $maxPts): void
|
|
{
|
|
$this->results['categories'][$key] = [
|
|
'name' => $name, 'max_points' => $maxPts,
|
|
'earned_points' => 0, 'checks_passed' => 0, 'checks_failed' => 0,
|
|
];
|
|
}
|
|
|
|
private function addCheck(string $cat, string $name, bool $passed, int $pts): void
|
|
{
|
|
$this->results['checks'][] = ['category' => $cat, 'name' => $name, 'passed' => $passed, 'points' => $pts];
|
|
if ($passed) {
|
|
$this->results['categories'][$cat]['earned_points'] += $pts;
|
|
$this->results['categories'][$cat]['checks_passed']++;
|
|
} else {
|
|
$this->results['categories'][$cat]['checks_failed']++;
|
|
}
|
|
}
|
|
|
|
private function calculateScore(): void
|
|
{
|
|
$earned = $max = 0;
|
|
foreach ($this->results['categories'] as $c) {
|
|
$earned += $c['earned_points'];
|
|
$max += $c['max_points'];
|
|
}
|
|
$this->results['score'] = $earned;
|
|
$this->results['max_score'] = $max;
|
|
$this->results['percentage'] = $max > 0 ? ($earned / $max * 100) : 0;
|
|
$pct = $this->results['percentage'];
|
|
$this->results['level'] = match (true) {
|
|
$pct >= 90 => 'excellent', $pct >= 80 => 'good',
|
|
$pct >= 70 => 'fair', $pct >= 60 => 'poor', default => 'critical',
|
|
};
|
|
}
|
|
|
|
private function displayResults(): void
|
|
{
|
|
$this->section('Results');
|
|
foreach ($this->results['checks'] as $c) {
|
|
$this->status($c['passed'], $c['name'], $c['passed'] ? '' : "{$c['points']} pts");
|
|
}
|
|
$p = count(array_filter($this->results['checks'], fn($c) => $c['passed']));
|
|
$f = count(array_filter($this->results['checks'], fn($c) => !$c['passed']));
|
|
$this->printSummary($p, $f, $this->elapsed());
|
|
$this->log(sprintf(
|
|
"Score: %d/%d (%.1f%%) — %s",
|
|
$this->results['score'],
|
|
$this->results['max_score'],
|
|
$this->results['percentage'],
|
|
strtoupper($this->results['level'])
|
|
));
|
|
}
|
|
|
|
private function apiCheck(string $path, string $token): bool
|
|
{
|
|
$ch = curl_init("{$this->apiBaseUrl}/{$path}");
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => ["Authorization: token {$token}", 'User-Agent: moko-platform'],
|
|
]);
|
|
curl_exec($ch);
|
|
$s = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
return $s === 200;
|
|
}
|
|
|
|
private function apiFetch(string $path, string $token): array
|
|
{
|
|
$ch = curl_init("{$this->apiBaseUrl}/{$path}");
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_HTTPHEADER => ["Authorization: token {$token}", 'User-Agent: moko-platform'],
|
|
]);
|
|
$body = (string)curl_exec($ch);
|
|
$s = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
return $s === 200 ? (json_decode($body, true) ?? []) : [];
|
|
}
|
|
|
|
private function createHealthIssue(string $repo): void
|
|
{
|
|
$pct = round($this->results['percentage'], 1);
|
|
$title = "Repository Health: {$this->results['level']} ({$pct}%)";
|
|
$body = "## Health Check\n\nScore: {$this->results['score']}/{$this->results['max_score']} ({$pct}%)\n\n";
|
|
$body .= "| Category | Score |\n|---|---|\n";
|
|
foreach ($this->results['categories'] as $c) {
|
|
$body .= "| {$c['name']} | {$c['earned_points']}/{$c['max_points']} |\n";
|
|
}
|
|
$failed = array_filter($this->results['checks'], fn($c) => !$c['passed']);
|
|
if ($failed) {
|
|
$body .= "\n### Failed\n";
|
|
foreach ($failed as $c) {
|
|
$body .= "- {$c['name']} ({$c['points']}pts)\n";
|
|
}
|
|
}
|
|
$token = getenv('GH_TOKEN') ?: getenv('GITEA_TOKEN') ?: '';
|
|
if (!$token) {
|
|
return;
|
|
}
|
|
$ch = curl_init("{$this->apiBaseUrl}/repos/{$repo}/issues");
|
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode(['title' => $title, 'body' => $body]),
|
|
CURLOPT_HTTPHEADER => ["Authorization: token {$token}", 'Content-Type: application/json', 'User-Agent: moko-platform']]);
|
|
curl_exec($ch);
|
|
curl_close($ch);
|
|
}
|
|
}
|
|
|
|
$app = new RepoHealthChecker();
|
|
exit($app->execute());
|